
前言本节课核心概念状态提升State‑Hoisting以及业务组件抽取。把上一课Todo清单里面的列表条目单独抽成独立可复用Compose组件将状态向上提升子组件只接收参数与回调子组件内部不持有remember状态实现UI组件与业务状态分离。知识点清单什么是状态提升 State‑Hoisting抽取独立的TodoItemCard子组件子组件不定义remember状态使用回调lambda向上传递交互事件勾选、删除父组件维护数据源与全部业务状态重构原有Todo页面拆分父页面与子条目组件完整示例源码importandroidx.compose.foundation.layout.Arrangementimportandroidx.compose.foundation.layout.Rowimportandroidx.compose.foundation.layout.fillMaxSizeimportandroidx.compose.foundation.layout.fillMaxWidthimportandroidx.compose.foundation.layout.paddingimportandroidx.compose.foundation.lazy.LazyColumnimportandroidx.compose.foundation.lazy.itemsimportandroidx.compose.material.icons.Iconsimportandroidx.compose.material.icons.filled.Deleteimportandroidx.compose.material3.Buttonimportandroidx.compose.material3.Cardimportandroidx.compose.material3.CardDefaultsimportandroidx.compose.material3.Checkboximportandroidx.compose.material3.Iconimportandroidx.compose.material3.IconButtonimportandroidx.compose.material3.Scaffoldimportandroidx.compose.material3.Textimportandroidx.compose.material3.TextFieldimportandroidx.compose.material3.TopAppBarimportandroidx.compose.material3.AnimatedVisibilityimportandroidx.compose.runtime.Composableimportandroidx.compose.runtime.mutableStateListOfimportandroidx.compose.runtime.mutableStateOfimportandroidx.compose.runtime.rememberimportandroidx.compose.runtime.getValueimportandroidx.compose.runtime.setValueimportandroidx.compose.ui.Modifierimportandroidx.compose.ui.text.style.TextDecorationimportandroidx.compose.ui.unit.dpdataclassTodoItem(valid:Int,valcontent:String,valisFinished:Boolean)/** * 子组件单条待办卡片不持有内部状态状态由父组件传入 */ComposablefunTodoItemCard(todo:TodoItem,onCheckChange:(Boolean)-Unit,onDelete:()-Unit,modifier:ModifierModifier){Card(modifiermodifier,elevationCardDefaults.cardElevation(3.dp)){Row(modifierModifier.fillMaxWidth().padding(12.dp),horizontalArrangementArrangement.SpaceBetween){Row{Checkbox(checkedtodo.isFinished,onCheckedChangeonCheckChange)Text(texttodo.content,modifierModifier.padding(top10.dp),textDecorationif(todo.isFinished)TextDecoration.LineThroughelseTextDecoration.None)}IconButton(onClickonDelete){Icon(Icons.Default.Delete,contentDescription删除)}}}}/** * 父页面组件维护全部数据源与业务状态 */ComposablefunTodoRefactorPage(){varinputTextbyremember{mutableStateOf()}valtodoListremember{mutableStateListOfTodoItem()}Scaffold(topBar{TopAppBar(title{Text(重构版‑待办清单(状态提升))})}){innerPadding-Column(modifierModifier.fillMaxSize().padding(innerPadding).padding(16.dp),verticalArrangementArrangement.spacedBy(14.dp)){Row(modifierModifier.fillMaxWidth(),horizontalArrangementArrangement.spacedBy(10.dp)){TextField(valueinputText,onValueChange{inputTextit},placeholder{Text(输入待办任务)},modifierModifier.weight(1f))Button(onClick{if(inputText.isNotBlank()){valnewIdtodoList.size1todoList.add(TodoItem(newId,inputText,false))inputText}}){Text(新增)}}AnimatedVisibility(visibletodoList.isEmpty()){Card(modifierModifier.fillMaxWidth(),elevationCardDefaults.cardElevation(4.dp)){Text(暂无待办任务请添加,modifierModifier.padding(20.dp))}}LazyColumn(modifierModifier.weight(1f),verticalArrangementArrangement.spacedBy(10.dp)){items(todoList,key{it.id}){todo-TodoItemCard(todotodo,onCheckChange{checked-validxtodoList.indexOf(todo)todoList[idx]todo.copy(isFinishedchecked)},onDelete{todoList.remove(todo)},modifierModifier.fillMaxWidth())}}}}}逐行解读状态提升 State‑Hoisting 核心思想尽量把子组件里面的 remember 向上移动到上层父组件子组件只接收数据参数通过lambda回调把点击、勾选事件向上通知父组件子组件不要自己修改业务数据。TodoItemCard子组件分析参数接收实体 todo:TodoItem 向下传入数据onCheckChange:(Boolean)-Unit 勾选回调勾选之后向上通知父页面onDelete:()-Unit 删除点击回调modifier 作为可选形参是Compose组件的惯例写法方便父组件控制子组件布局样式TodoItemCard内部没有任何remember/mutableStateOf属于纯UI展示组件。父组件TodoRefactorPage所有数据源 todoList 、输入框 inputText 依然定义在父页面当子条目勾选或者点击删除通过lambda回调执行父组件的业务逻辑修改状态集合状态变化触发重组刷新子卡片UI。抽取组件带来的好处① 代码分层页面主体不会过于臃肿② TodoItemCard可以在别的页面直接复用③ 方便单独预览、单独测试卡片UI④ 业务逻辑集中在父层数据修改入口统一不容易出现状态混乱。容易踩坑❌ 抽取子组件之后又在子组件内部写remember维护业务状态会造成父子两份状态不一致。❌ 忘记传递modifier参数后续父层想要控制子组件的padding、fillMaxWidth时无法设置。❌ 在子组件内部直接修改todo对象字段依然坚持不可变原则数据修改交给父组件回调处理。✅ 惯例Compose自定义组件形参最后一位放上 modifier:ModifierModifier 默认值。多元化习题每道题后紧跟答案解读习题1【基础‑改错题】题目下面是错误的子组件写法违背状态提升说明问题并修正思路不需要完整写代码ComposablefunBadTodoCard(todo:TodoItem){varlocalCheckedbyremember{mutableStateOf(todo.isFinished)}Checkbox(checkedlocalChecked,onCheckedChange{localCheckedit})}答案错误子组件内部新建了一份localChecked本地状态后续父组件数据源更新的时候子本地状态不会同步出现数据不一致。修正删除子组件里面 remember 本地状态勾选状态由父组件通过入参todo传入增加回调lambda onCheckChange:(Boolean)-Unit 勾选事件向上交给父组件修改原始数据源。解读子组件复制一份业务状态保存会出现两份数据源当页面其它逻辑修改父层数据子UI不会跟着刷新这是开发中高频bug来源是状态提升要解决的典型问题。习题2【基础‑代码修改题】题目给TodoItemCard子组件增加额外的modifier使用在父层调用的时候给卡片增加 .padding(horizontal 8.dp) 边距写出父层调用TodoItemCard的调用片段。答案TodoItemCard(todotodo,onCheckChange{checked-validxtodoList.indexOf(todo)todoList[idx]todo.copy(isFinishedchecked)},onDelete{todoList.remove(todo)},modifierModifier.fillMaxWidth().padding(horizontal8.dp))解读modifier从父组件传入子组件内部接收之后绑定到Card父层就可以灵活控制边距、尺寸这是Compose自定义组件的标准习惯。习题3【进阶‑补充代码题】题目继续抽取顶部输入区域把新增任务输入行单独抽成 TodoInputRow 子组件需要向上回调提交文本写出该子组件签名函数头。答案ComposablefunTodoInputRow(onSubmit:(String)-Unit,modifier:ModifierModifier){}解读输入框的输入状态inputText此时有两种选择方案A输入状态依然保留在父组件完整状态提升value与onValueChange都回调方案B短期临时输入状态保留在TodoInputRow内部提交的时候把字符串通过onSubmit向上抛输入框这种临时UI状态允许留在子组件业务数据源必须提升到上层。习题4【综合‑逻辑应用题】题目简述业务规则哪些状态适合向上提升放到父组件哪些临时UI状态可以保留在子组件内部。答案业务数据源状态列表数据、实体内容、勾选标记会被多个组件共享访问的数据尽量状态提升放到上层父组件短暂的临时UI状态例如输入框正在输入的文字、下拉菜单是否展开、本组件内部弹窗显示标记只服务当前组件不对外共享允许保留在子组件内部使用remember。解读状态提升不是要求所有状态一律上移区分「业务共享状态」和「组件本地临时UI状态」盲目全部上移会造成父组件状态爆炸维护负担加重。习题5【概念简答题】题目请解释什么是状态提升State‑Hoisting以及状态提升带来的开发收益。答案状态提升把子组件中维护的业务状态向上移动到上层父组件子组件接收数据作为入参交互事件通过lambda回调向上通知父层修改数据。收益业务数据修改入口集中减少多份副本带来的数据不同步bugUI组件可以复用页面业务和UI视图分离代码更容易维护。解读状态提升是Compose声明式UI开发非常核心的设计思想写复杂页面、拆分多个自定义组件时必须遵循。本节课知识点总结状态提升 State‑Hoisting业务状态向上移动至父组件子组件接收数据lambda回调上报交互。自定义Compose组件惯例最后参数声明 modifier:ModifierModifier 方便父层控制布局。子组件尽量不要新建remember来保存一份业务数据副本容易造成父子状态不一致。状态提升不是绝对组件内部仅使用的临时UI状态下拉展开标记、输入临时文本允许留在子组件。将页面UI进行组件拆分之后页面代码更加清晰抽取出来的子卡片可以跨页面复用。下一课预告第20课 回顾总结 基础阶段综合复习Jetpack‑Compose基础完结