ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

【中国方言题库|15】HarmonyOS ArkTS 本地状态持久化实战:让保存、删除和页面返回后的数据即时一致 本地应用最容易出现的状态问题并不是“数据完全没保存”而是两套状态不同步用户刚收藏题目当前按钮已经变色但返回首页统计仍是旧数字删除错题后列表消失重新启动又回来设置页显示了新考试时长练习页却仍读取旧值。根因通常是页面状态与持久化存储各自维护一份数据却没有明确谁负责写入、谁负责通知 UI。中国方言题库采用一条简单但可复核的链路Preferences 保存可跨进程重启恢复的数据UserDataManager统一序列化与业务更新AppStorage承载当前运行时共享状态页面通过StorageLink消费并把服务返回的新数组重新赋值。本文面向 HarmonyOS 5.0 及以上版本结合收藏、笔记、错题、进度、考试历史和学习设置的真实 ArkTS 源码解释保存、删除和页面返回后的数据为何能即时一致也会明确当前异常处理与数据迁移上的边界。本文唯一核验标记先持久化新值再把同一结果写回共享状态。一、持久化链路涉及哪些真实文件本文主要复核librarya/src/main/ets/utils/UserDataManager.ets entry/src/main/ets/entryability/EntryAbility.ets entry/src/main/ets/pages/PracticePage.ets entry/src/main/ets/views/FavoritePage.ets entry/src/main/ets/views/HomePage.ets entry/src/main/ets/views/MinePage.ets entry/src/main/ets/pages/SettingsPage.ets entry/src/main/ets/pages/ExamResultPage.ets entry/src/main/ets/common/components/BankCard.ets数据只保存在本地 Preferences没有云同步、账号绑定、跨设备同步或导出备份。文章不会把AppStorage描述成磁盘存储也不会把 Preferences 描述成数据库。二、先区分运行时状态与持久化状态当前架构中有两层数据AppStorage StorageLink - 当前进程内的共享响应式状态 Preferences - 应用重新启动后仍可恢复的键值数据页面直接读取StorageLink所以 AppStorage 决定“当前 UI 何时更新”UserDataManager.persist()写 Preferences所以 Preferences 决定“下次启动能否恢复”。两层缺一不可。三、为什么页面不直接操作 Preferences如果练习页、收藏页、设置页都直接调用 Preferences会产生多套键名、序列化格式和异常策略。当前项目把所有存储键集中在UserDataManagerprivate static readonly STORE_NAME dialect_quiz private static readonly K_FAV favoriteRecords private static readonly K_NOTES noteRecords private static readonly K_WRONG wrongRecords private static readonly K_PROGRESS bankProgress private static readonly K_EXAM examHistory private static readonly K_CHAPTER chapterProgress设置项也有各自键名。页面只表达“切换收藏”“保存笔记”“清空错题”这类业务动作。四、持久化数据模型保持最小字段收藏记录只保存export interface FavoriteRecord { questionId: string bankId: string createdAt: string }笔记保存题目、题库、内容和更新时间错题保存题目、题库和错误时间进度保存题库 ID、累计已答、累计答对、最后章节和更新时间。没有把题干、选项、封面等目录数据重复写入 Preferences。页面需要展示时再用questionId与bankId回查本地题库目录减少冗余和数据不一致。五、EntryAbility 在首屏之前恢复数据应用创建时调用UserDataManager.init(this.context)这发生在windowStage.loadContent(pages/SplashPage)之前。init()同步取得 Preferences读取各键解析 JSON并写入 AppStorage。因此主页、题库卡片、收藏页第一次构建时就能读取恢复后的状态不需要每个页面重复发起一次异步加载。六、Preferences 实例只在服务内部保存服务字段为private static prefs: preferences.Preferences | null null初始化时UserDataManager.prefs preferences.getPreferencesSync( context, { name: UserDataManager.STORE_NAME } )页面不持有 Context也不保存 Preferences 实例。平台存储能力被限制在公共服务层UI 组件只依赖类型化方法。七、所有复杂数据统一序列化为 JSON读取收藏列表时const favStr UserDataManager.prefs.getSync( UserDataManager.K_FAV, [] ) as string AppStorage.setOrCreateFavoriteRecord[]( favoriteRecords, JSON.parse(favStr) as FavoriteRecord[] )数组和对象都以 JSON 字符串保存。数字、布尔和字符串设置也同样经过JSON.stringify()因此读取默认值必须是合法 JSON时间默认值是\09:00\数字是1800布尔值是false。八、为什么字符串默认值需要双层引号JSON.parse(09:00)会失败因为它不是合法 JSON 字符串JSON.parse(09:00)才返回普通字符串。源码中const reminderStr prefs.getSync(K_DAILY_REMINDER, 09:00) as string这一细节保证首次启动也能通过统一 JSON 解析链路得到09:00。九、初始化失败时会整体回退默认值init()把 Preferences 获取、所有键读取和所有 JSON 解析放在一个try/catch中。异常时写入favoriteRecords [] noteRecords [] wrongRecords [] bankProgress [] examHistory [] chapterProgress [] dailyReminderTime 09:00 examDurationSec 1800 autoNextQuestion false这能避免损坏数据阻塞应用启动但当前不是逐字段恢复一个键解析失败可能让本次运行的整组状态回退默认值。十、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 (_) {} }业务方法不重复写putSync()和flushSync()。键名、序列化与提交都集中在一个位置。十一、同步写入为何适合当前数据量当前数据是收藏、笔记、错题、进度和少量设置集合规模有限。同步写入使业务方法可以在返回前完成持久化调用侧不需要处理 Promise。但同步并不意味着可以无限扩张。若未来笔记很长、记录数成千上万或写入频繁flushSync()可能影响交互线程需要用真实性能数据评估异步写、批处理或结构化存储。十二、收藏切换如何同时解决新增与删除toggleFavorite()先查找题目const idx records.findIndex( r r.questionId questionId )存在则复制数组并删除不存在则把新记录放到头部result [{ questionId, bankId, createdAt: nowStr() }, ...records]最后持久化result并返回同一个结果数组。十三、调用侧必须重新赋值练习页的真实写法是this.favRecords UserDataManager.toggleFavorite( this.favRecords, q.id, q.bankId )服务负责生成新数组并写磁盘页面负责把返回值赋给StorageLink。只有两步都完成当前 UI 与下次启动的数据才一致。如果只调用服务但不赋值Preferences 可能已更新当前页面却仍引用旧数组如果只改页面数组而不调用服务当前 UI 正常重启后数据会丢失。十四、不可变数组让响应式更新更明确收藏删除时源码没有直接对传入数组执行splice()而是先复制const next [...records] next.splice(idx, 1) result next笔记、错题和进度也会构造新数组。新的引用被赋给StorageLink后ArkUI 更容易识别状态变化其他消费同一 AppStorage 键的页面也会得到新值。十五、笔记保存与删除共用 upsertupsertNote()先移除旧记录const filtered records.filter( r r.questionId ! questionId )内容为空时直接返回过滤后的数组相当于删除笔记内容非空时把新版本放到数组头部result [{ questionId, bankId, content: content.trim(), updatedAt: nowStr() }, ...filtered]页面编辑弹窗保存后把返回值写回noteRecords弹窗关闭时列表已经读取到最新共享状态。十六、错题为何按 questionId 去重addWrong()会先过滤同题旧记录再把新记录插入头部const filtered records.filter( r r.questionId ! questionId ) const result: WrongRecord[] [{ questionId, bankId, wrongAt: nowStr() }, ...filtered]同一道题再次答错只更新时间并移动到最前不会无限追加重复项。错题模式答对后removeWrong()过滤该题并持久化。十七、答题动作如何即时影响收藏页徽标练习页选择错误答案时this.wrongRecords UserDataManager.addWrong( this.wrongRecords, q.id, q.bankId )Index、首页、收藏页和“我的”都通过StorageLink(wrongRecords)读取同一个键。赋值完成后错题数量、收藏 Tab 徽标和快捷入口文案都能基于新数组重新计算。不需要页面返回后再重新读取 Preferences。十八、进度更新采用“旧值 本次增量”updateProgress()根据bankId查找旧记录。存在时构造const updated: BankProgress { bankId, finished: old.finished addFinished, correct: old.correct addCorrect, lastChapterId: chapterId, updatedAt: nowStr() }不存在时创建首条记录。它记录的是累计答题次数和答对次数不是去重完成题目集合。十九、题库进度与章节进度分别保存题库级键是bankProgress章节级键是chapterProgress。章节记录用bankId chapterId作为查找条件records.findIndex( r r.bankId bankId r.chapterId chapterId )练习完成时先更新题库进度章节模式下再更新章节进度。题库详情页可分别展示总体完成情况和各章节数据。二十、考试历史为什么最新记录在前addExamHistory()构造const result: ExamHistory[] [{ bankId, score, total, correct, durationSec, finishedAt: nowStr() }, ...records]最新记录位于数组头部考试页和统计服务可以直接读取最近 N 条。页面把返回值赋给examHistory后“我的”考试次数和首页统计也会立即变化。二十一、设置值采用“共享状态 持久化方法”设置页修改考试时长时this.examDurationSec value this.displayExamDurationSec value UserDataManager.saveExamDurationSec(value)第一行更新跨页面共享值练习页的StorageLink(examDurationSec)可立即读取第二行更新设置页的展示副本第三行保证下次启动恢复。每日提醒时间与自动下一题采用相同模式。二十二、为什么设置页还有 display 副本设置页同时维护持久化关联状态和当前展示状态StorageLink(examDurationSec) examDurationSec: number 1800 State displayExamDurationSec: number 1800aboutToAppear()会把共享值同步到展示值并刷新各数据数量。当前代码通过settingsRevision和dataRevision辅助触发相关显示更新。这比直接从 Preferences 读取更轻因为返回页面时 AppStorage 已经是当前运行时真值。二十三、页面返回后为何不需要再次查磁盘用户从练习页返回题库或首页时根页面仍通过StorageLink观察同一 AppStorage 键。练习页已经把服务返回的新数组赋值因此返回后页面直接呈现最新进度。Preferences 的作用是跨重启恢复不是每次路由返回都重新查询。把磁盘读取限制在启动初始化减少重复 I/O 和页面级加载状态。二十四、删除操作为什么也必须返回新数组收藏页清空错题this.wrongRecords UserDataManager.clearWrong()clearWrong()先把空数组写入 Preferences再返回空数组。调用侧赋值后列表、徽标和统计同时归零。如果服务只执行persist(K_WRONG, [])而不返回值调用侧还要自己构造空数组容易出现磁盘和 UI 使用不同结果。二十五、清空全部数据如何保持多键一致设置页二次确认后依次调用this.favRecords UserDataManager.clearFavorites() this.noteRecords UserDataManager.clearNotes() this.wrongRecords UserDataManager.clearWrong() this.progressList UserDataManager.clearProgress() this.chapterProgressList UserDataManager.clearChapterProgress() this.examHistory UserDataManager.clearExamHistory()随后把显示计数归零并提示“所有学习数据已清除”。当前是多个独立同步写不是事务中途若有写入失败可能出现部分键已清空、部分键仍保留。二十六、二次确认避免误删requestClearAll()第一次点击只设置this.pendingClear true this.showTip( 再次点击红色按钮确认清除所有学习数据 )再次点击才执行实际清空。它不是系统弹窗但明确区分意图确认和不可恢复的数据删除。二十七、当前 persist 会吞掉写入异常persist()的catch为空并且返回类型是void。如果putSync()或flushSync()失败页面仍会把服务返回的新数组写入 AppStorage。结果可能是“当前运行时看起来成功但重启后恢复旧数据”。因此当前实现保障的是正常路径下的一致性不具备可见的写失败反馈或回滚。二十八、为什么不能声称保存一定成功服务在prefs null时直接返回也不会通知调用侧。页面无法区分持久化成功 Preferences 尚未初始化 putSync 失败 flushSync 失败更稳健的演进方式是让持久化方法返回布尔值或明确结果并让页面在失败时提示、重试或恢复旧状态。但这是改进方向不是当前已有能力。二十九、初始化解析也缺少结构校验JSON.parse()后直接使用类型断言JSON.parse(progStr) as BankProgress[]类型断言只影响编译期不会在运行时检查每一项是否包含bankId、finished、correct。格式合法但结构错误的数据仍可能进入 AppStorage。当前数据由同一应用写入风险有限加入版本迁移、导入或外部数据后需要显式校验。三十、当前没有数据版本与迁移Preferences 存储中没有 schema version。若未来给BankProgress增加必填字段旧用户数据不会自动转换。可在存储中加入版本键并把迁移放在UserDataManager.init()的服务边界内。页面不应知道旧版本格式也不应在多个页面各自修复数据。三十一、当前数据没有加密或账号隔离收藏、笔记、错题和学习进度保存在应用本地 Preferences。源码没有加密、用户账号维度或云端上传。这些数据主要是学习记录不包含密码或支付信息。若未来加入个人身份、账号或敏感内容应重新评估存储位置、加密、删除、隐私披露与备份策略。三十二、AppStorage 键名也是运行时契约服务初始化使用favoriteRecords页面必须写StorageLink(favoriteRecords) favRecords: FavoriteRecord[] []键名是字符串编译器无法发现拼写不一致。当前项目把键名在服务与页面中重复书写规模尚可但后续可以集中为常量避免某页监听了一个永远不会更新的新键。三十三、职责边界可以归纳为四层当前数据流分为UI Action - 产生保存、删除、答题或设置意图 UserDataManager - 计算新值、序列化并写 Preferences AppStorage - 保存当前运行时共享结果 Consumer Pages - 通过 StorageLink 自动消费新状态页面不直接拼 JSON服务不负责绘制提示Preferences 不承担响应式通知AppStorage 不承担跨重启保存。三十四、如何验证收藏的一致性建议执行1. 进入练习页收藏一道题 2. 当前收藏按钮立即变为已收藏 3. 返回收藏页该题立即出现 4. 首页收藏统计立即加一 5. 结束应用并重新启动 6. 收藏记录仍然存在 7. 再次取消收藏 8. 当前页、收藏页和首页统计同步减少 9. 重启后确认已删除这组测试同时覆盖 AppStorage 即时更新和 Preferences 重启恢复。三十五、如何验证错题与进度错题测试应覆盖答错新增、同题再次答错不重复、错题模式答对后移除、清空后徽标归零、重启后保持。进度测试应覆盖随机练习、章节练习、考试自动交卷和正常交卷。由于finished是累计答题次数重复练习后允许超过题库题量进度条会限制到 100%但文字仍显示累计值。三十六、如何验证设置返回后的即时一致在设置页修改考试时长与自动下一题后返回并进入练习页检查examDurationSec 是否使用新值 autoNextQuestion 是否立即生效 设置页再次进入是否显示新值 应用重启后是否恢复新值同时模拟或制造 Preferences 写失败的开发场景确认当前 UI 可能先更新而磁盘未成功并为后续错误反馈改造提供证据。三十七、适合当前项目的演进顺序在保持架构简单的前提下可按风险排序让persist()返回成功或失败结果不再静默吞错。把 AppStorage 键名集中为类型化常量。将各键解析隔离避免单键损坏导致全部回退。增加运行时结构校验与存储版本号。清空全部数据需要更强一致性时再设计批量提交或恢复策略。数据规模显著扩大后再评估 RDB 或异步存储而不是提前迁移。三十八、这套实现真正保证了什么在正常写入路径下它保证业务更新只在 UserDataManager 中计算 写入和返回使用同一个新值 页面将返回值赋给 StorageLink 当前所有消费页即时读取同一 AppStorage 状态 应用重启时从 Preferences 恢复 删除与清空同样返回新数组 页面返回不必重新查询磁盘它没有保证写失败可见、跨键事务、结构迁移、加密、云同步或跨设备一致性。三十九、结语中国方言题库没有让 Preferences 直接散落在 ArkUI 页面中而是由UserDataManager统一管理键名、JSON 和业务更新。页面每次保存或删除时把服务返回的新数组写回StorageLink首页、收藏页、“我的”和题库卡片因此共享同一运行时状态应用重新启动后再由EntryAbility恢复磁盘数据。这条“服务生成新值并持久化页面把同一结果写回共享状态”的链路是当前即时一致性的核心。与此同时静默写失败、整体解析回退、无结构校验和无版本迁移仍是明确边界。把这些限制如实保留比笼统宣称“本地数据永不丢失”更符合工程事实。AI 辅助声明本文由 AI 辅助整理与润色存储键、数据模型、初始化顺序、写入流程、页面赋值和异常边界均依据项目真实源码复核。
返回列表