资讯动态

【天体运行模拟|15】HarmonyOS ArkTS 本地状态持久化实战:让保存、删除和页面返回后的数据即时一致

发布时间:2026/9/1 20:35:01 来源:尧图企业网站定制
【天体运行模拟15】HarmonyOS ArkTS 本地状态持久化实战让保存、删除和页面返回后的数据即时一致本地状态最让人困惑的故障不是完全保存失败而是“这个页面已经变了另一个页面还没变”。用户在模拟页点亮收藏返回实验列表仍显示空心星标删除笔记后列表立即消失重新进入却又出现统计页的收藏数量晚一步更新。它们本质上都是同一问题页面状态、Preferences 持久状态和跨页面快照没有清晰的提交顺序。“天体运行模拟”的DataStore.ets已经实现收藏、实验记录、笔记、实验次数和学习时长的本地保存并通过AppStorage发布收藏数、实验数和学习秒数快照。本文基于这条真实数据链面向 HarmonyOS 5.0 及以上版本分析怎样让保存、删除、返回刷新和统计更新形成一个可验证闭环而不是依赖“下次进入页面自然会好”。本文重点Preferences 与页面State谁是权威数据源写入、flush()、快照通知和 UI 更新的正确顺序为什么页面返回后要刷新以及如何避免重复请求覆盖收藏、笔记删除和统计累加如何防止并发丢数据失败时怎样回滚或保留输入不制造“假成功”。项目基线应用版本1.0.0targetSdkVersion 6.0.2(22)compatibleSdkVersion 6.0.1(21)设备包含 phone、tablet 与 2in1。当前源码使用 ArkData Preferences 和 AppStorage没有云同步。一、先划分三种状态项目里至少存在三层状态层级示例生命周期页面状态收藏列表、笔记数组、选中星标页面实例持久状态Preferences 中的 JSON 和 number应用安装周期跨页快照AppStorage 中的计数应用进程页面State负责立即渲染Preferences 负责重启后恢复AppStorage 负责多个页面观察轻量统计。三者不能都当权威业务记录以持久层为准页面和快照都是投影。二、真实写入链路字符串保存的源码是static async putString( key: string, value: string ): Promisevoid { if (!DataStore.prefInstance) return try { await DataStore.prefInstance.put(key, value) await DataStore.prefInstance.flush() DataStore.notifyStatsChanged(key, value) } catch (_) { } }正确部分是顺序先put再flush最后通知统计变化。问题是返回void且吞掉异常页面无法知道提交是否完成。三、保存成功应当有返回值建议基础写入返回显式结果export interface SaveResult { ok: boolean message?: string }static async putString( key: string, value: string ): PromiseSaveResult { const pref DataStore.prefInstance if (!pref) { return { ok: false, message: 本地存储尚未就绪 } } try { await pref.put(key, value) await pref.flush() DataStore.notifyStatsChanged(key, value) return { ok: true } } catch (_) { return { ok: false, message: 保存失败请重试 } } }调用方只有收到ok: true才能显示成功状态。四、UI 更新有两种策略保存前更新页面叫乐观更新保存成功后更新叫保守提交。// 保守提交 const next [...this.favoriteIds, id] const result await repository.saveFavorites(next) if (result.ok) { this.favoriteIds next }收藏切换频繁、失败概率低时可以乐观更新但失败必须回滚。删除笔记和清空记录更适合保守提交避免破坏性动作看起来成功后又复现。五、收藏为什么适合保存 ID源码保存string[]static async saveFavorites( ids: string[] ): Promisevoid { await DataStore.putString( favorite_experiments, JSON.stringify(ids) ) }实验名称、描述和图标仍由实验目录维护。应用升级后只要 ID 稳定收藏就能映射到新模型。保存完整实验对象反而容易产生旧副本。六、收藏写入前先去重快速点击或多个入口可能产生重复 IDfunction normalizeFavoriteIds( ids: string[] ): string[] { return [...new Set( ids.filter(id id.trim().length 0) )] }去重规则应放在仓库或 DataStore 上层而不是每个页面各写一次。七、读改写必须防止覆盖收藏切换、实验计数和学习时间都属于“读旧值、计算新值、整体写回”。两个异步操作并发时可能丢失一次更新。private static writeChain: Promisevoid Promise.resolve()static enqueueWrite( task: () Promisevoid ): Promisevoid { DataStore.writeChain DataStore.writeChain.then(task, task) return DataStore.writeChain }同一进程内用写队列串行化即可。未来若引入多端同步需使用版本号与冲突策略内存队列不再足够。八、实验记录追加的真实风险当前代码static async appendRecordT( record: T ): Promisevoid { const list await DataStore.loadRecordsT() list.unshift(record) await DataStore.putString( experiment_records, JSON.stringify(list) ) }两次追加同时读取同一旧列表后一次保存可能覆盖前一次。应把完整读改写放进同一个串行任务而不是只串行最后的putString。九、笔记删除要决定提交时点页面常见写法const next this.notes.filter( note note.id ! id ) this.notes next await DataStore.saveNotes(next)如果保存失败UI 已删除但磁盘没删除。保守方案const next this.notes.filter( note note.id ! id ) const result await repository.replaceNotes(next) if (!result.ok) { this.errorMessage result.message ?? 删除失败 return } this.notes next破坏性动作还应有确认并明确影响范围。十、页面返回后为什么需要 reload收藏可能在实验页修改列表页恢复显示时必须重新读取。常见生命周期aboutToAppear(): void { this.reload() }onPageShow(): void { this.reload() }首次显示可能触发两次读取。可以保留双入口但用版本号保证最后一次请求更新页面private reloadVersion: number 0private async reload(): Promisevoid { const version this.reloadVersion const result await repository.listFavorites() if (version ! this.reloadVersion) return this.favoriteIds result }十一、不要用 AppStorage 保存完整业务数组源码只把计数写入 AppStorageAppStorage.setOrCreatenumber( FAVORITE_COUNT_KEY, ids.length )这是合理边界。完整收藏和笔记仍由 Preferences 管理AppStorage 只承载favorite_count、experiment_count_total、learning_seconds_total等快照。否则页面修改数组与持久层读取会形成两个权威来源。十二、stats_version 的作用每次相关键变化时DataStore.statsVersion AppStorage.setOrCreatenumber( STATS_VERSION_KEY, DataStore.statsVersion )观察页面可监听版本变化后重算展示。版本号本身不是业务数据不需要持久化进程重启后由refreshStatsSnapshot()重新建立快照。十三、统计快照要在落盘后更新如果先更新favorite_count后续flush()失败统计页会显示新数量重启后又回到旧值。真实源码把通知放在flush()后这是应保留的关键规则。同样页面成功 Toast 也应在落盘完成后出现而不是按钮点击时立即出现。十四、初始化快照的真实算法项目启动时读取收藏、记录、实验次数和学习时长const count Math.max( experimentCount, recordCount )实验计数与记录数取较大值是兼容已有数据的策略。学习时长优先秒字段没有时用分钟乘 60。文章只能描述这条真实逻辑不能说已实现完整数据库迁移。十五、旧字段迁移要幂等建议增加 schema 版本const SCHEMA_VERSION_KEY schema_version const CURRENT_SCHEMA_VERSION 2迁移步骤读取当前版本只执行缺失步骤新值落盘成功再更新版本重复启动不会二次换算。迁移失败时保留旧字段不能先删除再尝试写新值。十六、保存中状态防止重复点击页面应显式维护State saving: boolean falseprivate async save(): Promisevoid { if (this.saving) return this.saving true try { await this.commit() } finally { this.saving false } }按钮保存中禁用并显示进度。这样既减少并发写也给用户明确反馈。十七、失败回滚要保留输入新建笔记保存失败时弹窗不能关闭并清空文本。推荐const result await repository.addNote(draft) if (!result.ok) { this.errorMessage 保存失败请重试 return } this.controller.close()用户输入属于高价值临时状态应留在页面内直到确认持久化成功或用户主动取消。十八、数据加载应区分四种状态type PageState | loading | empty | content | errorPreferences 尚未初始化、读取异常不能显示 empty。返回页面刷新时可保留旧 content 并显示轻量刷新态避免整页闪白。十九、刷新策略按数据所有权决定收藏页返回时重新读取因为其他页面可以修改收藏笔记列表若只有自身编辑也可在本地提交后直接更新统计页观察stats_version即可。页面推荐刷新触发收藏页aboutToAppear / onPageShow笔记页保存、删除成功后本地更新返回时复核实验记录页页面显示时读取我的统计观察快照版本不是所有页面都需要轮询或全量刷新。二十、持久化仓库的最小边界export class LearningStateRepository { async toggleFavorite( id: string ): PromiseSaveResult { return DataStore.enqueueResult(async () { const ids await DataStore.loadFavorites() const set new Set(ids) set.has(id) ? set.delete(id) : set.add(id) return DataStore.saveFavorites([...set]) }) } }页面不再拼 JSON、处理键名或维护写队列。仓库也不操作 ArkUI 组件只返回结果。二十一、多设备与生命周期验证phone 上快速进出模拟页tablet 分屏下切换收藏2in1 用鼠标连续点击都可能放大时序问题。需要验证窗口缩放不重复触发保存页面重建后从 Preferences 恢复回前台只刷新必要数据底部操作不被系统导航区遮挡保存中状态在宽窄窗口都清晰错误提示支持键盘和触控重试。二十二、隐私边界当前收藏、笔记、记录与学习时长都在本地 Preferences 中源码未显示上传。隐私材料应如实描述本地处理不宣称云同步。日志不能记录笔记正文或完整 JSON导出、备份、跨端同步若未来加入需重新评估权限、隐私政策和删除机制。二十三、测试保存与返回一致性核心用例interface PersistenceCase { action: save | delete | toggle | append flush: success | failure expectedUi: committed | rollback }至少测试收藏成功后列表和统计同时更新收藏写入失败时星标回滚删除笔记失败时条目保留两次并发追加均存在页面返回后读取最新状态慢请求不覆盖新请求重启后状态与退出前一致旧分钟字段只迁移一次。二十四、常见问题与修复顺序现象优先检查修复方向页面已变、重启又恢复flush 是否成功成功后提交 UI返回列表还是旧收藏onPageShow 是否刷新按所有权重新读取删除后笔记复现是否先改 UI保守提交或失败回滚统计数早于数据变化快照更新时机放在落盘成功后快速操作丢一条并发读改写整个事务串行首次进入闪空状态初始化未完成区分 loading 与 empty两次 reload 结果倒序异步竞态使用请求版本号迁移后时长翻倍重复执行迁移schema 版本与幂等二十五、发布前检查清单[ ] Preferences 是业务记录权威源[ ] AppStorage 只保存轻量快照[ ] 保存成功包含flush()[ ] UI 成功状态发生在提交后[ ] 删除失败不会伪装成功[ ] 保存失败保留用户输入[ ] 读改写操作完整串行[ ] 返回页面能获得最新数据[ ] 重复刷新不会倒序覆盖[ ] 统计快照与落盘数据一致[ ] 迁移有版本且幂等[ ] phone、tablet、2in1 完成流程验证[ ] release 包完成保存、退出、重启、删除和卸载冒烟。总结本地状态一致性的关键不是“每次都调用 save”而是明确提交协议页面生成候选状态仓库串行执行读改写Preferencesflush()成功后才更新页面和 AppStorage 快照页面返回时再按数据所有权刷新。“天体运行模拟”的 DataStore 已经具备这条链路的主体尤其是落盘后再通知统计的顺序值得保留。补上显式保存结果、失败回滚、写队列、请求版本和迁移版本后收藏、笔记、实验记录与统计就能在保存、删除、返回和重启之间保持即时一致。本文唯一标记CSDN-SERIES:ALL-163200980AI 辅助声明本文部分内容由 AI 辅助整理源码事实、工程边界与验证结论均依据文中所列项目文件复核。本文没有执行新的构建、并发压力、重启、真机或发布包验收因此相关状态均不表述为已验证。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价