前言列表能显示不代表它能在筛选后保持正确待办页面的任务卡片看起来是最容易写的部分遍历数组展示一行标题再放一个复选框。问题通常不在第一次渲染而在数据开始变化以后。新任务插到顶部复选框勾选到了另一项切换“进行中”后再切回来某张卡片仍保留旧样式搜索结果变化时列表滚动和内容区域互相挤压。要避免这些问题不能把ForEach当成“普通循环”。在可交互列表中它要和稳定身份、滚动容器、单项承载组件、状态写入路径一起设计。本文以鸿蒙 ArkUI 待办面板中的真实页面结构为例讲清List、ForEach、ListItem和任务卡片各自该负责什么以及为什么 key、布局权重和事件回调都是列表正确性的一部分。一、先决定谁负责滚动谁负责生成谁负责展示当前任务区域采用四层结构List └── ForEach(可见任务) └── ListItem └── TodoCard(一条任务)它们并非可以随意互换。List是列表容器负责承载一组可滚动的条目ForEach根据当前的任务数组生成多个子节点ListItem表示列表中的单个项目让每张卡片处于正确的列表语义中TodoCard负责一条任务的排版、文本和交互控件。当前页面代码如下List({space:12}){ForEach(this.getVisibleTodos(),(item:TodoItem):void{ListItem(){this.TodoCard(item);}},(item:TodoItem):stringtodo-${item.id});}.width(100%).height(100%).layoutWeight(1).edgeEffect(EdgeEffect.Spring);space: 12让卡片之间留出统一间距layoutWeight(1)让列表在顶部摘要和输入区占用高度后填满剩余空间edgeEffect(EdgeEffect.Spring)提供边缘滚动反馈。这里的重点不是具体数值而是把“可滚动内容区”限制在右侧主区域避免整个页面和列表同时争抢高度。二、稳定 key 为什么不能用数组下标代替ForEach的第三个参数为每条任务提供 key(item:TodoItem):stringtodo-${item.id}这行代码的意义不只是让语法完整。列表在新增、筛选、搜索和完成任务后都会重新排列。例如当前数组是[1, 2, 3]在顶部插入新任务后变成[4, 1, 2, 3]。如果 key 使用下标原先下标 0 对应任务 1插入后却对应任务 4框架复用节点时就可能把前一项的局部表现错误地带给后一项。任务id才是稳定身份。任务从“进行中”切换到“已完成”它的数组位置和是否可见会变但id不变。因此 key 使用todo-${item.id}能把渲染节点与任务身份绑定而不是与短暂位置绑定。下面的写法应尽量避免// 不建议排序、筛选或插入后下标含义会变化ForEach(this.getVisibleTodos(),(item,index){// ...},(item,index)index.toString());只有列表元素天然没有身份、永远不会插入删除重排时下标才勉强可用。待办任务显然不满足这个前提。三、把每张卡片做成只依赖 item 的展示单元当前工程通过Builder定义TodoCard(item)将一条任务的显示集中起来左边是复选框中间是标题、分类和时间标签右边是优先级标签。BuilderprivateTodoCard(item:TodoItem):void{Row({space:14}){Checkbox({name:todo-${item.id},group:todos}).select(item.done).selectedColor($r(app.color.primary)).onChange((value:boolean):void{this.toggleTodo(item.id,value);});Column({space:6}){Text(item.title).fontSize(17).fontWeight(FontWeight.Medium).fontColor(item.done?$r(app.color.text_done):$r(app.color.text_primary)).decoration({type:item.done?TextDecorationType.LineThrough:TextDecorationType.None});// 分类与时间标签}.alignItems(HorizontalAlign.Start).layoutWeight(1);Text(${item.priority}优先级).fontSize(12).fontColor(this.priorityColor(item.priority));}.width(100%).padding(18).borderRadius(14).backgroundColor($r(app.color.card_background));}卡片拿到的输入是item由它决定标题、完成样式、分类、时间和优先级。卡片不再另外声明一个State checked来保存勾选结果复选框的select(item.done)永远从任务源数据读取。事件发生时把新的布尔值交给页面层由页面统一更新数组。这是一条很实用的规则卡片负责展示和发出动作页面负责修改集合。一旦让每张卡片维护自己的完成状态筛选、复用、重新渲染时就容易产生两份不同的真相。四、完成状态不只改变复选框勾选任务后用户应该立即能理解发生了什么。当前卡片通过文本颜色和删除线表达完成状态.fontColor(item.done?$r(app.color.text_done):$r(app.color.text_primary)).decoration({type:item.done?TextDecorationType.LineThrough:TextDecorationType.None});这样做有两个好处。第一任务是否完成不依赖用户是否注意到小复选框第二文本仍然存在用户可以回顾已完成事项而不是把它们彻底隐藏。颜色来自资源文件中的text_primary、text_done等命名资源而非散落的十六进制值后续调整主题时页面逻辑不需要跟着修改。优先级也采用文字加颜色的组合高优先级、普通优先级、低优先级。仅用红黄绿圆点对色觉差异用户不够友好文字能补足含义。注意优先级颜色由priorityColor()映射而不是让卡片自己判断一长串样式分支同一种业务规则应有集中出口。五、中间内容为什么要使用 layoutWeight一条卡片内部有固定宽度倾向的控件左侧复选框、右侧优先级标签。标题和分类区域的宽度则应该随可用空间变化因此中间Column使用.layoutWeight(1).alignItems(HorizontalAlign.Start)layoutWeight(1)让中间区域占用剩余宽度。没有它时长标题可能和右侧标签挤在一起把宽度写死又会在不同窗口尺寸中留下空洞或发生截断。宽屏待办面板的核心不是给每个元素设置一堆固定像素而是识别哪些部分固定、哪些部分弹性。页面外层也遵循同一逻辑左侧导航设置 220 的宽度右侧内容列layoutWeight(1)右侧顶部的标题区域再layoutWeight(1)搜索框使用固定的 260 宽度。层层分配后搜索框和添加按钮保持可用任务列表仍能拿到剩余空间。六、列表只渲染“当前可见任务”当前ForEach的输入不是this.todos而是this.getVisibleTodos()。这意味着列表组件不关心用户是因为筛选、搜索还是任务完成才看到这一批卡片它只接收已经计算好的可见结果。ForEach(this.getVisibleTodos(),(item:TodoItem):void{ListItem(){this.TodoCard(item);}},(item:TodoItem):stringtodo-${item.id});这种分工很重要。若在卡片里再判断“这是不是已完成”“标题是否包含搜索词”一方面重复了查询规则另一方面会让列表计数、空状态和实际卡片采用不同条件。筛选属于页面层的派生数据卡片只展示传进来的任务。当getVisibleTodos().length 0时页面不创建空List而是显示一个解释性的空状态。它告诉用户可以清空搜索词、切换条件或添加任务。对列表来说这不是额外装饰而是和数据结果同样重要的分支没有匹配项与还没有任何任务是两种不同情境。七、从事件到重绘复选框应该走完整链路勾选一项任务时推荐按以下路径理解Checkbox.onChange(value) ↓ toggleTodo(item.id, value) ↓ this.todos 新数组 ↓ getVisibleTodos() 重新计算 ↓ ForEach 重新根据稳定 key 渲染卡片这条路径说明为什么不必在TodoCard中手动处理“从进行中列表移除”。只要done已写回todos当当前筛选是“进行中”时该任务自然不再满足条件当切换到“已完成”时又会自然出现在相应列表。UI 的变化来自数据和规则而不是多个组件互相通知。八、建议按这些场景检查列表是否可靠新增任务到顶部新增后原有卡片的勾选状态和内容不能错位这能检查 key 是否稳定。筛选后勾选任务在“进行中”中勾选当前卡片应消失计数同步变化这能检查事件是否回写源数组。搜索后切换筛选列表只显示同时满足两个条件的项这能检查ForEach是否使用了统一派生结果。长标题标题区域应获得弹性空间右侧优先级标签不应被挤出容器这能检查layoutWeight的分配。没有匹配项应显示可理解的空状态而不是一片白色区域这能检查结果分支是否完整。已完成任务复选框、删除线、颜色应同时反映done避免只有一种视觉提示。当前模拟器画面这张图对应当前模拟器已验证的运行场景它不替代签名、发布链接或未实现功能的验证。小结鸿蒙 ArkUI 中的List ForEach不只是把数组“画出来”。稳定 key 让节点跟随任务身份ListItem保持每条记录的列表结构TodoCard集中展示一条任务layoutWeight处理弹性空间复选框事件则回到页面层修改唯一任务数组。把这些职责分清后筛选、搜索、新增和完成状态变化都不会破坏列表。等以后再加入编辑、删除或拖拽排序时也能继续围绕稳定id和单一数据源扩展而不必推倒当前的卡片结构。