
【时光清单06】HarmonyOS ArkTS 应用状态仓库实战让页面统计与持久化结果保持同步在一个包含首页、全部清单、分类列表、详情页和新增页的 HarmonyOS 应用里最容易被低估的问题不是“数据能不能保存”而是“保存之后已经打开的页面什么时候知道数据变了”。如果每个页面都持有一份数组副本新增页写入 Preferences 后返回首页首页仍可能展示旧统计详情页完成置顶后列表顺序也可能停留在操作前。反过来如果把完整业务数据都塞进AppStorage又会让持久化模型、响应式状态和页面生命周期纠缠在一起。时光清单的真实源码采用了一种轻量方案AnniversaryRepository负责内存集合与持久化AppViewModel作为页面调用仓库的窄入口AppStore在启动阶段初始化全局状态键页面通过StorageLink(StateKeys.DATA_VERSION)订阅一个递增的“数据版本号”。当前页面代码在 Repository 方法返回后递增版本号正在显示的列表页面由Watch重新从仓库读取数据。这不是 Redux 式完整状态树也不是数据库的变更通知而是适合中小型本地应用的一种失效信号。本文基于 项目现存 ArkTS 源码完整拆解它的边界、执行顺序、失败分叉与可演进方向。本文将完成六项可复核分析区分AppStore、AppViewModel、Repository 与页面状态的职责。还原新增、编辑、删除、置顶之后的真实刷新链路。解释为什么只把dataVersion放入AppStorage。检查统计值、排序结果和持久化结果如何保持同源。指出当前写操作与版本递增分散在页面中的一致性风险。给出不推翻现有结构的渐进式改造与测试办法。本文唯一标记CSDN-SERIES:ALL-163203208证据边界当前事实、历史证据与建议实现本文把三类内容明确分开。当前事实来自本次静态核验的DataStore.ets、AnniversaryRepository.ets、AppViewModel.ets、StateKeys.ets以及相关页面源码历史证据来自项目错误记录其中记载了 2026 年 5 月 20 日“编辑后列表和统计停留在旧值”的问题、根因与当时的修复建议实现是为了补齐失败可见性和提交语义而给出的演进代码并不代表仓库当前已经具备这些能力。本次没有执行新的工程构建、真机运行或发布回归因此本文不会把历史assembleHap通过记录写成当前构建结论也不会把源码静态路径推断成已验证的设备行为。文章讨论的是可从源码证明的调用关系以及应如何设计下一轮可重复验收。一、先确定四个状态边界状态仓库这个词很容易让人误以为所有数据都应该集中到一个全局对象。真实工程里更重要的是按生命周期划分状态。状态类型当前归属生命周期示例持久化业务数据AnniversaryRepositoryDataStore跨启动纪念日、置顶状态业务访问入口AppViewModel页面实例期间查询、保存、删除跨页面失效信号AppStorage应用进程期间dataVersion页面展示快照State页面期间anniversaries、加载状态这个划分的关键是AppStorage不保存纪念日数组本身只保存“仓库内容已变化”的通知信号。页面数组仍然来自 Repository因此列表、统计和详情最终读取的是同一数据源。真实StateKeys.ets直接把这个意图写进了注释export class StateKeys { static readonly NAV_STACK: string navStack; static readonly DARK_MODE: string isDarkMode; // 数据版本号触发 UI 刷新 static readonly DATA_VERSION: string dataVersion; static readonly THEME_BG: string themeBgColor; static readonly THEME_CARD: string themeCardColor; }DATA_VERSION不是业务版本、数据库 schema 版本或应用版本号。它只是一个单调递增的失效计数器。名字虽然短但使用方必须理解它的语义否则很容易把它当作可持久化业务字段。二、启动阶段AppStore 只建立全局运行时状态AppStore.bootstrap()在应用启动阶段执行并通过静态布尔值避免重复初始化export class AppStore { private static initialized: boolean false; private static appContext: common.ApplicationContext | null null; static bootstrap(context: common.UIAbilityContext | common.Context): void { if (AppStore.initialized) return; AppStorage.setOrCreateboolean(StateKeys.DARK_MODE, isDark); AppStorage.setOrCreatestring(StateKeys.CURRENT_THEME, chinese_ink); if (AppStorage.getNavPathStack(StateKeys.NAV_STACK) undefined) { AppStorage.setOrCreateNavPathStack( StateKeys.NAV_STACK, new NavPathStack() ); } AppStorage.setOrCreatenumber(StateKeys.DATA_VERSION, 0); AppStore.applyTheme(chinese_ink); AppStore.initialized true; } }这里有三个工程细节。第一使用setOrCreate而不是无条件覆盖。页面或服务已经设置过值时重复启动逻辑不会把状态重置为默认值。第二dataVersion从零开始只存在于当前应用运行期。重新启动后页面会重新从持久化仓库加载完整数据因此不需要延续上一次运行时的计数。第三AppStore同时管理主题、颜色模式、导航栈和窗口系统栏但它没有直接读写纪念日集合。全局 UI 状态与领域数据之间仍有明确边界。三、Repository 是运行期查询入口Preferences 是预期持久化来源AnniversaryRepository是单例内部维护items数组并通过DataStore完成 JSON 持久化export class AnniversaryRepository { private static _instance: AnniversaryRepository | null null; private items: Anniversary[] []; private initialized: boolean false; private store: DataStore DataStore.getInstance(); static getInstance(): AnniversaryRepository { if (!AnniversaryRepository._instance) { AnniversaryRepository._instance new AnniversaryRepository(); } return AnniversaryRepository._instance; } async init(): Promisevoid { if (this.initialized) return; this.items await this.store.getJsonAnniversary[]( DataKeys.ANNIVERSARIES, [] ); this.initialized true; } }单例让不同页面创建的AppViewModel最终访问同一个 Repository 实例。initialized保证正常情况下只从持久化介质水合一次后续查询从内存集合返回。这里必须避免把“同一个 Repository 实例”扩大解释成“持久化一定成功”。当前DataStore.putJson()、getJson()、getJsonSync()和remove()都在内部捕获异常当 Preferences 尚未初始化或写入失败时写方法可能直接返回调用者拿不到明确失败结果。因此Repository 的items是页面在本次进程中的即时查询来源Preferences 是预期的耐久来源两者在正常写入时一致但源码本身不能证明异常路径下仍然一致。但 Repository 没有把内部数组直接交出去。getAll()调用sortedCopy()private sortedCopy(): Anniversary[] { return [...this.items].sort((a: Anniversary, b: Anniversary) { if (a.pinned ! b.pinned) return a.pinned ? -1 : 1; return a.targetDate - b.targetDate; }); } async getAll(): PromiseAnniversary[] { await this.init(); return this.sortedCopy(); }[...this.items]创建浅拷贝后再排序避免页面查询改变仓库内部数组顺序。排序规则也集中在仓库置顶项优先其余按目标日期升序。于是首页、全部列表和筛选列表不会各自实现一套稍有差异的排序。四、写操作顺序先改变内存再完成持久化保存方法先判断是编辑还是新增。编辑时更新现有对象和updatedAt新增时通过createAnniversary()建立完整模型最后统一调用persist()async save(data: AnniversarySaveData): PromiseAnniversary { await this.init(); const existing data.id ? this.items.find((item: Anniversary) item.id data.id) : undefined; if (existing) { if (data.title) { existing.title data.title; } if (data.targetDate) { existing.targetDate data.targetDate; } if (data.pinned ! undefined) { existing.pinned data.pinned; } existing.updatedAt Date.now(); await this.persist(); return existing; } const newItem createAnniversary(data); this.items.push(newItem); await this.persist(); return newItem; }删除和置顶也遵循同一顺序async delete(id: string): Promiseboolean { await this.init(); const index this.items.findIndex( (item: Anniversary) item.id id ); if (index 0) { this.items.splice(index, 1); await this.persist(); return true; } return false; } async togglePin(id: string): Promiseboolean { await this.init(); const item this.items.find( (i: Anniversary) i.id id ); if (item) { item.pinned !item.pinned; item.updatedAt Date.now(); await this.persist(); return true; } return false; }只有persist()成功返回方法才向页面报告成功。因此页面应当在await完成并确认结果之后再发出版本变更信号。若保存异常就提前递增版本号订阅页面虽然会重读但得到的仍是旧数据还会制造一次没有意义的重绘。五、AppViewModel窄接口而不是第二份状态AppViewModel没有维护anniversaries数组也没有复制 Repository 的初始化状态。它只把页面需要的操作包装成类型明确的方法export class AppViewModel { private anniversaryRepo: AnniversaryRepository AnniversaryRepository.getInstance(); async loadAnniversaries(): PromiseAnniversary[] { return this.anniversaryRepo.getAll(); } async saveAnniversary( data: AnniversarySaveData ): PromiseAnniversary { return this.anniversaryRepo.save(data); } async deleteAnniversary(id: string): Promiseboolean { return this.anniversaryRepo.delete(id); } async togglePin(id: string): Promiseboolean { return this.anniversaryRepo.togglePin(id); } }这层看起来很薄却提供了三个价值。页面不需要知道 Repository 是单例也不直接依赖DataStore。日期文案计算被集中到getDaysText()、getDaysSubtitle()等方法。未来若加入事务、错误映射、埋点或多仓库组合页面接口可以保持稳定。需要注意当前 ViewModel 并不是一个拥有响应式字段的长期对象。每个页面都可以创建自己的实例但这些实例通过单例 Repository 汇合到同一数据源。因此它更接近页面服务门面而不是完整 MVVM 中负责持有 UI State 的状态容器。六、读页面如何订阅 dataVersionHomeView的核心做法是用StorageLink连接全局键再通过Watch观察变化Component export struct HomeView { StorageLink(StateKeys.DATA_VERSION) Watch(onDataChange) dataVersion: number 0; State anniversaries: Anniversary[] []; private viewModel: AppViewModel new AppViewModel(); async aboutToAppear(): Promisevoid { await this.loadData(); } private async onDataChange(): Promisevoid { await this.loadData(); } private async loadData(): Promisevoid { this.anniversaries await this.viewModel.loadAnniversaries(); } }AllView与FilteredListView使用相同模式。页面首次出现时主动加载之后版本号变化时重新查询。这样能覆盖两类场景首次进入页面版本号尚未变化也必须有初始数据。页面已经存在于导航结构中其他页面写入后无需销毁重建即可刷新。页面统计应从本次加载得到的anniversaries派生而不是另外持久化一个计数器。例如“全部数量”“纪念日数量”“爱情天数”都应该从同一数组计算。否则删除一条记录时需要同时更新集合、总数、分类数和首页摘要任何一个遗漏都会形成漂移。七、新增链路方法返回后递增版本但返回不等于落盘确认AddView的真实保存链路是const saved await this.viewModel.saveAnniversary(data); if (!saved) { return; } const ver AppStorage.getnumber(StateKeys.DATA_VERSION) ?? 0; AppStorage.setnumber( StateKeys.DATA_VERSION, ver 1 );从交互到页面同步可以还原为以下时序用户点击保存 - AddView 校验输入并组装 AnniversarySaveData - AppViewModel.saveAnniversary() - AnniversaryRepository.save() - 内存 items 更新 - DataStore.putJson() 尝试写入 Preferences - AddView 递增 DATA_VERSION - HomeView / AllView / FilteredListView 的 Watch 触发 - 页面重新调用 loadAnniversaries() - 新数组驱动列表和统计刷新从页面表面顺序看await DataStore.putJson()位于通知之前但当前putJson()会在内部捕获异常并返回voidRepository 无法区分“写入成功”和“异常被吞掉”。所以当前事实只能表述为“写入尝试返回后通知页面”不能表述为“已确认持久化成功后通知”。版本号可以让其他页面重读内存快照却不能证明重启后仍能得到相同数据。真正需要建立的不变量应当是只有拿到可判定的持久化成功结果才发布刷新信号并展示成功反馈。八、详情页的置顶、删除与编辑详情页包含三种会改变列表结果的操作。1. 置顶置顶改变pinned而sortedCopy()把置顶作为第一排序条件。操作成功后如果没有通知详情页中的图标或许已更新但返回列表时顺序仍可能是旧快照。2. 删除删除会影响列表数量、分类统计、首页摘要以及可能存在的桌面卡片选择。页面应该只在deleteAnniversary()返回true后递增版本并返回上一页。3. 编辑编辑可能改变标题、类型、目标日期和置顶状态。即使列表数量不变排序位置、分类归属和倒计时数字也可能同时变化因此同样需要触发失效。九、为什么不把完整数组放入 AppStorage直接把Anniversary[]放入AppStorage看似能省掉重读但会带来一组难以察觉的问题。第一页面可能直接修改数组元素而没有经过 Repository 的persist()。界面显示已经变化重启后却恢复原值。第二排序、过滤和更新对象引用会触发不同的响应式行为。开发者需要额外保证每次都替换数组引用而不是只修改内部字段。第三业务模型中若加入复杂对象、迁移字段或不可序列化值全局状态与持久化格式会互相限制。第四多个写入口很难确保 AppStorage 数组和 Repository 内存数组始终同步系统中出现两个真源。版本号方案的优势正是让全局状态保持极小AppStorage: dataVersion 17 Repository: 完整 Anniversary[] 持久化职责 Page: 当前展示快照 派生统计它牺牲了一次内存查询却换来清晰的数据所有权。由于 Repository 已水合并缓存items正常刷新并不会每次都重新读取 PreferencesgetAll()主要执行的是数组复制与排序。十、当前实现的第一个风险通知逻辑分散真实源码中AddView、CoupleView、HabitView、HabitWallView、WishListView等页面都存在相似代码const ver AppStorage.getnumber(StateKeys.DATA_VERSION) ?? 0; AppStorage.setnumber( StateKeys.DATA_VERSION, ver 1 );这说明通知机制已经接入多处页面但发布责任分散到了各个写入口。新增一种写入口时开发者可能完成内存修改和持久化尝试却忘记递增版本。结果通常不是立即崩溃而是某些页面继续展示旧快照这类问题最难定位。最小改进是先提供统一函数export class DataVersion { static notifyChanged(): void { const current AppStorage.getnumber(StateKeys.DATA_VERSION) ?? 0; AppStorage.setnumber( StateKeys.DATA_VERSION, current 1 ); } }页面仍可在拿到明确结果后调用但重复代码被收拢键名和递增规则不会散落。进一步可以由 ViewModel 在确认提交成功后通知。下面代码属于建议实现不是当前源码async saveAnniversary( data: AnniversarySaveData ): PromiseAnniversary { const result await this.anniversaryRepo.save(data); DataVersion.notifyChanged(); return result; }这样页面只负责展示保存结果不再承担跨页面一致性协议。不过这会改变现有 ViewModel 的职责需要统一迁移所有写入口不能一半由页面通知、一半由 ViewModel 通知否则一次写入可能递增两次。十一、当前实现的第二个风险读改写并非原子操作版本递增由“读取当前值”和“写入加一”两步组成const current AppStorage.getnumber(key) ?? 0; AppStorage.setnumber(key, current 1);ArkUI 页面交互通常运行在 UI 线程连续点击又有按钮状态保护时丢失递增的概率很低。但如果未来引入 Worker、并行异步回调或批量导入两次写操作可能先后读到同一个旧值然后都写入相同的新值。这里不必盲目引入复杂锁。因为版本号只承担失效作用即使两次写合并为一次变化订阅页面只要在最终持久化后重读通常仍能得到最新集合。真正需要保证的是最后一次写成功后至少发生一次通知。页面加载不能把较早请求的结果覆盖到较晚请求之上。批处理期间不要每写一条就触发一次昂贵刷新。批量导入更适合在事务或批处理完成后统一通知一次而不是追求版本号精确等于写入次数。十二、异步重载的竞态保护Watch回调是异步的。若连续发生两次变更页面可能同时启动两次loadData()。当前 Repository 查询很快风险有限一旦数据源扩展为 RDB、文件解析或云同步就应防止旧请求后返回并覆盖新结果。可以在页面维护请求序号State anniversaries: Anniversary[] []; private loadSequence: number 0; private async loadData(): Promisevoid { const sequence this.loadSequence; const result await this.viewModel.loadAnniversaries(); if (sequence ! this.loadSequence) { return; } this.anniversaries result; }也可以在短时间连续通知时做合并刷新。选择哪种方案取决于查询成本而不是为了形式上的“架构完整”。当前本地数组查询无需增加节流保持简单更重要。十三、失败状态不能只写日志Repository 的persist()可能因存储初始化、空间、序列化或系统异常失败。若异常一路抛到页面页面至少要恢复保存按钮、显示错误并保留用户输入。推荐把页面状态显式化type SaveState idle | saving | success | error; State saveState: SaveState idle; State errorMessage: string ; private async submit(): Promisevoid { if (this.saveState saving) return; this.saveState saving; try { await this.viewModel.saveAnniversary(this.formData); DataVersion.notifyChanged(); this.saveState success; } catch (error) { this.errorMessage 保存失败请稍后重试; this.saveState error; } }关键不是捕获后沉默而是确保失败时不递增版本、不退出编辑页、不丢失草稿。重复点击也应在saving状态被阻止避免产生重复记录。十四、统计与列表必须从同一快照派生页面刷新后统计逻辑应一次性基于同一数组计算interface AnniversaryStats { total: number; pinned: number; upcoming: number; expired: number; } function buildStats( items: Anniversary[], now: number ): AnniversaryStats { return { total: items.length, pinned: items.filter( (item: Anniversary) item.pinned ).length, upcoming: items.filter( (item: Anniversary) item.targetDate now ).length, expired: items.filter( (item: Anniversary) item.targetDate now ).length }; }如果列表调用一次loadAnniversaries()统计又单独调用四次查询虽然本地数据通常一致但会增加重复排序也给未来异步数据源留下不同快照的可能。一次加载、一次派生更容易测试。日期统计还应固定now。同一次计算中多次调用Date.now()在午夜边界可能让两个分类使用不同时间基准。把now作为参数传入测试也能覆盖临界日期。十五、深浅色状态与业务版本号为何能共存AppStore同时管理主题值和DATA_VERSION但两者更新路径不同。主题变化通过AppStore.applyTheme()写入多个语义色键并更新系统栏内容颜色业务数据变化只递增DATA_VERSION由页面重读 Repository。两类状态共用AppStorage并不等于它们共用同一种处理方式。主题变化 - 写 themeBg/themeCard/themePrimary - ArkUI 直接响应颜色值 业务数据变化 - 写 dataVersion - Watch 触发查询 - 页面替换业务数组快照颜色值本身适合直接成为响应式状态持久化业务集合则更适合保留在 Repository。根据数据性质选择同步方式比强行统一状态工具更可靠。十六、测试这条同步链路可以把验证分成三层。Repository 层空存储初始化得到空数组。新增后getAll()返回新对象。编辑后updatedAt更新且字段落盘。删除不存在 ID 返回false。置顶后排序前移。重建 Repository 或重新水合后结果仍一致。ViewModel 层保存、删除、置顶正确委托给 Repository。getDaysText()对未来、过去与爱情起始日返回正确文案。Repository 失败时异常不会被错误吞掉。页面联动层启动 - 首页显示 2 条 新增 - 保存成功 - 首页显示 3 条 置顶 - 返回列表 - 目标项移动到首位 编辑日期 - 倒计时和排序同时变化 删除 - 首页、全部页和分类统计都减少 1 保存失败 - 页面不退出、统计不变化 快速连续保存 - 无重复记录、最终列表正确HarmonyOS 多设备场景还应检查手机、平板和 2in1 的导航缓存行为。不同布局可能让多个子页面同时存活正是dataVersion失效机制比“返回时刷新”更有价值的地方。十七、性能边界什么时候该升级方案当前实现每次变化都会让订阅页面重新执行数组复制和排序。几十或几百条本地纪念日数据时这个成本很低。以下信号出现时才值得升级信号可选演进数据达到数万条RDB 分页、索引与增量查询多模块频繁写入统一 Mutation Service页面只关心局部变化按领域拆分版本键云端与本地共同更新明确同步状态与冲突策略多进程或多设备协同使用平台支持的数据同步机制批量导入频繁刷新批处理完成后合并通知不要因为未来可能增长就提前把简单本地应用改造成复杂事件总线。当前机制的价值在于用一个整数连接“写方法返回”和“读页面失效”它解决了运行期页面刷新问题但还没有单独解决异常路径下的持久化确认问题。十八、建议的渐进式重构顺序如果要在现有项目上提高一致性建议按以下顺序推进先让DataStore返回可判定的写入结果不再吞掉失败语义。新增DataVersion.notifyChanged()收拢重复递增代码。为保存、删除、置顶补充saving/error/disabled状态。统一页面loadData()的错误与空状态。把列表统计改为从同一查询快照派生。只有在异步查询明显变慢后再加入请求序号保护。只有在数据规模增长后再评估 RDB 与分页。每一步都能独立验证也不会迫使项目一次性更换状态框架。1. 历史问题证明了“页面失效信号”的必要性项目错误记录在 2026 年 5 月 20 日留下过一条可追溯证据编辑倒计时、纪念日或恋爱数据后相关列表和统计不会立即刷新往往要切换页面才能看到新结果。当时确认的根因有两个一是页面之间缺少稳定的数据变化通知二是列表ForEach只用id作为键编辑同一对象后键值不变局部渲染可能继续复用旧节点。对应修复是让HomeView、AllView、FilteredListView订阅DATA_VERSION详情页完成编辑、删除、置顶后递增该值并把列表键改为${id}_${updatedAt}。这段历史记录能证明当前通知设计为何存在也能证明“只刷新详情页局部字段”不足以覆盖列表排序、分类统计和倒计时变化。历史记录还提到当时的assembleHap通过但那是历史时点证据。本次文章精修只做源码静态核验不能据此宣称当前工作区已经重新构建通过。可靠的写法是把“过去发生过什么”和“本轮实际验证了什么”分别记录。2. 建议一让持久化层返回结果而不是只返回 void当前DataStore.putJson()的核心缺口不是使用 Preferences而是失败信息没有越过存储边界。建议先定义一个小而明确的联合类型让调用方必须处理成功与失败。以下代码是建议接口export type WriteFailure | storage_unavailable | serialize_failed | flush_failed; export type WriteResult | { ok: true } | { ok: false; reason: WriteFailure };这个结果不需要携带异常堆栈更不应把底层隐私数据带到界面。它只需要回答一个影响业务决策的问题这次修改能否被当作已提交。页面可以据此决定是否退出、是否显示成功提示Repository 也可以决定是否保留内存修改。建议的putJson应把序列化和 Preferences 写入分别映射到稳定错误类型async putJsonT(key: string, value: T): PromiseWriteResult { const pref await this.requirePreferences(); if (!pref) return { ok: false, reason: storage_unavailable }; try { const payload JSON.stringify(value); await pref.put(key, payload); await pref.flush(); return { ok: true }; } catch (error) { return { ok: false, reason: flush_failed }; } }实际工程可以进一步区分JSON.stringify与put/flush的异常但不要为了错误分类而泄漏底层对象。重要的是不再把失败压缩成和成功相同的void。3. 建议二Repository 提交失败时恢复内存快照既然当前 Repository 先修改items就应在写入失败时恢复旧快照。对中小型本地数组提交前复制一次集合通常足够清晰export interface MutationResultT { ok: boolean; value?: T; reason?: WriteFailure; } async save(data: AnniversarySaveData): PromiseMutationResultAnniversary { await this.init(); const before this.items.map((item: Anniversary) ({ ...item })); const saved this.applySaveToMemory(data); const write await this.store.putJson(DataKeys.ANNIVERSARIES, this.items); if (!write.ok) { this.items before; return { ok: false, reason: write.reason }; } return { ok: true, value: saved }; }这里的回滚只是一种建议。数据量很大或模型存在深层对象时应改用不可变更新、领域命令或数据库事务而不是无条件深拷贝。本文示例的目标是把提交边界讲清楚页面看到的新数据、Repository 内存和 Preferences 至少要在方法返回成功时达成一致。4. 建议三只有提交成功才发布刷新事件通知应靠近确认成功的边界而不是散落在每个页面。ViewModel 可以接收结果、发布领域修订号并把稳定结果交给页面async saveAnniversary( data: AnniversarySaveData ): PromiseMutationResultAnniversary { const result await this.anniversaryRepo.save(data); if (result.ok) { DataVersion.notifyChanged(); } return result; }页面拿到失败结果时保持编辑内容不退出当前路由并展示可恢复的错误状态成功时再清空表单或返回。这样“成功提示”“刷新通知”和“持久化确认”使用同一个结果不会出现页面提示已保存、列表数字已增加但重启后记录消失的三方分叉。5. 建议四页面只消费同一修订号下的快照当查询逐渐复杂时可以把数组、统计和状态放进同一个不可变快照。以下仍是建议模型export interface AnniversarySnapshot { revision: number; items: Anniversary[]; total: number; pinned: number; status: ready | saving | error; }首页数量、全部列表和分类统计都从同一items派生并携带对应revision。如果页面收到较旧修订号的异步结果可以直接丢弃避免后发请求先返回后又被旧请求覆盖。这里的revision仍是运行期同步概念不要与 Preferences schema 版本或发布版本混为一谈。6. 验收必须覆盖重启而不能只看当前页面只验证保存后当前列表增加一条最多证明内存链路和刷新链路能工作。持久化是否可靠必须销毁单例缓存或重启应用后重新加载。可以为 Repository 测试提供受控的重建入口执行如下检查思路const saveResult await repository.save(sample); expect(saveResult.ok).assertTrue(); AnniversaryRepository.resetForTest(); const reloaded await AnniversaryRepository.getInstance().getAll(); expect(reloaded.some((item: Anniversary) item.id sample.id)).assertTrue();还要注入 Preferences 未初始化、序列化失败和flush失败确认失败时内存已回滚、DATA_VERSION没有变化、页面不显示成功、表单内容仍可重试。这样才能分别验证提交语义、通知语义和页面语义而不是把一次界面变化误认为整条链路已经可靠。十九、审核与发布前的复核点状态同步问题通常不会在编译阶段暴露却会直接影响 HarmonyOS 应用审核中的功能完整性和运行稳定性。发布前至少检查新增、编辑、删除、置顶后的页面结果与重启后结果一致。写入失败不会显示虚假的成功状态。快速重复点击不会创建重复数据。空列表、加载中、加载失败均有明确界面。页面返回、系统返回与多窗口切换后数据仍正确。深浅色切换不会触发业务数据重置。平板或 2in1 双栏布局中同时存在的页面能一起刷新。不把个人数据、调试日志或持久化内容上传到网络。应用实际存储行为与隐私说明保持一致。这组检查对应的不是“状态管理是否高级”而是用户能否相信页面上看到的数字。二十、总结时光清单的应用状态同步链路可以概括为四句话AnniversaryRepository维护运行期业务集合并负责持久化尝试与统一排序。AppViewModel给页面提供类型明确的窄接口不复制第二份状态。AppStore初始化DATA_VERSION但不接管纪念日集合。当前页面在写方法返回后递增版本号订阅页面重读仓库并重新派生列表与统计。这种设计适合数据规模不大的本地应用结构足够清楚响应式刷新成本可控也保留了向 RDB、批处理和更完整 ViewModel 演进的空间。当前源码已经有跨页面失效信号但DataStore吞掉写入异常使“方法返回”还不能等同于“耐久提交成功”。下一步真正需要守住的不变量有两个只有持久化确认成功才发出失效通知页面统计始终从同一修订快照派生。本文基于时光清单项目中的AppStore.ets、StateKeys.ets、AppViewModel.ets、AnniversaryRepository.ets以及相关页面源码复核整理。文中改进代码用于解释工程演进方向已明确区别于当前实现。AI 辅助声明本文在真实源码核验、结构梳理和文字编辑过程中使用了 AI 辅助关键接口、调用关系和实现结论均以项目源码为依据进行人工复核。