前言任务卡片变复杂后先别急着加字段待办列表最初往往只有标题和完成状态。做着做着需求开始增加要显示分类、截止时间、优先级要支持筛选和搜索输入框里还要暂存一条尚未提交的新任务。很多页面就是从这里开始失控的——任务对象里混入搜索词卡片里保存勾选状态筛选后又维护一份“已完成列表”最后一个点击动作要改三四个地方。解决方法不是把类型写得更长而是在写字段之前先问一句这个值是任务本身的事实还是当前页面的观看方式还是尚未提交的用户输入三者放错位置后面再漂亮的 UI 也会被同步问题拖住。当前鸿蒙待办面板把这个边界控制在一个TodoItem接口和少量页面状态里。本文从实际的字段设计出发说明哪些值应该进入任务模型哪些值只应该存在于页面状态为什么列表结果和计数不应该被重复保存以及修改数组时为什么要让写入路径保持集中。一、先把“任务是什么”定义清楚当前工程为任务定义了以下类型typeTodoPriority高|普通|低;interfaceTodoItem{id:number;title:string;category:string;dueLabel:string;priority:TodoPriority;done:boolean;}这六个字段都描述同一条任务本身。id稳定身份用于更新和列表 keytitle任务要做什么category任务归属的业务分类dueLabel当前页面展示的时间标签priority预先约束为高、普通、低避免任意字符串扩散done任务是否完成是筛选和视觉样式的重要依据。其中最容易被低估的是id。页面一开始只有三条静态数据时使用数组下标似乎也能找到任务但新任务插到顶部、列表被筛选、以后再加入排序时下标代表的对象就会变化。稳定id是任务的身份不是它当前排在第几位。TodoPriority采用联合类型而不是任意string是另一个小但有用的约束。当前颜色方法只处理“高”“普通”“低”三种优先级privatepriorityColor(priority:TodoPriority):ResourceColor{if(priority高){return$r(app.color.priority_high);}if(priority低){return$r(app.color.priority_low);}return$r(app.color.priority_normal);}如果外部随意传入“紧急”“中等”之类的值页面不会知道该怎样展示。先收紧模型的可选范围比在每个渲染位置补一层兜底更可靠。二、区分源数据、页面条件与临时输入任务模型之外当前页面还维护了几类状态typeTodoFilter全部|进行中|已完成;Stateprivatetodos:TodoItem[][];Stateprivatefilter:TodoFilter全部;Stateprivatekeyword:string;StateprivatenewTitle:string;privatenextId:number4;可以按下面的方式理解它们类别代表值应该回答的问题源数据todos当前有哪些任务它们各自是什么状态页面条件filter、keyword用户此刻想看哪一部分任务临时输入newTitle用户正在编辑、但尚未提交什么内容内部辅助值nextId下一条内存任务该分配哪个编号这里有一个决定性的边界搜索词不属于任务筛选条件也不属于任务。用户把搜索框从“文章”改成“验证”并不意味着所有任务的分类被修改用户点了“已完成”也不意味着未完成任务从数据源消失。它们只改变当前视图的选择方式。同理newTitle也不应该立刻写进todos。只有用户点击添加或提交输入后经过trim()校验才创建新的TodoItem。这样用户输入到一半后取消、切换筛选、清空文本都不会污染已存在的任务数据。三、不要给同一事实保存第二份副本下面这种设计表面直观实际上非常危险// 不建议同一批任务被存成多份状态StateprivateallTodos:TodoItem[][];StateprivateactiveTodos:TodoItem[][];StateprivatecompletedTodos:TodoItem[][];StateprivatecompletedCount:number0;勾选一条任务时你必须同时更新allTodos、从activeTodos移除、向completedTodos插入、修改completedCount。新增、删除、恢复、搜索后又会出现更多同步分支。只要有一个按钮漏掉某个更新页面不同区域就会互相矛盾。当前实现只保存todos列表结果和数量通过方法计算privategetRemainingTodoCount():number{returnthis.todos.filter((item:TodoItem):boolean!item.done).length;}privatefilterCount(filter:TodoFilter):number{if(filter全部){returnthis.todos.length;}if(filter进行中){returnthis.getRemainingTodoCount();}returnthis.todos.length-this.getRemainingTodoCount();}这段代码每次都会遍历数组。对于当前这样的轻量待办列表换来的是更清楚的正确性计数永远从任务事实得出不需要猜测某个局部变量是否已经同步。如果未来任务量非常大需要优化再基于真实的性能测量决定是否缓存不要在没有瓶颈时先引入多份状态。四、修改任务时使用一个明确的写入入口任务卡片上的复选框会触发完成状态变化。当前工程没有让卡片直接修改自己持有的对象而是把id和新的done值交给页面的toggleTodo()privatetoggleTodo(id:number,done:boolean):void{this.todosthis.todos.map((item:TodoItem):TodoItem{if(item.idid){return{id:item.id,title:item.title,category:item.category,dueLabel:item.dueLabel,priority:item.priority,done:done};}returnitem;});}这里包含两层意图。第一id让更新定位到任务身份而不是当前数组位置。第二重新赋值给this.todos让状态变化入口一眼可见。以后查看 bug 时搜索toggleTodo就能知道“完成状态是在哪里改的”。写成原地修改也许更短例如直接给某项赋值item.done done但当页面数据被多个区域读取、以后又要接入持久化时明确地生成新数组更容易追踪变化。这里并不是说所有业务代码都不能改对象而是建议在状态边界上保留清晰的输入和输出。五、新建任务不要跳过模型约束新增动作把输入框临时状态转换为真正任务必须经过一次明确的“提交边界”。当前方法先清理空白字符空标题直接返回成功后再补齐模型要求的分类、时间标签、优先级和完成状态privateaddTodo():void{consttitlethis.newTitle.trim();if(title.length0){return;}this.todos[{id:this.nextId,title:title,category:收集箱,dueLabel:待安排,priority:普通,done:false},...this.todos];this.nextId1;this.newTitle;this.filter全部;}这段代码解决了三个实际体验问题空白输入不会生成空卡片新任务有完整字段渲染卡片时不必到处判断undefined添加后切到“全部”用户能立即看到新增结果。后续增加日期选择、分类选择或优先级选择时也应该在提交边界组装完整对象而不是先插入半成品、再由多个组件分别填字段。六、哪些东西现在不该硬塞进 TodoItem为了避免模型膨胀下面几类值暂时不适合进入当前任务接口当前筛选页签它描述页面不描述任务搜索词它描述一次查询不描述任务卡片是否处于按下、悬停或展开状态它属于 UI 局部交互列表中第几项它会随筛选和排序变化“是否已保存到本地”如果未来需要可由数据层的同步状态统一描述不能混淆为任务完成状态组件颜色当前工程把颜色放在资源文件中用priorityColor()映射而不是把十六进制颜色写入每条任务。模型越靠近业务事实越容易被不同页面复用。比如将来做“今日任务”“按分类统计”“任务详情”都可以继续读取同一份TodoItem而搜索框内容、卡片展开状态则由各自页面负责。七、用几个边界场景检查模型有没有放错位置判断状态边界是否合理可以用以下场景做自检。场景一先搜索再勾选。在“进行中”中搜索一个标题勾选匹配任务后列表应该根据同一筛选与搜索条件重新计算。若卡片还留在当前列表通常说明局部状态没有回写到todos。场景二先添加再切换页签。新任务默认未完成添加后在“全部”和“进行中”可见在“已完成”不可见。若只能在某一个页签看到说明新增逻辑写进了派生结果而不是源数组。场景三搜索无结果。搜索不到任务时todos不应变空清空关键词后原任务仍出现。若清空后无法恢复说明搜索动作错误地改写了源数据。场景四任务顺序变化。新任务插到头部后勾选旧任务仍应定位正确。这能检查是否使用稳定id而不是数组下标。小结待办页面的数据设计可以浓缩成一句话任务模型保存任务事实页面状态保存用户当前意图临时输入只在提交时转为任务列表和计数一律从源数据派生。当前TodoItem虽然简单却已经给后续扩展留下了正确边界。将来加入编辑、删除、持久化时优先继续保持“一个任务源、一条写入路径、多处派生展示”的结构这样新功能增加的是能力而不是新的同步负担。
鸿蒙ArkUI实战:为待办面板划清数据模型与状态边界
前言任务卡片变复杂后先别急着加字段待办列表最初往往只有标题和完成状态。做着做着需求开始增加要显示分类、截止时间、优先级要支持筛选和搜索输入框里还要暂存一条尚未提交的新任务。很多页面就是从这里开始失控的——任务对象里混入搜索词卡片里保存勾选状态筛选后又维护一份“已完成列表”最后一个点击动作要改三四个地方。解决方法不是把类型写得更长而是在写字段之前先问一句这个值是任务本身的事实还是当前页面的观看方式还是尚未提交的用户输入三者放错位置后面再漂亮的 UI 也会被同步问题拖住。当前鸿蒙待办面板把这个边界控制在一个TodoItem接口和少量页面状态里。本文从实际的字段设计出发说明哪些值应该进入任务模型哪些值只应该存在于页面状态为什么列表结果和计数不应该被重复保存以及修改数组时为什么要让写入路径保持集中。一、先把“任务是什么”定义清楚当前工程为任务定义了以下类型typeTodoPriority高|普通|低;interfaceTodoItem{id:number;title:string;category:string;dueLabel:string;priority:TodoPriority;done:boolean;}这六个字段都描述同一条任务本身。id稳定身份用于更新和列表 keytitle任务要做什么category任务归属的业务分类dueLabel当前页面展示的时间标签priority预先约束为高、普通、低避免任意字符串扩散done任务是否完成是筛选和视觉样式的重要依据。其中最容易被低估的是id。页面一开始只有三条静态数据时使用数组下标似乎也能找到任务但新任务插到顶部、列表被筛选、以后再加入排序时下标代表的对象就会变化。稳定id是任务的身份不是它当前排在第几位。TodoPriority采用联合类型而不是任意string是另一个小但有用的约束。当前颜色方法只处理“高”“普通”“低”三种优先级privatepriorityColor(priority:TodoPriority):ResourceColor{if(priority高){return$r(app.color.priority_high);}if(priority低){return$r(app.color.priority_low);}return$r(app.color.priority_normal);}如果外部随意传入“紧急”“中等”之类的值页面不会知道该怎样展示。先收紧模型的可选范围比在每个渲染位置补一层兜底更可靠。二、区分源数据、页面条件与临时输入任务模型之外当前页面还维护了几类状态typeTodoFilter全部|进行中|已完成;Stateprivatetodos:TodoItem[][];Stateprivatefilter:TodoFilter全部;Stateprivatekeyword:string;StateprivatenewTitle:string;privatenextId:number4;可以按下面的方式理解它们类别代表值应该回答的问题源数据todos当前有哪些任务它们各自是什么状态页面条件filter、keyword用户此刻想看哪一部分任务临时输入newTitle用户正在编辑、但尚未提交什么内容内部辅助值nextId下一条内存任务该分配哪个编号这里有一个决定性的边界搜索词不属于任务筛选条件也不属于任务。用户把搜索框从“文章”改成“验证”并不意味着所有任务的分类被修改用户点了“已完成”也不意味着未完成任务从数据源消失。它们只改变当前视图的选择方式。同理newTitle也不应该立刻写进todos。只有用户点击添加或提交输入后经过trim()校验才创建新的TodoItem。这样用户输入到一半后取消、切换筛选、清空文本都不会污染已存在的任务数据。三、不要给同一事实保存第二份副本下面这种设计表面直观实际上非常危险// 不建议同一批任务被存成多份状态StateprivateallTodos:TodoItem[][];StateprivateactiveTodos:TodoItem[][];StateprivatecompletedTodos:TodoItem[][];StateprivatecompletedCount:number0;勾选一条任务时你必须同时更新allTodos、从activeTodos移除、向completedTodos插入、修改completedCount。新增、删除、恢复、搜索后又会出现更多同步分支。只要有一个按钮漏掉某个更新页面不同区域就会互相矛盾。当前实现只保存todos列表结果和数量通过方法计算privategetRemainingTodoCount():number{returnthis.todos.filter((item:TodoItem):boolean!item.done).length;}privatefilterCount(filter:TodoFilter):number{if(filter全部){returnthis.todos.length;}if(filter进行中){returnthis.getRemainingTodoCount();}returnthis.todos.length-this.getRemainingTodoCount();}这段代码每次都会遍历数组。对于当前这样的轻量待办列表换来的是更清楚的正确性计数永远从任务事实得出不需要猜测某个局部变量是否已经同步。如果未来任务量非常大需要优化再基于真实的性能测量决定是否缓存不要在没有瓶颈时先引入多份状态。四、修改任务时使用一个明确的写入入口任务卡片上的复选框会触发完成状态变化。当前工程没有让卡片直接修改自己持有的对象而是把id和新的done值交给页面的toggleTodo()privatetoggleTodo(id:number,done:boolean):void{this.todosthis.todos.map((item:TodoItem):TodoItem{if(item.idid){return{id:item.id,title:item.title,category:item.category,dueLabel:item.dueLabel,priority:item.priority,done:done};}returnitem;});}这里包含两层意图。第一id让更新定位到任务身份而不是当前数组位置。第二重新赋值给this.todos让状态变化入口一眼可见。以后查看 bug 时搜索toggleTodo就能知道“完成状态是在哪里改的”。写成原地修改也许更短例如直接给某项赋值item.done done但当页面数据被多个区域读取、以后又要接入持久化时明确地生成新数组更容易追踪变化。这里并不是说所有业务代码都不能改对象而是建议在状态边界上保留清晰的输入和输出。五、新建任务不要跳过模型约束新增动作把输入框临时状态转换为真正任务必须经过一次明确的“提交边界”。当前方法先清理空白字符空标题直接返回成功后再补齐模型要求的分类、时间标签、优先级和完成状态privateaddTodo():void{consttitlethis.newTitle.trim();if(title.length0){return;}this.todos[{id:this.nextId,title:title,category:收集箱,dueLabel:待安排,priority:普通,done:false},...this.todos];this.nextId1;this.newTitle;this.filter全部;}这段代码解决了三个实际体验问题空白输入不会生成空卡片新任务有完整字段渲染卡片时不必到处判断undefined添加后切到“全部”用户能立即看到新增结果。后续增加日期选择、分类选择或优先级选择时也应该在提交边界组装完整对象而不是先插入半成品、再由多个组件分别填字段。六、哪些东西现在不该硬塞进 TodoItem为了避免模型膨胀下面几类值暂时不适合进入当前任务接口当前筛选页签它描述页面不描述任务搜索词它描述一次查询不描述任务卡片是否处于按下、悬停或展开状态它属于 UI 局部交互列表中第几项它会随筛选和排序变化“是否已保存到本地”如果未来需要可由数据层的同步状态统一描述不能混淆为任务完成状态组件颜色当前工程把颜色放在资源文件中用priorityColor()映射而不是把十六进制颜色写入每条任务。模型越靠近业务事实越容易被不同页面复用。比如将来做“今日任务”“按分类统计”“任务详情”都可以继续读取同一份TodoItem而搜索框内容、卡片展开状态则由各自页面负责。七、用几个边界场景检查模型有没有放错位置判断状态边界是否合理可以用以下场景做自检。场景一先搜索再勾选。在“进行中”中搜索一个标题勾选匹配任务后列表应该根据同一筛选与搜索条件重新计算。若卡片还留在当前列表通常说明局部状态没有回写到todos。场景二先添加再切换页签。新任务默认未完成添加后在“全部”和“进行中”可见在“已完成”不可见。若只能在某一个页签看到说明新增逻辑写进了派生结果而不是源数组。场景三搜索无结果。搜索不到任务时todos不应变空清空关键词后原任务仍出现。若清空后无法恢复说明搜索动作错误地改写了源数据。场景四任务顺序变化。新任务插到头部后勾选旧任务仍应定位正确。这能检查是否使用稳定id而不是数组下标。小结待办页面的数据设计可以浓缩成一句话任务模型保存任务事实页面状态保存用户当前意图临时输入只在提交时转为任务列表和计数一律从源数据派生。当前TodoItem虽然简单却已经给后续扩展留下了正确边界。将来加入编辑、删除、持久化时优先继续保持“一个任务源、一条写入路径、多处派生展示”的结构这样新功能增加的是能力而不是新的同步负担。