ArkUI + HarmonyOS 页面任务流:结果回传与列表局部刷新(连载02/04)【共创稿事节】
任务列表与任务详情都能正常展示,反而容易让人忽略一个细节:用户在详情里点击“标记完成”,再返回列表,原来那一行依然写着“待处理”。按钮没有失效,路由也没有走错。真正的问题是,详情修改了仓储中的状态,列表却仍持有进入详情之前那份数组副本。业务数据已改变,不代表页面本地状态会自动重建。

上一篇把工程的三条边界定了下来:Navigation 容纳列表和子页面;NavPathStack 表达进出顺序;仓储根据 taskId 查找与更新实体。本篇继续使用同一组示例任务 tk-101、tk-102、tk-103,不额外引入全局事件总线,也不把详情页的一整份对象塞进路由参数。我们要补上的,是一条明确、可验证的操作结果返回链路。
返回动作和业务结果,本来就不是一回事
在导航系统看来,pop() 的职责是让当前子页面退出。它不知道用户刚才做的是提交、取消还是看完了文本。若把“只要返回就重新查询全部业务数据”设为统一规则,虽然能掩盖旧状态,却可能制造多余查询、列表闪动和滚动位置变化。反过来,完全不刷新也不行:用户完成任务后看不到结果,会怀疑操作是否生效。
这里把详情返回事件分成三种:changed 表示仓储确实发生变化,需要更新列表;unchanged 表示用户只是浏览或取消;invalid 表示任务不存在或参数不合法。结果本身只携带 taskId 和操作标识,列表根据仓储中的最新记录决定显示内容。这样回传的是“发生了什么”,不是又复制一套可能已过期的任务实体。
本次使用的是 NavPathStack.pushPath 的路由信息中提供的 onPop 回调,详情通过 NavPathStack.pop(result) 传回对象。华为官方的 Navigation 传参与接收 FAQ 同时介绍了 onPop 与 NavDestination.onResult:前者和指定入栈操作绑定,后者更适合承接多条返回路径。在当前只有列表到详情这一条主要路径的示例里,onPop 是一条够直白的路。若未来出现跨级返回、弹窗与多入口并存,应评估 onResult 的触发范围,而不是继续堆叠回调。

