HarmonyOS 应用开发《掌上英语》第42篇:首选项工具链——从 PreferenceUtil 到业务 Manager

📅 2026/7/24 23:58:52
HarmonyOS 应用开发《掌上英语》第42篇:首选项工具链——从 PreferenceUtil 到业务 Manager
首选项工具链——从 PreferenceUtil 到业务 Manager一、三层架构概览在本项目中数据持久化被设计为清晰的三层架构基础层PreferenceUtil → 封装 kit.ArkData preferences 的 CRUD 常量层PreferConstant → 统一定义存储 Key 业务层NewWordManager / LearningPlanManager / StatisticsManager / DashboardManager → 封装业务语义这种分层带来了几个关键好处关注点分离Manager 层无需关心数据怎么存只关心数据的业务含义复用性所有 Manager 共享同一个 PreferenceUtil 实例化逻辑可维护性Key 集中管理避免字符串散落在各处二、PreferenceUtil 基础层2.1 单实例与多文件管理exportclassPreferenceUtil{privatestaticpreferenceRecord:Mapstring,PreferenceUtilnewMap()privatedataPreferences:preferences.Preferences|nullnullprivateconstructor(context:Context,fileName:string){try{preferences.removePreferencesFromCacheSync(context,fileName)this.dataPreferencespreferences.getPreferencesSync(context,{name:fileName})}catch(e){Logger.error(PreferenceUtil error :${JSON.stringify(e)})}}publicstaticgetInstance(fileName:stringdefault){if(PreferenceUtil.preferenceRecord.has(fileName)){returnPreferenceUtil.preferenceRecord.get(fileName)!!}letpreferenceUtil:PreferenceUtilnewPreferenceUtil(GlobalContextUtils.globalContext,fileName);PreferenceUtil.preferenceRecord.set(fileName,preferenceUtil);returnpreferenceUtil}// ...}多文件策略getInstance(fileName)允许以不同的文件名创建独立的 Preferences 实例。默认使用default作为文件名对于像笔记这种需要隔离的数据PreferConstant.TOPIC_NOTES可以创建独立的文件实例。缓存机制preferenceRecord是一个 Map相同的 fileName 不会重复创建实例避免了多次打开同一个 Preferences 文件的开销。2.2 CRUD 方法PreferenceUtil 提供了完整的增删改查接口// 写入publicput(key:string,value:preferences.ValueType){this.dataPreferences?.putSync(key,value);this.dataPreferences?.flush();// 立即写入磁盘}// 读取publicget(key:string,defaultValue?:preferences.ValueType){returnthis.dataPreferences?.getSync(key,defaultValue);}// 检查存在publichasSync(key:string):boolean{returnthis.dataPreferences?.hasSync(key);}// 删除publicdelete(key:string){this.dataPreferences?.deleteSync(key);this.dataPreferences?.flush();}// 清空publicclear(){this.dataPreferences.clearSync();this.dataPreferences?.flush();}关键设计点同步 API使用getSync/putSync等同步方法简化调用链同步操作在数据量小时性能足够flush 策略每次 put/delete 后立即 flush确保数据不丢失ValueType 泛化preferences.ValueType支持 string、number、boolean、Array、Object 等类型足够覆盖所有业务场景三、PreferConstant 常量层所有存储 Key 集中在PreferConstant类中exportclassPreferConstant{staticreadonlyTOPIC_NOTES:stringTOPIC_NOTES;staticreadonlyERROR_RECORDS:stringERROR_RECORDS;staticreadonlyEXAM_PREFER_COLLECT:stringEXAM_PREFER_COLLECT;staticreadonlyFEEDBACK_RECORD:stringFEEDBACK_RECORD;staticreadonlyFIRST_LAUNCH:stringFirstLaunch;staticreadonlyCOLOR_MODE:stringColor_Mode;staticreadonlyDAILY_CHALLENGE_DATA:stringDAILY_CHALLENGE_DATA;}这样做的好处避免魔法字符串所有 Key 有明确的语义名称集中管理修改 Key 只需要改一个地方可发现性新开发者可以通过 PreferConstant 快速了解应用存储了哪些数据四、业务 Manager 层4.1 NewWordManagerNewWordManager 使用 Preferences 存储生词列表exportclassNewWordManager{privateNEW_WORD_KEYNEW_WORDS;publicgetAllNewWords():NewWordItem[]{try{letwordsPreferenceUtil.getInstance().get(this.NEW_WORD_KEY,[])asNewWordItem[];returnwords||[];}catch(e){Logger.error(NewWordManager,获取生词列表失败:${JSON.stringify(e)});return[];}}publicaddNewWord(topicItem:TopicItemType,wordPackage:string默认词汇包):boolean{letwords:NewWordItem[]this.getAllNewWords();letexistIndexwords.findIndex(itemitem.keyIDtopicItem.keyID);if(existIndex0){words[existIndex].addTimeDate.now();this.saveNewWords(words);returnfalse;}letnewWord:NewWordItem{keyID:topicItem.keyID,title:topicItem.title,// ... 其他字段addTime:Date.now(),masteryLevel:0,wordPackage:wordPackage};words.push(newWord);this.saveNewWords(words);returntrue;}privatesaveNewWords(words:NewWordItem[]):void{PreferenceUtil.getInstance().put(this.NEW_WORD_KEY,words);}}NewWordManager 的职责是生词本的业务语义——去重、掌握程度更新、按词汇包筛选。它把整个生词列表作为一个数组存储在 Preferences 中。4.2 StatisticsManagerStatisticsManager 存储聚合的统计数据publiccalculateStatistics(ques:TopicItemType[],duration:number):PracticeStatistics{// 计算各个指标// ...this.saveStatistics(statistics);returnstatistics;}privatesaveStatistics(statistics:PracticeStatistics):void{lethistoryStatsthis.getHistoryStatistics();letmergedStatsthis.mergeStatistics(historyStats,statistics);PreferenceUtil.getInstance().put(this.STATISTICS_KEY,mergedStats);}与 NewWordManager 不同的是StatisticsManager 保存的是经过聚合计算的统计对象而不是原始数据列表。它体现了写时计算的思路在每次完成练习后即时计算并累加统计数据。4.3 LearningPlanManagerLearningPlanManager 存储的是结构化的学习计划对象publicgetLearningPlan():LearningPlan{try{letplanPreferenceUtil.getInstance().get(this.LEARNING_PLAN_KEY,undefined)asLearningPlan;if(!plan){planthis.cloneLearningPlan(DEFAULT_LEARNING_PLAN);this.saveLearningPlan(plan);}returnplan;}catch(e){Logger.error(LearningPlanManager,获取学习计划失败:${JSON.stringify(e)});returnthis.cloneLearningPlan(DEFAULT_LEARNING_PLAN);}}注意它的懒初始化 默认值模式如果 Preferences 中还没有数据首次使用就使用DEFAULT_LEARNING_PLAN创建一份默认计划。4.4 各层调用关系页面组件 (CourseHomePage) ↓ 业务语义层 (NewWordManager.getInstance().addNewWord(...)) ↓ 基础存储层 (PreferenceUtil.getInstance().put(NEW_WORDS, data)) ↓ 系统 API (preferences.getPreferencesSync / putSync / flush)五、架构优势总结替换成本低如果将来需要从 Preferences 迁移到数据库只需要改 PreferenceUtil 的实现Manager 层无需改动测试友好可以用 Mock 的 Preferences 替换真实实例进行单元测试统一错误处理PreferenceUtil 中统一 try-catchManager 层不再需要关注底层存储异常线程安全Preferences 是线程安全的多 Manager 并发写入不会冲突从 PreferenceUtil 到业务 Manager 的工具链设计体现了高内聚低耦合的经典原则。每一层各司其职下层不知道上层的业务含义上层不关心下层的实现细节是值得借鉴的分层架构范式。