【寻迹校园 HarmonyOS NEXT 实战 07】不引入全局状态库:用 dataRevision 实现跨页面刷新
【寻迹校园 HarmonyOS NEXT 实战 07】不引入全局状态库:用 dataRevision 实现跨页面刷新
这是“寻迹校园 HarmonyOS NEXT 实战”系列第 7 篇。本文拆解
Index.ets的dataRevision方案:Service 和 Repository 保存权威数据,页面只传递失效信号,让首页、消息和“我的发布”在写入后重新读取,而不是维护多份可变副本。

上图为本文原创生成的状态刷新概念图,不是项目截图。中心是 Service/Repository 权威数据,周围页面只监听版本变化并重新读取。
一、跨页面刷新最容易犯什么错
一个失物招领记录会同时影响多个页面:
- 发布成功后,首页列表要出现新记录;
- 提交认领后,消息页要出现待处理信息;
- 撤回或删除后,“我的发布”要更新;
- 双方确认交接后,详情状态和统计数量都要变化;
- 举报成立后,被隐藏记录不应继续出现在首页。
最直接的做法是在回调里手工修改多个数组和计数,例如给首页列表 push() 一条记录、给个人中心数量加一、给消息角标加一。问题是这些副本没有统一事务边界:一次写入失败、一次返回路径遗漏或一次重复回调,都可能让页面状态和数据库不一致。
项目选择更简单的规则:写入只发生在 Service/Repository;页面收到成功结果后只递增一个版本号,所有消费者再从权威数据源读取。
二、dataRevision 是失效信号,不是业务数据
Index.ets 中只有一个轻量状态:
@Local dataRevision: number = 0;
它不表示记录数量、消息数量或数据库版本,也不会被持久化。它只表达一句话:“权威数据可能已经变化,请重新加载。”
这和缓存失效的思想相同。消费者不需要知道发生了发布、认领还是举报,只要发现 dataRevision 改变,就重新调用对应 Service。
这种设计有两个好处:
- 页面之间不传递整份可变列表;
- 写入方不需要知道有哪些页面正在展示这些数据。
三、初始化完成后为什么也要递增一次
应用启动时,Repository 可能需要初始化 RelationalStore、种子数据和本地回退。页面第一次构建得比异步初始化更快,就可能先显示空态或回退数据。
项目在业务初始化全部完成后递增一次版本:
private async initializeApplication(): Promise<void> {
const context: common.UIAbilityContext | undefined = this.getAbilityContext();
if (!context) return;
await this.initializeBusiness(context);
this.dataRevision++;
}
private async initializeBusiness(context: common.UIAbilityContext): Promise<void> {
await reportService.initialize(context);
await claimService.initialize(context);
await handoffService.initialize(context);
await moderationService.initialize(context);
settingsService.initialize(context);
}
这一步很关键:它把“数据层准备完成”也当成一次失效事件,通知已渲染页面重新读取,避免首页长期停留在初始化前结果。
四、子页面只在业务成功后触发 onDataChanged
页面导航由 Index 统一组装。发布、认领、交接和举报页面都接收一个回调:
PublishFormPage({
pathStack: this.pathStack,
reportType: ReportType.LOST,
onPublished: () => this.dataRevision++
})
ClaimReviewPage({
pathStack: this.pathStack,
claimId: claimId,
onDataChanged: () => this.dataRevision++
})
子页面不直接访问 Index 的列表,也不返回一份修改后的全局对象。它只在 Service 返回成功之后调用回调。
例如发布页的顺序是:校验草稿、调用 createReport()、确认 OperationResult 成功、触发 onPublished()、进入成功页。如果保存失败,版本不递增,其他页面也不会被无意义刷新。

