答案之书-02-离线应用别让页面直接读 Preferences:封一层仓库再写业务

发布时间:2026/9/13 8:14:21

答案之书-02-离线应用别让页面直接读 Preferences:封一层仓库再写业务 答案之书 序列2离线应用别让页面直接读 Preferences封一层仓库再写业务在答案之书这种离线小工具里最容易被低估的不是页面布局而是本地数据边界。页面直接读 Preferences 的时候第一版确实写得快点按钮、拿字符串、JSON.parse、再刷新列表。但题库、收藏、历史、当前题库和默认种子版本都进来以后页面会同时背上 key 命名、序列化、异常兜底和刷新通知。最后问题会变成某个页面能跑整个应用的数据链路不可信。这篇文章会把问题拆成四个可落地的点把 Preferences 收进统一 Store页面不直接碰持久化细节。用 Repository 管 key 和序列化用 Service 管业务规则。写清楚 JSON 损坏、索引悬挂、删除当前题库这类真实故障的处理边界。给出能迁移到本地离线 HarmonyOS 应用的数据分层方法。1. 页面直接读写的坏处不是代码长实际项目里页面直接调用 Preferences 最先暴露的问题通常不是崩溃而是每个页面都以为自己知道数据格式。首页保存当前题库题库页保存列表收藏页保存来源历史页又单独维护时间线一旦 key 改名或 JSON 结构升级排查点会散在多个 ETS 文件里。答案之书把底层入口收进PreferencesStore让 UI 只面对领域方法。classPreferencesStoreImpl{privatereadonlystores:Mapstring,preferences.PreferencesnewMap();privatecontext:common.UIAbilityContext|nullnull;privateinited:booleanfalse;asyncinit(context:common.UIAbilityContext):Promisevoid{if(this.inited){return;}this.contextcontext;awaitthis.openStore(PrefStoreName.App);awaitthis.openStore(PrefStoreName.Deck);awaitthis.openStore(PrefStoreName.Favorite);this.initedtrue;}}这段代码只负责一件事把持久化 store 的生命周期集中起来。页面不需要知道preferences.getPreferences什么时候打开也不需要在每个入口重复判断是否初始化。2. 先按数据域拆 storeName答案之书没有把所有内容塞进一个 Preferences 文件。当前题库、种子版本属于应用域题库索引和题库实体属于题库域收藏列表属于收藏域。这样拆的价值在发布后才明显用户反馈收藏丢失时不需要怀疑题库实体要做备份或清理时也能按域给出明确策略。exportclassPrefStoreName{staticreadonlyApp:stringpref_app;staticreadonlyDeck:stringpref_deck;staticreadonlyFavorite:stringpref_favorite;}exportclassAppPrefKey{staticreadonlyCurrentDeckId:stringcurrent_deck_id;staticreadonlyLastSeededVersion:stringlast_seeded_version;staticreadonlySchemaVersion:stringschema_version;}这里的拆分不是为了显得分层而是为了让数据归属可以被追踪。一个 key 属于哪个域后续的迁移、清理和排障就从哪个域开始。3. Repository 管 key不替业务做决定Repository 的责任是稳定读写不是判断业务能不能发生。比如题库仓库知道deck_index和deck:id的关系但它不应该决定默认题库能不能删除也不应该决定答案最少几条。这些规则放到 Service仓库保持薄而可靠。classDeckPrefKey{staticreadonlyIndex:stringdeck_index;staticdeckEntry(id:string):string{returndeck:${id};}}asyncsaveDeck(deck:Deck):Promisevoid{awaitPreferencesStore.setJsonDeck(PrefStoreName.Deck,DeckPrefKey.deckEntry(deck.id),deck);constids:string[]awaitthis.loadIndex();if(!ids.includes(deck.id)){awaitthis.saveIndex(ids.concat(deck.id));}}这段写法保证了实体和索引的关系不散落到页面。页面只触发保存Service 决定能不能保存Repository 保证保存后索引仍能找到实体。4. JSON 解析失败要回退也要留下线索本地 Preferences 最常见的脏数据来自开发期结构变更、导入覆盖和手工调试。解析失败时直接崩溃会伤用户但静默返回空值也危险因为页面会误判成“用户没有数据”。更稳的做法是返回 fallback同时把 store 和 key 打进日志。asyncgetJsonT(storeName:string,key:string,fallback:T):PromiseT{constraw:stringawaitthis.getString(storeName,key,);if(!raw){returnfallback;}try{returnJSON.parse(raw)asT;}catch(err){hilog.warn(DOMAIN,TAG,parse json failed, store%{public}s key%{public}s,storeName,key);returnfallback;}}fallback 是给运行期兜底日志是给排查留证。两者缺一都会把问题推到页面层。5. Service 才能决定什么能写进去答案之书的题库保存不是简单落盘。题库名要 trim空答案要过滤答案长度要限制默认颜色要补齐已有题库的创建时间和内置标记要保留。这些规则如果写在页面里导入页、新建页、编辑页迟早会不一致。asyncsave(payload:SaveDeckPayload):PromiseDeck{consttrimmedName:stringpayload.name.trim();if(!trimmedName){thrownewError(题库名称不能为空);}constcleaned:string[]payload.answers.map((item:string):stringitem.trim()).filter((item:string):booleanitem.length0item.lengthMAX_ANSWER_LEN);if(cleaned.lengthMIN_ANSWERS_PER_DECK){thrownewError(题库至少需要${MIN_ANSWERS_PER_DECK}条答案);}returnthis.persistDeck(payload.id,trimmedName,cleaned);}Service 接收页面 payload但不信任 payload。它统一做清洗和校验Repository 只保存已经成形的领域对象。6. 运行期刷新不要指望 Preferences 自动通知Preferences 是持久层不是页面状态总线。题库保存成功后列表页和首页需要知道“题库数据发生了变化”。答案之书用AppStorageKey.LastDeckUpdateAt做轻量刷新信号页面通过StorageLink订阅再重新从 Service 读取最新数据。awaitDeckRepository.saveDeck(deck);AppStorage.setOrCreate(AppStorageKey.LastDeckUpdateAt,deck.updatedAt);StorageLink(lastDeckUpdateAt)Watch(onDeckUpdate)lastDeckUpdateAt:number0;privateonDeckUpdate():void{this.reloadDecks().catch((err:Error){hilog.warn(DOMAIN,TAG,reload deck list failed: %{public}s,err.message);});}这里的关键点是 AppStorage 只放刷新信号不放完整题库。完整数据仍由 Service 从持久层读取避免页面之间复制多个版本。7. 删除当前题库时要同时处理回退很多离线应用的数据问题发生在删除路径实体删了索引没删索引删了当前选中 id 还指向旧实体页面刷新后还拿旧 id 去抽答案。答案之书把删除题库后的回退放在 Service确保当前题库始终可用。asyncremoveDeck(id:string):Promisevoid{constdeck:Deck|nullawaitDeckRepository.loadDeck(id);if(deck?.builtIn){thrownewError(默认题库不能删除);}awaitDeckRepository.removeDeck(id);constcurrentId:stringawaitAppRepository.loadCurrentDeckId();if(currentIdid){constids:string[]awaitDeckRepository.loadIndex();constfallback:stringids[0]??DEFAULT_DECK_ID;awaitAppRepository.saveCurrentDeckId(fallback);AppStorage.setOrCreate(AppStorageKey.CurrentDeckId,fallback);}AppStorage.setOrCreate(AppStorageKey.LastDeckUpdateAt,Date.now());}删除不是单点操作而是实体、索引、当前选择和刷新信号的组合事务。即使底层不是数据库也要按事务思路收口。8. 迁移策略放启动期不放页面里当题库结构升级时不要让每个页面自己判断版本。更稳的做法是在启动链路读取 schemaVersion完成迁移后再挂载页面。页面永远消费当前结构旧数据兼容问题停在启动和仓储层。asyncfunctionmigrateIfNeeded():Promisevoid{constversion:numberawaitPreferencesStore.getNumber(PrefStoreName.App,AppPrefKey.SchemaVersion,0);if(version2){awaitDeckMigration.fillMissingAnswerIds();}awaitPreferencesStore.setNumber(PrefStoreName.App,AppPrefKey.SchemaVersion,2);}迁移逻辑越靠近页面越难保证所有入口一致。启动期迁移能让错误更早暴露也让页面代码保持稳定。9. 验证与排障这层的验证要刻意制造坏输入而不是只走正常新建流程。建议准备空题库名、过长答案、破坏 JSON、删除当前题库、重复保存五类场景。目标是确认错误停在 Service 或 Repository页面拿到的是可展示的结果。用例顺序 1. 清应用数据确认默认题库写入 pref_deck 2. 新建题库后重启确认 deck_index 和 deck:id 都存在 3. 手工破坏 deck:id JSON列表不崩溃日志出现 parse json failed 4. 删除当前题库currentDeckId 回退到可用题库 5. 保存后返回首页抽取页使用最新题库不要只看页面 Toast。真正要看的是写入路径、持久化结果、刷新信号和页面重读是否在同一个闭环里。验证清单清应用数据后从冷启动进入确认默认数据、页面状态和日志分支符合预期。对本文涉及的写路径准备正常、空值、重复、越界四类输入确认错误停在 Service 或 Repository。页面返回、重新进入、切换题库、收藏、历史或删除后确认对应刷新信号触发重新读取。修改资源或模块归属后重新构建确认 HAP、HAR、HSP 的依赖方向没有反转。涉及真机体验、备份恢复、发布素材的内容单独记录是否已经在设备或平台侧验证。常见问题与处理现象先看哪里处理方式保存后重启数据没了PreferencesStore.flush或封装写入是否 await统一通过 Store 写入页面不直接写 Preferences列表显示旧题库名LastDeckUpdateAt是否变化保存成功后同步刷新信号删除后抽取页报空currentDeckId是否仍指向被删 id删除当前题库后回退到默认或第一条可用题库偶发 JSON 解析错误日志里的 store/key保留 fallback同时补迁移或清理脏数据小结离线应用可以没有后端但不能没有数据边界。把 Preferences 收进 Store把 key 交给 Repository把规则交给 Service页面才不会在功能增加后变成隐形的数据层。
延伸阅读

