# HarmonyOS 本地存储实战第三篇:用 Preferences 实现建议历史、采纳状态与首页统计
HarmonyOS 本地存储实战第三篇用 Preferences 实现建议历史、采纳状态与首页统计摘要一个生活助手 APP 如果只会“生成建议”体验会很轻如果能记录用户采纳了什么、忽略了什么、今天完成了多少就会形成真正的成长感。本文以“知行生活小助手”为例讲解如何使用 HarmonyOS Preferences 完成建议记录的本地持久化并在此基础上实现首页统计、历史分组、采纳率计算和数据清理。目录为什么先用 Preferences建议记录的数据结构Repository 层封装保存、更新、删除与清空首页统计如何计算历史记录如何分组总结一、为什么先用 PreferencesHarmonyOS 中可以选择 Preferences、关系型数据库 RDB、分布式数据等多种存储方案。知行生活小助手当前使用 Preferences 保存建议记录原因很实际数据量不大建议记录主要是文本、分类、时间戳和采纳状态。查询逻辑简单目前只需要全量读取、按时间分组、按 id 更新。开发成本低适合 MVP、课程项目和比赛原型。后续可迁移当数据量变大时可以再迁移到 RDB。对于早期项目先把业务闭环跑通比一开始就设计复杂数据库更重要。二、历史页效果与数据闭环建议发布文章时配一张历史记录页截图截图位置可以使用doc/APP_05_UI/stitch_ai_life_assistant/history_records_final_polish/screen.png历史页承接的是用户行动闭环生成建议 - 保存 SuggestionRecord - 用户采纳或忽略 - 更新 isAdopted - 首页统计刷新 - 历史页按时间分组展示这个闭环比单纯的“本地存储示例”更有项目价值。文章中建议明确说明历史记录不是为了展示列表而是为了让用户看见自己持续做出的选择。三、建议记录的数据结构项目中的记录模型如下exportinterfaceSuggestionRecord{id:string;content:string;reasoning:string;category:SuggestionCategory;iconKey?:string;isAdopted:boolean;adoptedAt:number|null;createdAt:number;updatedAt:number;}字段设计有三个亮点字段作用content用户看到的建议正文reasoning系统给出建议的理由isAdopted是否采纳支撑统计adoptedAt采纳时间支撑今日采纳数createdAt/updatedAt支撑排序、分组和同步如果要写高质量技术文章这里不要只说“定义了一个接口”而要解释每个字段服务于哪个功能。四、Repository 层封装项目将本地存储封装在SuggestionRepository中constSTORE_NAMEzhixing_suggestions;constKEY_RECORDSrecords;exportclassSuggestionRepository{privatestaticinstance:SuggestionRepository|nullnull;privatepref:preferences.Preferences|nullnull;staticgetInstance():SuggestionRepository{if(!SuggestionRepository.instance){SuggestionRepository.instancenewSuggestionRepository();}returnSuggestionRepository.instance;}asyncinit(context:Context):Promisevoid{this.prefawaitpreferences.getPreferences(context,{name:STORE_NAME});}}这里用了单例模式保证整个应用只维护一个建议仓库实例。页面不直接调用 Preferences而是通过 Service - Repository 的路径访问数据。推荐的调用链是Page - SuggestionService - SuggestionRepository - Preferences这样后续如果从 Preferences 换成 RDB只需要主要改 Repository不需要大面积改页面。五、保存、更新、删除与清空1. 读取全部记录记录以 JSON 字符串保存在 Preferences 中asyncgetAll():PromiseSuggestionRecord[]{if(!this.pref){return[];}constrawawaitthis.pref.get(KEY_RECORDS,[])asstring;try{returnJSON.parse(raw)asSuggestionRecord[];}catch{return[];}}这里有一个容错点如果 JSON 解析失败直接返回空数组避免应用崩溃。2. 保存记录保存时先读取全部记录再根据 id 判断是新增还是覆盖asyncsave(record:SuggestionRecord):Promisevoid{if(!this.pref){return;}constallawaitthis.getAll();constidxall.findIndex(rr.idrecord.id);if(idx0){all[idx]record;}else{all.unshift(record);}awaitthis.pref.put(KEY_RECORDS,JSON.stringify(all));awaitthis.pref.flush();}使用unshift可以让最新记录排在最前面历史页读取后无需再做复杂排序。3. 更新采纳状态采纳建议时只更新部分字段asyncadopt(id:string):Promisevoid{awaitthis.repo.update(id,{isAdopted:true,adoptedAt:Date.now()});this.bumpStats();}取消或忽略时则保持记录存在只改变状态asyncdismiss(id:string):Promisevoid{awaitthis.repo.update(id,{isAdopted:false});this.bumpStats();}这比直接删除记录更好因为“没有采纳”本身也是一种用户偏好数据。4. 删除与清空历史页可以删除单条记录设置页可以清空全部数据asyncdeleteById(id:string):Promisevoid{if(!this.pref){return;}constallawaitthis.getAll();constnextall.filter(rr.id!id);awaitthis.pref.put(KEY_RECORDS,JSON.stringify(next));awaitthis.pref.flush();}asyncclearAll():Promisevoid{if(!this.pref){return;}awaitthis.pref.put(KEY_RECORDS,[]);awaitthis.pref.flush();}六、首页统计如何计算首页需要展示今日采纳数、总采纳数、专注分钟等信息。聚合逻辑在 Service 层完成privateasynccomputeSummary(all:SuggestionRecord[]):PromiseSuggestionSummary{constadoptedCountall.filter(rr.isAdopted).length;consttotalCountall.length;constadoptionRatetotalCount0?adoptedCount/totalCount:0;consttodayAdoptedCountall.filter(rr.isAdoptedDateUtil.isToday(r.createdAt)).length;return{totalCount,adoptedCount,adoptionRate,todayAdoptedCount,focusMinutesToday:awaitthis.settings.getTodayFocusMinutes()};}这里的统计并不复杂但它让首页从“展示一条建议”升级为“展示用户今天的行动反馈”。七、历史记录如何分组历史页按“今天、昨天、本周”分组asyncgetGroupedHistory():PromiseGroupedHistory[]{constallawaitthis.repo.getAll();consttoday:SuggestionRecord[][];constyesterday:SuggestionRecord[][];constthisWeek:SuggestionRecord[][];for(constrofall){if(DateUtil.isToday(r.createdAt)){today.push(r);}elseif(DateUtil.isYesterday(r.createdAt)){yesterday.push(r);}else{thisWeek.push(r);}}constgroups:GroupedHistory[][];if(today.length0)groups.push({label:今天,records:today});if(yesterday.length0)groups.push({label:昨天,records:yesterday});if(thisWeek.length0)groups.push({label:本周,records:thisWeek});returngroups;}这样的分组方式符合用户浏览习惯用户通常不是按数据库时间戳看记录而是按“今天做了什么、昨天有没有坚持、本周整体如何”来回顾。八、数据一致性与异常处理本地存储最容易被忽略的是异常场景。当前 Repository 已经处理了部分异常例如 JSON 解析失败时返回空数组try{returnJSON.parse(raw)asSuggestionRecord[];}catch{return[];}如果继续提升稳定性可以补充以下策略场景建议处理Preferences 未初始化页面进入前强制调用service.init(ctx)JSON 解析失败返回空数组并可记录 hilog保存失败给用户 toast并保留当前页面状态记录过多设置最大保存条数或迁移到 RDB多端同步增加updatedAt冲突处理为什么要调用flush()put只是写入缓存flush才能确保数据落盘awaitthis.pref.put(KEY_RECORDS,JSON.stringify(all));awaitthis.pref.flush();如果没有flush()应用退出或页面快速切换时可能出现记录没有保存的情况。九、从 Preferences 迁移到 RDB 的思路当历史记录变多以后可以考虑迁移到关系型数据库。迁移思路如下保留 SuggestionRecord 模型 - 新建 RdbSuggestionRepository - 实现 getAll/save/update/deleteById/clearAll - Service 层保持不变 - 页面层保持不变这说明当前分层设计是有价值的Repository 是可替换的业务层和 UI 层不需要关心底层存储细节。十、测试用例建议用例操作预期新增记录生成一条建议历史列表新增一条采纳记录点击采纳按钮isAdoptedtrue首页采纳数增加忽略记录点击换一条未采纳记录保留生成新记录删除记录历史页删除当前记录不再显示清空记录设置页清空数据首页统计归零异常 JSON手动写入非法 JSON应用不崩溃返回空列表十一、常见问题与踩坑1. 为什么历史页偶尔为空优先检查SuggestionService.init(ctx)是否在页面aboutToAppear中执行。如果 Repository 没有拿到 Preferences 实例getAll()会返回空数组。2. 为什么采纳后首页统计没有马上变化检查adopt()后是否调用了首页刷新逻辑。项目中通过refresh()重新获取HomeData并用AppStorage写入统计 tick避免页面显示旧数据。3. 为什么不建议页面直接操作 Preferences页面直接操作存储会让 UI、业务和数据耦合在一起。后续换 RDB、加同步、做测试都会变得很麻烦。Repository 层看起来多了一层文件但它是项目可维护性的关键。十二、参考资料HarmonyOS ArkData Preferences 官方文档。HarmonyOS ArkTS 异步编程实践。CSDN 质量分规则代码格式、段落结构、正文长度、实用性和可验证步骤都会影响文章质量。项目文件SuggestionRepository.ets、SuggestionService.ets、SuggestionRecord.ets。十三、互动问题这个项目目前使用 Preferences 保存历史记录。你觉得什么时候应该迁移到 RDB记录超过 100 条。需要按分类筛选。需要多端同步。需要复杂统计图表。十四、发布前质量自检这篇文章属于“本地存储实战类”质量分想稳定在 90需要避免写成纯 API 说明。发布前建议检查下面几点检查项当前文章处理是否有真实业务场景建议历史、采纳状态、首页统计是否有数据结构SuggestionRecord、SuggestionSummary是否有分层设计Page - Service - Repository - Preferences是否有核心代码getAll、save、update、delete、clear、computeSummary是否有异常处理JSON 解析失败、Preferences 未初始化、flush 说明是否有迁移思路Preferences 到 RDB 的演进路径是否有测试用例新增、采纳、忽略、删除、清空、异常 JSON是否有互动问题已补充 RDB 迁移讨论十五、总结知行生活小助手使用 Preferences 实现了轻量持久化通过 Repository 层封装读写逻辑再由 Service 层计算首页统计和历史分组。这个方案简单、稳定、易讲清楚非常适合 HarmonyOS 项目早期阶段。后续如果数据规模扩大可以将SuggestionRepository替换为 RDB 实现而页面层基本不需要变化。