上图展示完整时序:页面发起动作,Service 写入权威数据,成功后 Shell 递增版本,多个消费者分别重新查询。版本号从不承载业务内容。
五、消费者应该如何响应版本变化
首页、消息页和“我的发布”把 dataRevision 作为输入参数。组件在首次出现或参数变化时触发加载,最终数据仍来自 Service。
逻辑上可以概括为:
dataRevision 变化
-> 页面将 loading 设为 true
-> 调用对应 Service 查询
-> 将结果映射成 UI-ready 状态
-> 处理 empty / error / success
-> loading 结束
这里最重要的不是某个生命周期 API,而是数据所有权:版本变化只能促使页面“重新读”,不能把页面当前数组当成数据库继续修改。
六、为什么不直接使用 AppStorage 保存列表
AppStorage 适合轻量跨页面状态或刷新信号,但不适合存放完整的权威业务记录。把报告列表、认领记录和治理案件同时塞进全局存储,会带来:
- 页面可以绕过 Service 任意修改数据;
- 全局对象与 RelationalStore 出现双重真相;
- 大列表更新扩大重渲染范围;
- 生命周期和持久化边界变得模糊;
- 测试难以判断数据来自数据库还是上一次页面残留。
dataRevision 的优点正是它足够“小”:只通知失效,不复制业务数据。
七、错误的“手工改多个计数”为什么危险
假设一次认领提交需要同时执行:新增 Claim、把 Report 从 OPEN 改为 CLAIMING、刷新消息、更新个人中心统计。
如果页面自己执行 messageCount++、claimingCount++、替换列表项,至少存在四种风险:
- Service 实际写入失败,但计数已经增加;
- 用户快速点击导致回调执行两次;
- 其他页面没有挂载,错过这次局部更新;
- 状态机后续回滚,旧计数没有恢复。
重新从 canonical data 聚合统计虽然多一次读取,却换来了可验证的一致性。对于当前单机数据量,这个成本远小于维护多份补丁状态。
八、如何防止刷新风暴
失效信号简单,但仍需要约束:
- 只在业务动作真正成功后递增;
- 一个完整事务只发一次信号;
- 页面加载方法要能处理快速连续调用;
- 异步结果回写前确认页面仍需要该结果;
- 不在
build()中发起持久化查询; - loading、empty 和 error 必须稳定,不因刷新闪烁布局;
- 大数据场景要增加缓存、去抖或请求 ID,而不是无限全量查询。
在更复杂页面中,可以为加载请求增加递增 ID,只接受最后一次响应,防止旧请求覆盖新状态。
九、什么时候该升级为 ViewModel 或事件总线
dataRevision 适合当前单进程、单机、页面数量有限的项目,但它不是万能全局状态方案。
出现以下情况时应升级:
- 多个页面需要共享同一份复杂 UI-ready 状态;
- 列表规模大,任何变更都全量重查成本过高;
- 写入事件需要携带类型化增量信息;
- 需要撤销、乐观更新或离线冲突合并;
- 多窗口、多设备或远端推送同时修改数据;
- 页面逻辑已经难以在组件内部测试。
升级方向可以是明确的 ViewModel、Repository 观察流或类型化事件总线。无论采用哪种方式,Service/Repository 仍应持有权威业务数据。
十、它不解决跨设备同步
dataRevision 只存在当前进程内。应用被关闭后它会重置,也不会通知另一台设备,更不具备服务端版本、冲突检测和增量同步能力。
因此正确表述是:它解决当前单机 HarmonyOS 应用的跨页面刷新,不是云同步、分布式数据或多账号实时协作方案。
十一、验收清单
本项目用以下场景验证刷新链:
- 初始化完成后首页从 Service 重新读取;
- 发布成功返回首页出现新记录;
- 编辑、撤回和删除后“我的发布”更新;
- 认领提交后目标记录进入处理中;
- 审核、交接和双方确认后相关页面状态一致;
- 举报隐藏后首页不再展示目标记录;
- 写入失败时版本不递增,页面显示统一错误。
这些场景验证的是状态流,而不是只检查某个按钮能否点击。
十二、把一次刷新问题拆成可定位的故障矩阵
dataRevision 看起来只有一行自增,但线上问题不能只检查这个数字。更实用的排查方法,是把完整链路拆成“写入、发信号、消费信号、重新查询、渲染结果”五个观察点:
| 现象 | 优先检查 | 常见根因 | 最小修复方向 |
|---|---|---|---|
| 发布成功但首页不变 | 成功回调是否触发 | 写入成功后遗漏 onPublished() |
只在成功分支补一次失效信号 |
| 首页刷新但消息未变 | 消费者是否接收新版本 | 某个页面没有把版本作为输入 | 统一页面入口参数,不复制业务数组 |
| 页面短暂显示旧数据 | 并发查询完成顺序 | 旧请求晚于新请求回写 | 增加请求序号,只接受最后一次结果 |
| 一次操作触发多次闪烁 | 版本递增次数 | 一个事务内多处调用回调 | 把通知收敛到事务成功出口 |
| 返回页面一直 loading | 查询异常后的收尾 | finally 或错误映射缺失 |
确保 loading、error、empty 都有终态 |
这张矩阵让问题可以复现和归属:Repository 写入失败属于数据层,成功后没发信号属于动作编排,页面收到信号却没重查属于消费端。团队不用把所有“页面没更新”都归咎于 ArkUI 生命周期。
十三、变更影响、回滚与可追踪验收
当新增一个会修改数据的业务动作时,应先列出它影响的权威实体和页面消费者,再决定是否沿用同一个版本信号。不要为了让某个角标立刻变化,就在新页面里直接修改旧页面的局部状态。
一次安全变更至少要留下三类证据:
- 写入结果:Service 返回成功,Repository 中能重新读到目标状态;
- 刷新结果:
dataRevision只递增一次,相关页面重新执行查询; - 用户结果:首页、消息或“我的发布”展示由新查询计算出的状态。
如果升级 ViewModel 或事件流后出现回归,回滚点也应是“通知机制”,不是回退到多页面手工修改列表。只要 Repository 接口和 Service 成功出口保持稳定,就可以临时恢复统一失效信号,而不破坏业务数据。
维护时还要避免把调试日志当成产品证据。日志只能证明某个分支被执行;只有重新读取到权威数据并在页面呈现正确 empty/error/success 状态,才证明整条刷新链可用。
十四、本文小结
dataRevision 是一个低成本的失效信号:Service/Repository 负责写入 canonical data,子页在成功后递增版本,首页、消息和个人中心重新读取自己的权威数据。
它避免了多页面手工拼补数组和计数,也保留了未来升级 ViewModel 或事件流的空间。下一篇将从交付风险出发,说明为什么核心应用与小艺 Agent 要拆成 V1.0 和 V1.1 两条可独立验收的路线。
系列导航:第 7 篇 / 共 50 篇。上一篇:《共享权威词表设计》;下一篇:《V1.0 核心应用 + V1.1 小艺增强》。
更多推荐


所有评论(0)