首选项工具链——从 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)

五、架构优势总结

  1. 替换成本低:如果将来需要从 Preferences 迁移到数据库,只需要改 PreferenceUtil 的实现,Manager 层无需改动
  2. 测试友好:可以用 Mock 的 Preferences 替换真实实例进行单元测试
  3. 统一错误处理:PreferenceUtil 中统一 try-catch,Manager 层不再需要关注底层存储异常
  4. 线程安全:Preferences 是线程安全的,多 Manager 并发写入不会冲突

从 PreferenceUtil 到业务 Manager 的工具链设计,体现了"高内聚低耦合"的经典原则。每一层各司其职,下层不知道上层的业务含义,上层不关心下层的实现细节,是值得借鉴的分层架构范式。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