news 2026/9/1 20:34:56

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

作者头像

张小明

前端开发工程师

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

【天体运行模拟|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 ): Promise<void> { 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 ): Promise<SaveResult> { 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[] ): Promise<void> { await DataStore.putString( 'favorite_experiments', JSON.stringify(ids) ) }

实验名称、描述和图标仍由实验目录维护。应用升级后,只要 ID 稳定,收藏就能映射到新模型。保存完整实验对象反而容易产生旧副本。

六、收藏写入前先去重

快速点击或多个入口可能产生重复 ID:

function normalizeFavoriteIds( ids: string[] ): string[] { return [...new Set( ids.filter(id => id.trim().length > 0) )] }

去重规则应放在仓库或 DataStore 上层,而不是每个页面各写一次。

七、读改写必须防止覆盖

收藏切换、实验计数和学习时间都属于“读旧值、计算新值、整体写回”。两个异步操作并发时可能丢失一次更新。

private static writeChain: Promise<void> = Promise.resolve()
static enqueueWrite( task: () => Promise<void> ): Promise<void> { DataStore.writeChain = DataStore.writeChain.then(task, task) return DataStore.writeChain }

同一进程内用写队列串行化即可。未来若引入多端同步,需使用版本号与冲突策略,内存队列不再足够。

八、实验记录追加的真实风险

当前代码:

