尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

【知律|15】HarmonyOS ArkTS 本地状态持久化实战:让保存、删除和页面返回后的数据即时一致 本地持久化真正难的并不是调用一次putSync而是同时满足三件事用户点击收藏或保存笔记后当前页面立刻变化返回首页、错题本或设置页时统计数字同步变化应用重新启动后之前的数据还能恢复。只完成写盘页面可能仍拿着旧数组只更新 ArkUI 状态应用重启后数据又会消失。本文基于知律项目D:\huawei\one19-11、包名com.jiaweikang.one19的真实源码重点核对EntryAbility.ets、UserDataManager.ets、PracticePage.ets、FavoritePage.ets、HomePage.ets、Index.ets与SettingsPage.ets。项目面向 HarmonyOS 5.0 及以上版本当前已经用 Preferences 保存收藏、笔记、错题、题库进度、章节进度、考试历史和部分学习设置并通过AppStorage、StorageLink让多个页面观察同一份进程内状态。需要先说明边界源码没有账号体系、云数据库或跨设备同步逻辑本文不会把本机 Preferences 描述成云同步能力。讨论重点是“本机磁盘可恢复”和“ArkUI 页面即时一致”如何形成闭环。一、把持久化拆成磁盘状态和界面状态知律的本地状态实际上有两个副本Preferences 中的字符串数据用于应用退出后的恢复AppStorage中的结构化数组和设置值用于当前进程内的页面共享。这两个副本解决的问题不同。Preferences 不会自动驱动 ArkUI 重绘AppStorage也不会自动在进程结束后保留。因此一次可靠的用户操作必须同时完成磁盘写入和可观察状态替换。可以把链路写成用户操作 - 根据旧数组生成新数组 - 将新数组写入 Preferences - 方法返回新数组 - 页面赋值给 StorageLink - 其他绑定同一 AppStorage 键的页面获得新值这里最容易遗漏的是倒数第二步。页面状态必须接住持久化方法返回的新数组否则磁盘可能已经是新数据当前界面却仍显示旧数据。二、启动阶段先恢复数据再进入首页EntryAbility在加载首个页面前调用UserDataManager.init(this.context)UserDataManager.init通过preferences.getPreferencesSync打开本地存储然后读取多个键const favStr UserDataManager.prefs.getSync(UserDataManager.K_FAV, []) as string const noteStr UserDataManager.prefs.getSync(UserDataManager.K_NOTES, []) as string const wrongStr UserDataManager.prefs.getSync(UserDataManager.K_WRONG, []) as string读取后项目把 JSON 字符串解析为 ArkTS 模型并注入AppStorageAppStorage.setOrCreateFavoriteRecord[]( favoriteRecords, JSON.parse(favStr) as FavoriteRecord[] )这个时序是正确的首屏创建前完成 hydration页面第一次读取StorageLink(favoriteRecords)时就能拿到恢复值避免首页先显示 0、随后突然跳成真实数量。项目还恢复了dailyReminderTime、examDurationSec、autoNextQuestion等标量设置。虽然 Preferences 实际保存的是字符串但统一经过JSON.stringify和JSON.parse后布尔值和数字可以恢复为原类型。三、UserDataManager 用“返回新数组”连接两个世界收藏方法没有直接修改传入数组而是创建新数组static toggleFavorite( records: FavoriteRecord[], questionId: string, bankId: string ): FavoriteRecord[] { const idx records.findIndex(r r.questionId questionId) let result: FavoriteRecord[] if (idx 0) { const next [...records] next.splice(idx, 1) result next } else { result [{ questionId, bankId, createdAt: nowStr() }, ...records] } UserDataManager.persist(UserDataManager.K_FAV, result) return result }这里有两个值得保留的工程决策。第一新增和删除都产生新的数组引用。ArkUI 状态系统更容易识别引用替换避免原地push或splice后观察链路不完整。第二方法在持久化后返回同一个result。调用方不必再次查询 Preferences也不需要重复实现收藏规则。对应的页面代码是this.favRecords UserDataManager.toggleFavorite( this.favRecords, q.id, q.bankId )这行赋值同时表达了业务动作和状态提交。它比只调用toggleFavorite(...)更重要因为favRecords是StorageLink(favoriteRecords)新数组会回写共享状态。四、保存笔记为什么也要返回数组笔记使用upsertNote先过滤相同questionId的旧记录再根据文本是否为空决定更新还是删除。const filtered records.filter(r r.questionId ! questionId) if (content.trim().length 0) { result filtered } else { result [{ questionId, bankId, content: content.trim(), updatedAt: nowStr() }, ...filtered] }这段实现把“空文本等于删除笔记”固定为服务层规则。PracticePage保存时继续接住返回值this.noteRecords UserDataManager.upsertNote( this.noteRecords, q.id, q.bankId, this.noteText )因此保存对话框关闭后当前题目的笔记图标能基于新数组重新计算之后进入收藏页的笔记 Tab也会读取同一个noteRecords。这不是通过页面返回事件重新拉取磁盘实现的而是由共享状态引用替换自然传播。同样的模式用于错题this.wrongRecords UserDataManager.addWrong( this.wrongRecords, q.id, q.bankId )在错题模式答对后this.wrongRecords UserDataManager.removeWrong( this.wrongRecords, q.id )添加错题前先过滤同一题目可以避免重复记录答对后生成过滤结果可以让错题列表和导航角标共同更新。五、页面返回后的即时一致来自 StorageLinkPracticePage同时绑定收藏、错题、笔记、题库进度和章节进度StorageLink(favoriteRecords) favRecords: FavoriteRecord[] [] StorageLink(wrongRecords) wrongRecords: WrongRecord[] [] StorageLink(noteRecords) noteRecords: NoteRecord[] [] StorageLink(bankProgress) progressList: BankProgress[] [] StorageLink(chapterProgress) chapterProgressList: ChapterProgress[] []FavoritePage和SettingsPage又绑定其中相同的键Index绑定错题数据用于角标HomePage读取共享数组生成统计摘要。页面之间并没有互相持有实例也不需要发送自定义广播。这形成了一个清晰的单向数据流页面发起动作 - UserDataManager 计算新值 - 页面把返回值赋给 StorageLink - AppStorage 更新 - 所有观察相同键的组件刷新所以“返回后数据即时一致”不是依赖aboutToAppear再读一次 Preferences。路由页面返回时根页面仍然观察着共享状态即便组件重建它也会从AppStorage读取当前值。六、清空数据必须逐项提交共享状态设置页提供清空学习数据能力。真实代码不是只清磁盘而是逐项接收返回值this.favRecords UserDataManager.clearFavorites() this.noteRecords UserDataManager.clearNotes() this.wrongRecords UserDataManager.clearWrong() this.progressList UserDataManager.clearProgress() this.chapterProgressList UserDataManager.clearChapterProgress() this.examHistory UserDataManager.clearExamHistory()每个clearXxx都创建类型明确的空数组、写入对应 Preferences 键并返回空数组。这让设置页的数量、首页摘要和错题角标能够在同一次用户操作后归零。不过这组操作并不是事务。假设前两个键写入成功、第三个键失败磁盘可能处于部分清空状态。当前persist又吞掉异常页面仍会显示全部清空成功。对现有小型离线应用这种实现简单直接若清空动作承诺“全部成功或全部失败”就应升级为带结果的批量提交。建议的返回类型可以是interface PersistResultT { success: boolean value: T failedKey?: string message?: string }页面只有在success为真时替换共享状态并展示成功提示失败时保留旧值或进入可重试状态。这里是演进建议不是对现有源码能力的虚构描述。七、当前 persist 的性能和错误语义边界现有持久化方法非常集中private static persist( key: string, value: Object | string | number | boolean ): void { if (UserDataManager.prefs null) return try { UserDataManager.prefs.putSync(key, JSON.stringify(value)) UserDataManager.prefs.flushSync() } catch (_) {} }优点是调用点统一、行为易追踪收藏、笔记、错题和进度不会各自发明序列化格式。但也存在三个真实边界。1. 同步写入位于交互路径收藏、答题、保存笔记会在点击处理函数中触发flushSync。数据量小时通常可接受但题库进度和历史记录增长后JSON 序列化与同步刷盘时间可能影响 UI 响应。不能仅凭代码声称已经出现卡顿应通过实际 trace 或耗时埋点验证。2. 每次修改都重写整个数组收藏一题也会序列化完整收藏列表。Preferences 更适合轻量设置和小型数据集。若未来需要大量可查询记录、分页、索引或迁移应评估关系型数据库而不是继续扩大单个 JSON 数组。3. 异常被完全吞掉初始化失败和写入失败都没有错误日志、返回值或 UI 状态。页面无法区分“保存成功”和“内存更新但落盘失败”。至少应在开发版本记录不含敏感数据的错误类型并把失败结果传回页面。八、初始化的一个坏键会拖累全部数据init当前把所有读取和解析放在同一个try中。只要任意一个 JSON 字符串损坏就会进入统一catch把收藏、笔记、错题、进度、历史和设置全部初始化为默认值。这是一种“整体回退”策略代码短但故障隔离粒度偏大。例如只有examHistory损坏时原本有效的收藏也会在内存中变成空数组。更稳的方式是按键解析private static parseArrayT(raw: string, fallback: T[]): T[] { try { const value JSON.parse(raw) return Array.isArray(value) ? value as T[] : fallback } catch (_) { return fallback } }然后每个键独立回退。还可以对关键字段进行运行时校验例如收藏记录必须包含非空questionId和bankId。ArkTS 的as FavoriteRecord[]只影响编译期类型不会自动检查 JSON 中每个对象的真实结构。九、增加 schemaVersion才能安全演进当前存储没有显式版本号。今天的FavoriteRecord包含questionId、bankId、createdAt将来如果新增来源、标签或数据范围字段旧数据仍会被直接断言为新模型。可以新增const K_SCHEMA_VERSION: string schemaVersion const CURRENT_SCHEMA_VERSION: number 2启动时按版本执行幂等迁移读取版本 - 解析旧结构 - 补齐或转换字段 - 校验迁移结果 - 写入新结构 - 最后更新版本号版本号应最后提交避免迁移中断却提前标记完成。迁移逻辑还需要覆盖空数据、损坏数据、重复记录和降级后的旧数据不应只测试一条理想样本。十、把命名债务纳入发布复查UserDataManager当前 Preferences 存储名是private static readonly STORE_NAME: string dialect_quiz这和知律的法律学习领域不一致显然是历史模板遗留。它不必然导致运行故障但会降低排障可读性也可能在复制项目或做数据迁移时引起误判。直接改名会创建一个全新的 Preferences 空间用户原数据不会自动出现。因此不能只把字符串改成law_quiz。正确做法是先读取旧存储迁移并验证新存储再决定何时删除旧键或者保留旧名称并用注释明确兼容原因。发布前还应核对数据仅保存在本机时隐私说明不能声称上传或云同步清空学习数据的确认文案要与真实清空范围一致设置页成功提示应只在持久化成功后出现不记录法律笔记正文到普通日志卸载后本地数据的行为要与平台机制和用户说明一致。十一、推荐的职责分层在不推翻现有页面结构的前提下可以逐步把职责拆成四层ArkUI Page 负责用户动作、加载/错误/成功状态和 StorageLink 提交 UserDataService 负责收藏、笔记、错题、进度等业务变更规则 UserDataRepository 负责键、序列化、校验、版本迁移和写入结果 Preferences 负责本机轻量数据落盘AppStorage仍可作为进程内共享状态入口但不应同时承担业务规则和磁盘访问。这样可以单独测试“重复错题是否去重”“空笔记是否删除”“进度是否累加正确”也能用假的 Repository 测试写入失败时页面是否保留旧状态。十二、针对当前源码的测试矩阵持久化不能只测“重启后还在”。至少需要覆盖以下场景场景当前页面预期其他页面预期重启后预期收藏一道题图标立刻选中收藏数量增加收藏仍存在再次取消收藏图标立刻取消收藏列表移除记录不再出现保存非空笔记显示已有笔记状态笔记 Tab 增加正文可恢复保存空白笔记笔记状态取消笔记 Tab 移除记录不再出现答错一道题解析页显示错题状态角标和错题本增加错题仍存在错题模式答对当前题移出错题集合角标减少删除保持清空学习数据设置页数量归零首页和角标归零所有目标键为空单个 JSON 键损坏对应数据回退其他数据保留可继续使用写入失败明确失败提示不提交假成功状态旧数据仍可恢复对于同步写入性能可在收藏 10、100、1000 条记录时分别测量序列化和刷盘耗时并观察主线程帧耗时。只有拿到设备数据才决定是否需要异步批处理、去抖或迁移到关系型存储。十三、落地顺序应先补失败语义针对知律当前实现建议按风险从低到高推进为persist增加明确的成功/失败返回值页面不再无条件提示成功将初始化改为逐键解析和逐键回退避免一个坏键清空全部内存状态为 JSON 数据增加运行时结构校验和去重引入schemaVersion与可重复执行的迁移用性能数据决定是否把高频进度写入改为异步或批量数据规模需要查询时再评估关系型存储。这个顺序优先解决“用户看到成功但其实没落盘”的一致性问题同时保留项目现有的AppStorage和页面绑定方式不会为了架构形式一次性扩大改动面。十四、结语知律的本地状态链路已经具备一个很实用的骨架EntryAbility启动时恢复 PreferencesUserDataManager用新数组表达变更页面把返回值赋给StorageLink多个页面通过同一AppStorage键保持即时一致。收藏、笔记、错题、进度和清空操作都能从真实源码中复核到这条路径。它当前最需要补强的不是再增加一种存储而是把失败语义、逐键容错、结构校验和版本迁移补齐。只要坚持“磁盘可恢复”和“内存可观察”必须一起提交本地状态就不会在保存、删除、页面返回和应用重启之间出现两套事实。本文由 AI 辅助整理所有技术结论均基于项目真实源码复核未使用或虚构云同步、跨设备数据共享、线上指标、PV、点赞、收藏或平台推荐结果。
返回列表