结果契约先定好,再修改页面
先在上一篇 model/TaskModel.ets 旁增加契约,原来的 TaskItem、TaskRepository、TaskRouteParams 保持不变。建议把新的定义放到 model/TaskResult.ets,让入栈和出栈两端引用同一个结构。一个 TaskChangeResult 表达一次详情页操作的最终状态,不承担导航规则以外的隐式任务。
// model/TaskResult.ets
export type TaskReturnAction = 'changed' | 'unchanged' | 'invalid';
export class TaskChangeResult {
taskId: string;
action: TaskReturnAction;
constructor(taskId: string, action: TaskReturnAction) {
this.taskId = taskId;
this.action = action;
}
}
export function isTaskChanged(result: TaskChangeResult | undefined,
expectedId: string): boolean {
return result !== undefined &&
result.action === 'changed' &&
result.taskId === expectedId;
}
为什么校验返回 ID?因为用户可能快速点开不同任务,列表可能正在按筛选条件重排;只根据“某个页面返回 changed”就去更新被选中的那一行,容易改错目标。我们的列表索引仍以 task.id 为稳定键,入栈时记录的 expectedId 则是一道简单的交叉校验。它不是安全权限控制,也不能替代仓储检索,但有助于防止页面链路之间混淆业务身份。
详情页可以沿用上一版的 TaskRepository.find() 和 TaskRepository.complete()。这里将“确认修改并返回”的动作封装成一个有明确出口的函数,而不是在按钮中先修改 UI 再临时猜测返回路径。
// view/TaskDetailPage.ets:加入既有 TaskDetailPage 组件
import { TaskChangeResult } from '../model/TaskResult';
private finishTask(): void {
if (!this.taskId || !TaskRepository.find(this.taskId)) {
this.taskStack.pop(new TaskChangeResult(this.taskId, 'invalid'));
return;
}
const changed: boolean = TaskRepository.complete(this.taskId);
this.taskStack.pop(new TaskChangeResult(
this.taskId, changed ? 'changed' : 'unchanged'));
}
private leaveWithoutEdit(): void {
this.taskStack.pop(new TaskChangeResult(this.taskId, 'unchanged'));
}
把原来 Button('标记完成') 的点击事件改成 this.finishTask(),再增加一个“取消并返回”按钮调用 this.leaveWithoutEdit()。这里的代码段是对前一篇已有组件的局部替换,并非独立放在文件顶层执行的函数:它依赖组件已有的 taskId、taskStack 及 TaskRepository 导入。complete() 原实现只要找到任务就返回 true,即使该任务原本已完成也会返回 true。若实际项目需要只在状态真正变化时通知,可以将仓储更新改成具备幂等语义的形式。
// model/TaskModel.ets:替换 TaskRepository.complete
static complete(id: string): boolean {
const item = TaskRepository.tasks.find((t: TaskItem) => t.id === id);
if (!item || item.status === 'done') { return false; }
item.status = 'done';
return true;
}
把操作是否生效交给仓储判断,有一个非常实际的好处:按钮可以被重复点击,但第二次不会再宣称产生了一次新的修改。在线上协作应用里,操作还需要处理服务端冲突、鉴权、失败重试,这些属于业务层,不应在路由回调里硬编码。对于同进程内的演示,先把“真的修改过”这一事实表述准确。
列表只在必要时重新取得快照
入口页之前使用 @State tasks: TaskItem[] = TaskRepository.all() 保存列表快照,本篇不改动这个约定。问题也因此容易解释:静态仓储不是响应式数据源,调用 complete 并不会自动赋值给 @State tasks。我们在原 TaskIndex 中新增一个 openTask 方法,并把列表行的点击行为改为 this.openTask(task.id)。
// pages/Index.ets:在 TaskIndex 内补充
import { TaskChangeResult, isTaskChanged } from '../model/TaskResult';
private openTask(id: string): void {
const params: TaskRouteParams = { taskId: id };
this.taskStack.pushPath({
name: 'TaskDetail',
param: params,
onPop: (popInfo: PopInfo) => {
const result: TaskChangeResult | undefined =
popInfo.result as TaskChangeResult | undefined;
if (isTaskChanged(result, id)) {
this.refresh(); // refresh 仍是上篇的 this.tasks = TaskRepository.all()
}
}
});
}
这段实现沿用同一模块内 .navDestination(this.pageMap) 的页面映射,不需要新建路由表,也不需要修改 TaskDetail 路由名称。注意这里的 onPop 回调挂在入栈时指定的 NavPathInfo 上,真正拿到的是详情页 pop(result) 返回的业务结果。列表回调只做校验和刷新,绝不再调用一次 complete():返回结果应当报告更新事实,不能使更新操作被重复执行。
在列表页加入“全部 / 待处理 / 已完成”的分类筛选时,也可以保留筛选状态,只替换仓储快照。一个合适的写法是将筛选变成对本地 @State tasks 的派生计算,确保条件没有在回调里被偷偷重置。
// TaskIndex 内继续补充,ForEach 使用 this.visibleTasks()
@State filter: string = 'all';
private visibleTasks(): TaskItem[] {
if (this.filter === 'all') { return this.tasks; }
return this.tasks.filter((t: TaskItem) => t.status === this.filter);
}
例如用户在“待处理”筛选下打开 tk-102,详情里改成“已完成”后返回,列表通过刷新得到新数组,然后按原来的“待处理”条件重新计算,于是这一行自然消失。这不是数据被删掉,而是它已不再满足当前筛选。为了避免误会,可以在列表空态处提示“当前筛选下没有任务”,并提供切换“全部”的入口。若后端列表是分页查询,更新后的任务可能需要跨页重排,这时仅局部修改一行并不能保证排序正确,应按服务端排序规则重新查询当前页。

三条返回路径怎么复核
第一条是正常修改:进入 tk-102,调用完成操作,收到 changed,刷新后对应状态为“已完成”。第二条是不修改直接返回:结果为 unchanged,列表不触发更新。第三条是参数异常:通过模拟入口传入 tk-999,仓储查不到实体,详情展示空态,选择返回时不应把任何真实任务标记完成。为了确认刷新次数,可以在 refresh() 内仅增加一次调试日志并在测试之后删除,避免用“看起来变化了”代替事件验证。
系统手势返回还需要专门说清楚:用户可能没有点击我们的“取消”按钮,而是直接滑动返回。系统返回往往不会携带业务结果,此时 result 可按空值处理;因为尚未修改仓储,列表不刷新是合理的。但如果产品要求用户点击完成后仍留在详情页,再通过系统返回离开,就必须选择另一种一致性策略,例如在详情写入成功后及时更新可观察的数据源,或在页面重新显示时检查变更版本号。不能把所有返回路径都当成一定会显式调用自定义 pop(result)。
页面销毁、导航栈改变和数据写入各有生命周期。不要把服务端写入放在 onDisappear 中指望一定执行,也不要依靠 aboutToDisappear 发起尚未完成的异步更新;页面退出时不保证长期任务能够完成。本篇的内存仓储用于解释页面间协作,下篇才会讨论任务详情复用与 onNewParam:当同名目的页被复用而不是重新创建时,原来“只在 onReady 读一次参数”的策略会失效,届时需要更新参数感知而不让旧任务内容串到新任务上。
收尾时再把原则压缩成一句话:栈负责返回,结果负责告知,仓储负责真相,列表负责展示。 用户看到的状态是否新鲜,取决于这四者的边界是否清楚,而不是点击返回时有没有播放转场动画。
官方参考:
更多推荐



所有评论(0)