更多相关文章

2026/9/12 2:59:48

如何构建稳定可靠的Unity插件框架:BepInEx 6.0架构深度解析

如何构建稳定可靠的Unity插件框架:BepInEx 6.0架构深度解析 【免费下载链接】BepInEx Unity / XNA game patcher and plugin framework 项目地址: https://gitcode.com/GitHub_Trending/be/BepInEx 在Unity游戏模组开发领域,BepInEx作为一款功能强…

2026/9/6 22:34:32

答案之书-01-启动白屏先别改页面:把题库播种放进 WindowStage 之前

答案之书 序列1:启动白屏先别改页面:把题库播种放进 WindowStage 之前 这类问题在轻量工具类应用里很常见:页面看起来只有几个按钮,但一旦把离线题库、动画、收藏、导入和发布状态串起来,真正容易出错的不是某个组件&a…

2026/9/10 4:58:34

宝马发动机烧机油问题分级维修方案详解

1. 项目概述:宝马烧机油问题的行业现状 在高端汽车维修领域,宝马发动机烧机油问题一直是困扰车主和维修技师的"老大难"。特别是2010-2018年间生产的N20、N55等系列发动机,普遍存在每1000公里消耗0.5-1升机油的情况。这个问题在温州…

2026/9/13 8:12:25

桥式起重机防摇输入整形技术实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 8:12:25

5G随身WiFi与CPE避坑指南:从频段到资费的全面解读

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 8:12:25

Python安装与环境变量配置完全指南:从入门到跑通第一行代码

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/13 8:12:25

个人开发者Agent应用接入实战:从注册到上线全流程解析

最近好几个做独立开发的朋友问我同一个问题:个人开发者到底怎么上车 Agent 这个方向?市面上框架一堆,文档满天飞,可真要动手把脑子里的点子变成一个能跑、能用、能给别人用的 Agent 应用,绕来绕去总卡在“平台接入”这…

2026/9/13 0:01:16

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/13 0:01:16

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/12 6:29:36

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/12 14:32:17

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/12 6:37:43

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码