【寻迹校园 HarmonyOS NEXT 实战 07】不引入全局状态库:用 dataRevision 实现跨页面刷新

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

dataRevision 跨页面刷新原创概念图

上图为本文原创生成的状态刷新概念图,不是项目截图。中心是 Service/Repository 权威数据,周围页面只监听版本变化并重新读取。

一、跨页面刷新最容易犯什么错

一个失物招领记录会同时影响多个页面:

  • 发布成功后,首页列表要出现新记录;
  • 提交认领后,消息页要出现待处理信息;
  • 撤回或删除后,“我的发布”要更新;
  • 双方确认交接后,详情状态和统计数量都要变化;
  • 举报成立后,被隐藏记录不应继续出现在首页。

最直接的做法是在回调里手工修改多个数组和计数,例如给首页列表 push() 一条记录、给个人中心数量加一、给消息角标加一。问题是这些副本没有统一事务边界:一次写入失败、一次返回路径遗漏或一次重复回调,都可能让页面状态和数据库不一致。

项目选择更简单的规则:写入只发生在 Service/Repository;页面收到成功结果后只递增一个版本号,所有消费者再从权威数据源读取。

二、dataRevision 是失效信号,不是业务数据

Index.ets 中只有一个轻量状态:

@Local dataRevision: number = 0;

它不表示记录数量、消息数量或数据库版本,也不会被持久化。它只表达一句话:“权威数据可能已经变化,请重新加载。”

这和缓存失效的思想相同。消费者不需要知道发生了发布、认领还是举报,只要发现 dataRevision 改变,就重新调用对应 Service。

这种设计有两个好处:

  1. 页面之间不传递整份可变列表;
  2. 写入方不需要知道有哪些页面正在展示这些数据。

三、初始化完成后为什么也要递增一次

应用启动时,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 聚合统计虽然多一次读取,却换来了可验证的一致性。对于当前单机数据量,这个成本远小于维护多份补丁状态。

八、如何防止刷新风暴

失效信号简单,但仍需要约束:

  1. 只在业务动作真正成功后递增;
  2. 一个完整事务只发一次信号;
  3. 页面加载方法要能处理快速连续调用;
  4. 异步结果回写前确认页面仍需要该结果;
  5. 不在 build() 中发起持久化查询;
  6. loading、empty 和 error 必须稳定,不因刷新闪烁布局;
  7. 大数据场景要增加缓存、去抖或请求 ID,而不是无限全量查询。

在更复杂页面中,可以为加载请求增加递增 ID,只接受最后一次响应,防止旧请求覆盖新状态。

九、什么时候该升级为 ViewModel 或事件总线

dataRevision 适合当前单进程、单机、页面数量有限的项目,但它不是万能全局状态方案。

出现以下情况时应升级:

  • 多个页面需要共享同一份复杂 UI-ready 状态;
  • 列表规模大,任何变更都全量重查成本过高;
  • 写入事件需要携带类型化增量信息;
  • 需要撤销、乐观更新或离线冲突合并;
  • 多窗口、多设备或远端推送同时修改数据;
  • 页面逻辑已经难以在组件内部测试。

升级方向可以是明确的 ViewModel、Repository 观察流或类型化事件总线。无论采用哪种方式,Service/Repository 仍应持有权威业务数据。

十、它不解决跨设备同步

dataRevision 只存在当前进程内。应用被关闭后它会重置,也不会通知另一台设备,更不具备服务端版本、冲突检测和增量同步能力。

因此正确表述是:它解决当前单机 HarmonyOS 应用的跨页面刷新,不是云同步、分布式数据或多账号实时协作方案。

十一、验收清单

本项目用以下场景验证刷新链:

  • 初始化完成后首页从 Service 重新读取;
  • 发布成功返回首页出现新记录;
  • 编辑、撤回和删除后“我的发布”更新;
  • 认领提交后目标记录进入处理中;
  • 审核、交接和双方确认后相关页面状态一致;
  • 举报隐藏后首页不再展示目标记录;
  • 写入失败时版本不递增,页面显示统一错误。

这些场景验证的是状态流,而不是只检查某个按钮能否点击。

十二、把一次刷新问题拆成可定位的故障矩阵

dataRevision 看起来只有一行自增,但线上问题不能只检查这个数字。更实用的排查方法,是把完整链路拆成“写入、发信号、消费信号、重新查询、渲染结果”五个观察点:

现象 优先检查 常见根因 最小修复方向
发布成功但首页不变 成功回调是否触发 写入成功后遗漏 onPublished() 只在成功分支补一次失效信号
首页刷新但消息未变 消费者是否接收新版本 某个页面没有把版本作为输入 统一页面入口参数,不复制业务数组
页面短暂显示旧数据 并发查询完成顺序 旧请求晚于新请求回写 增加请求序号,只接受最后一次结果
一次操作触发多次闪烁 版本递增次数 一个事务内多处调用回调 把通知收敛到事务成功出口
返回页面一直 loading 查询异常后的收尾 finally 或错误映射缺失 确保 loading、error、empty 都有终态

这张矩阵让问题可以复现和归属:Repository 写入失败属于数据层,成功后没发信号属于动作编排,页面收到信号却没重查属于消费端。团队不用把所有“页面没更新”都归咎于 ArkUI 生命周期。

十三、变更影响、回滚与可追踪验收

当新增一个会修改数据的业务动作时,应先列出它影响的权威实体和页面消费者,再决定是否沿用同一个版本信号。不要为了让某个角标立刻变化,就在新页面里直接修改旧页面的局部状态。

一次安全变更至少要留下三类证据:

  1. 写入结果:Service 返回成功,Repository 中能重新读到目标状态;
  2. 刷新结果:dataRevision 只递增一次,相关页面重新执行查询;
  3. 用户结果:首页、消息或“我的发布”展示由新查询计算出的状态。

如果升级 ViewModel 或事件流后出现回归,回滚点也应是“通知机制”,不是回退到多页面手工修改列表。只要 Repository 接口和 Service 成功出口保持稳定,就可以临时恢复统一失效信号,而不破坏业务数据。

维护时还要避免把调试日志当成产品证据。日志只能证明某个分支被执行;只有重新读取到权威数据并在页面呈现正确 empty/error/success 状态,才证明整条刷新链可用。

十四、本文小结

dataRevision 是一个低成本的失效信号:Service/Repository 负责写入 canonical data,子页在成功后递增版本,首页、消息和个人中心重新读取自己的权威数据。

它避免了多页面手工拼补数组和计数,也保留了未来升级 ViewModel 或事件流的空间。下一篇将从交付风险出发,说明为什么核心应用与小艺 Agent 要拆成 V1.0 和 V1.1 两条可独立验收的路线。

系列导航:第 7 篇 / 共 50 篇。上一篇:《共享权威词表设计》;下一篇:《V1.0 核心应用 + V1.1 小艺增强》。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