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

一、三层架构概览
在本项目中,数据持久化被设计为清晰的三层架构:
基础层:PreferenceUtil → 封装 @kit.ArkData preferences 的 CRUD
常量层:PreferConstant → 统一定义存储 Key
业务层:NewWordManager / LearningPlanManager / StatisticsManager / DashboardManager → 封装业务语义
这种分层带来了几个关键好处:
- 关注点分离:Manager 层无需关心数据怎么存,只关心数据的业务含义
- 复用性:所有 Manager 共享同一个 PreferenceUtil 实例化逻辑
- 可维护性:Key 集中管理,避免字符串散落在各处
二、PreferenceUtil 基础层
2.1 单实例与多文件管理
export class PreferenceUtil {
private static preferenceRecord: Map<string, PreferenceUtil> = new Map()
private dataPreferences: preferences.Preferences | null = null
private constructor(context: Context, fileName: string) {
try {
preferences.removePreferencesFromCacheSync(context, fileName)
this.dataPreferences = preferences.getPreferencesSync(context, { name: fileName })
} catch (e) {
Logger.error(`PreferenceUtil error : ${JSON.stringify(e)}`)
}
}
public static getInstance(fileName: string = 'default') {
if (PreferenceUtil.preferenceRecord.has(fileName)) {
return PreferenceUtil.preferenceRecord.get(fileName)!!
}
let preferenceUtil: PreferenceUtil = new PreferenceUtil(
GlobalContextUtils.globalContext, fileName
);
PreferenceUtil.preferenceRecord.set(fileName, preferenceUtil);
return preferenceUtil
}
// ...
}
多文件策略:getInstance(fileName) 允许以不同的文件名创建独立的 Preferences 实例。默认使用 'default' 作为文件名,对于像笔记这种需要隔离的数据(PreferConstant.TOPIC_NOTES),可以创建独立的文件实例。
缓存机制:preferenceRecord 是一个 Map,相同的 fileName 不会重复创建实例,避免了多次打开同一个 Preferences 文件的开销。
2.2 CRUD 方法
PreferenceUtil 提供了完整的增删改查接口:
// 写入
public put(key: string, value: preferences.ValueType) {
this.dataPreferences?.putSync(key, value);
this.dataPreferences?.flush(); // 立即写入磁盘
}
// 读取
public get(key: string, defaultValue?: preferences.ValueType) {
return this.dataPreferences?.getSync(key, defaultValue);
}
// 检查存在
public hasSync(key: string): boolean {
return this.dataPreferences?.hasSync(key);
}
// 删除
public delete(key: string) {
this.dataPreferences?.deleteSync(key);
this.dataPreferences?.flush();
}
// 清空
public clear() {
this.dataPreferences.clearSync();
this.dataPreferences?.flush();
}
关键设计点:
- 同步 API:使用
getSync/putSync等同步方法,简化调用链;同步操作在数据量小时性能足够 - flush 策略:每次 put/delete 后立即 flush,确保数据不丢失
- ValueType 泛化:
preferences.ValueType支持 string、number、boolean、Array、Object 等类型,足够覆盖所有业务场景
三、PreferConstant 常量层
所有存储 Key 集中在 PreferConstant 类中:
export class PreferConstant {
static readonly TOPIC_NOTES: string = 'TOPIC_NOTES';
static readonly ERROR_RECORDS: string = 'ERROR_RECORDS';
static readonly EXAM_PREFER_COLLECT: string = 'EXAM_PREFER_COLLECT';
static readonly FEEDBACK_RECORD: string = 'FEEDBACK_RECORD';
static readonly FIRST_LAUNCH: string = 'FirstLaunch';
static readonly COLOR_MODE: string = 'Color_Mode';
static readonly DAILY_CHALLENGE_DATA: string = 'DAILY_CHALLENGE_DATA';
}
这样做的好处:
- 避免魔法字符串:所有 Key 有明确的语义名称
- 集中管理:修改 Key 只需要改一个地方
- 可发现性:新开发者可以通过 PreferConstant 快速了解应用存储了哪些数据
四、业务 Manager 层
4.1 NewWordManager
NewWordManager 使用 Preferences 存储生词列表:
export class NewWordManager {
private NEW_WORD_KEY = 'NEW_WORDS';
public getAllNewWords(): NewWordItem[] {
try {
let words = PreferenceUtil.getInstance().get(this.NEW_WORD_KEY, []) as NewWordItem[];
return words || [];
} catch (e) {
Logger.error('NewWordManager', `获取生词列表失败: ${JSON.stringify(e)}`);
return [];
}
}
public addNewWord(topicItem: TopicItemType, wordPackage: string = '默认词汇包'): boolean {
let words: NewWordItem[] = this.getAllNewWords();
let existIndex = words.findIndex(item => item.keyID === topicItem.keyID);
if (existIndex >= 0) {
words[existIndex].addTime = Date.now();
this.saveNewWords(words);
return false;
}
let newWord: NewWordItem = {
keyID: topicItem.keyID,
title: topicItem.title,
// ... 其他字段
addTime: Date.now(),
masteryLevel: 0,
wordPackage: wordPackage
};
words.push(newWord);
this.saveNewWords(words);
return true;
}
private saveNewWords(words: NewWordItem[]): void {
PreferenceUtil.getInstance().put(this.NEW_WORD_KEY, words);
}
}
NewWordManager 的职责是"生词本"的业务语义——去重、掌握程度更新、按词汇包筛选。它把整个生词列表作为一个数组存储在 Preferences 中。
4.2 StatisticsManager
StatisticsManager 存储聚合的统计数据:
public calculateStatistics(ques: TopicItemType[], duration: number): PracticeStatistics {
// 计算各个指标
// ...
this.saveStatistics(statistics);
return statistics;
}
private saveStatistics(statistics: PracticeStatistics): void {
let historyStats = this.getHistoryStatistics();
let mergedStats = this.mergeStatistics(historyStats, statistics);
PreferenceUtil.getInstance().put(this.STATISTICS_KEY, mergedStats);
}
与 NewWordManager 不同的是,StatisticsManager 保存的是经过聚合计算的统计对象,而不是原始数据列表。它体现了"写时计算"的思路:在每次完成练习后即时计算并累加统计数据。
4.3 LearningPlanManager
LearningPlanManager 存储的是结构化的学习计划对象:
public getLearningPlan(): LearningPlan {
try {
let plan = PreferenceUtil.getInstance().get(this.LEARNING_PLAN_KEY, undefined) as LearningPlan;
if (!plan) {
plan = this.cloneLearningPlan(DEFAULT_LEARNING_PLAN);
this.saveLearningPlan(plan);
}
return plan;
} catch (e) {
Logger.error('LearningPlanManager', `获取学习计划失败: ${JSON.stringify(e)}`);
return this.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-catch,Manager 层不再需要关注底层存储异常
- 线程安全:Preferences 是线程安全的,多 Manager 并发写入不会冲突
从 PreferenceUtil 到业务 Manager 的工具链设计,体现了"高内聚低耦合"的经典原则。每一层各司其职,下层不知道上层的业务含义,上层不关心下层的实现细节,是值得借鉴的分层架构范式。
更多推荐


所有评论(0)