鸿蒙ArkUI实战:为待办面板划清数据模型与状态边界

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


所有评论(0)