static async appendRecord<T>( record: T ): Promise<void> { const list = await DataStore.loadRecords<T>() 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 = 0
private async reload(): Promise<void> { const version = ++this.reloadVersion const result = await repository.listFavorites() if (version !== this.reloadVersion) return this.favoriteIds = result }

十一、不要用 AppStorage 保存完整业务数组

源码只把计数写入 AppStorage:

AppStorage.setOrCreate<number>( FAVORITE_COUNT_KEY, ids.length )

这是合理边界。完整收藏和笔记仍由 Preferences 管理,AppStorage 只承载favorite_countexperiment_count_totallearning_seconds_total等快照。否则页面修改数组与持久层读取会形成两个权威来源。

十二、stats_version 的作用

每次相关键变化时:

DataStore.statsVersion++ AppStorage.setOrCreate<number>( 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

迁移步骤:

  1. 读取当前版本;
  2. 只执行缺失步骤;
  3. 新值落盘成功;
  4. 再更新版本;
  5. 重复启动不会二次换算。

迁移失败时保留旧字段,不能先删除再尝试写新值。

十六、保存中状态防止重复点击

页面应显式维护:

@State saving: boolean = false
private async save(): Promise<void> { 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' | 'error'

Preferences 尚未初始化、读取异常不能显示 empty。返回页面刷新时可保留旧 content 并显示轻量刷新态,避免整页闪白。

十九、刷新策略按数据所有权决定

收藏页返回时重新读取,因为其他页面可以修改收藏;笔记列表若只有自身编辑,也可在本地提交后直接更新;统计页观察stats_version即可。

页面推荐刷新触发
收藏页aboutToAppear / onPageShow
笔记页保存、删除成功后本地更新;返回时复核
实验记录页页面显示时读取
我的统计观察快照版本

不是所有页面都需要轮询或全量刷新。

二十、持久化仓库的最小边界

export class LearningStateRepository { async toggleFavorite( id: string ): Promise<SaveResult> { 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' }

至少测试:

  1. 收藏成功后列表和统计同时更新;
  2. 收藏写入失败时星标回滚;
  3. 删除笔记失败时条目保留;
  4. 两次并发追加均存在;
  5. 页面返回后读取最新状态;
  6. 慢请求不覆盖新请求;
  7. 重启后状态与退出前一致;
  8. 旧分钟字段只迁移一次。

二十四、常见问题与修复顺序

现象优先检查修复方向
页面已变、重启又恢复flush 是否成功成功后提交 UI
返回列表还是旧收藏onPageShow 是否刷新按所有权重新读取
删除后笔记复现是否先改 UI保守提交或失败回滚
统计数早于数据变化快照更新时机放在落盘成功后
快速操作丢一条并发读改写整个事务串行
首次进入闪空状态初始化未完成区分 loading 与 empty
两次 reload 结果倒序异步竞态使用请求版本号
迁移后时长翻倍重复执行迁移schema 版本与幂等

二十五、发布前检查清单

  • [ ] Preferences 是业务记录权威源;
  • [ ] AppStorage 只保存轻量快照;
  • [ ] 保存成功包含flush()
  • [ ] UI 成功状态发生在提交后;
  • [ ] 删除失败不会伪装成功;
  • [ ] 保存失败保留用户输入;
  • [ ] 读改写操作完整串行;
  • [ ] 返回页面能获得最新数据;
  • [ ] 重复刷新不会倒序覆盖;
  • [ ] 统计快照与落盘数据一致;
  • [ ] 迁移有版本且幂等;
  • [ ] phone、tablet、2in1 完成流程验证;
  • [ ] release 包完成保存、退出、重启、删除和卸载冒烟。

总结

本地状态一致性的关键不是“每次都调用 save”,而是明确提交协议:页面生成候选状态,仓库串行执行读改写,Preferencesflush()成功后才更新页面和 AppStorage 快照;页面返回时再按数据所有权刷新。

“天体运行模拟”的 DataStore 已经具备这条链路的主体,尤其是落盘后再通知统计的顺序值得保留。补上显式保存结果、失败回滚、写队列、请求版本和迁移版本后,收藏、笔记、实验记录与统计就能在保存、删除、返回和重启之间保持即时一致。

本文唯一标记:CSDN-SERIES:ALL-163200980

AI 辅助声明:本文部分内容由 AI 辅助整理,源码事实、工程边界与验证结论均依据文中所列项目文件复核。本文没有执行新的构建、并发压力、重启、真机或发布包验收,因此相关状态均不表述为已验证。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 20:34:11

技术博客写作的底线:拒绝空壳,不编造教程

很抱歉&#xff0c;这个标题&#xff08;《カミイロアワセ【雾岛学院/反转pa】》&#xff09;看起来是同人创作、虚构故事或二次元作品相关的内容&#xff0c;并不属于技术项目、AI 模型、开源工具或本地部署教程的范畴。我的人物设定是撰写 CSDN 技术博客&#xff0c;内容需要…

作者头像 李华
网站建设 2026/9/1 20:32:57

饲料破碎机毕业设计全流程:SolidWorks建模+CAD出图+说明书写作指南

简介&#xff1a;本资源是一套面向机械设计类本科生的毕业设计完整实践包&#xff0c;聚焦饲料破碎机这一典型农业装备的工程化设计全流程&#xff0c;解决学生从任务理解、三维建模、工程制图到技术文档撰写的综合能力训练需求。压缩包共212个文件&#xff0c;含108个SolidWor…

作者头像 李华
网站建设 2026/9/1 20:30:15

以洱海SHP为例:GIS底图检查、处理与典型应用

简介&#xff1a;洱海SHP文件是一套面向GIS操作与空间分析的底图矢量数据&#xff0c;适用于需要制作洱海地图、统计水域面积、开展缓冲区分析或环境规划研究的技术人员。压缩包共31个文件&#xff0c;整体约8.86MB&#xff0c;除3个SHP主文件外&#xff0c;还有配套的DBF属性表…

作者头像 李华
网站建设 2026/9/1 20:29:23

音视频修炼之基础理论(四):色彩工程

色彩工程深挖 —— BT.709 / BT.2020 / HDR10 / Dolby Vision同一段视频在不同手机上颜色不一样、Netflix 4K HDR 看着特别亮、抖音红色"特别红"——这些都跟"色彩工程"有关。这一篇把色彩空间、色域、HDR、Tone Mapping 的核心概念讲明白。本文速览章节阅…

作者头像 李华
网站建设 2026/9/1 20:29:08

睿答架构解析

从 0 到 1 拆解「睿答」&#xff1a;一个面向虾皮卖家的 AI 客服系统是怎么搭起来的作者按&#xff1a;这篇文章从工程视角复盘我们做「睿答&#xff08;ReplyGen&#xff09;」时做的架构选型、关键模块设计与踩过的坑。它不是产品软文&#xff0c;而是一份给同行的技术解剖—…

作者头像 李华