鸿蒙应用开发UI基础第二十四节:构造Preferences用户首选项数据存储开源工具

【学习目标】

  1. 明确PreferencesUtil单利模式的局限,掌握「多实例池」的设计思路与实现逻辑;
  2. 完成 Preferences 工具类从「单文件单例」到「多文件实例池」的核心升级;
  3. 掌握鸿蒙静态库(HAR)的创建、配置、导出、打包全流程;
  4. 将升级后的「多实例池工具类」解耦封装为独立 HAR 库,实现跨项目复用;
  5. 掌握库工程规范、版本管理、接口设计、测试验证的企业级实践。

一、核心背景:上一节单例模式的局限

上一节 PreferencesUtil单例模式,仅能操作一个固定的 Preferences 文件,在复杂业务场景下存在明显局限:

局限点 具体问题
单文件存储 所有业务配置(用户信息/应用配置/缓存数据)挤在一个文件,易出现键名冲突
无法隔离业务数据 不同模块/业务的配置无法隔离,删除/修改数据时易误操作其他业务的配置
复用性差 工具类与业务工程耦合,无法直接迁移到其他项目
扩展能力弱 新增存储文件需修改工具类源码,不符合「开闭原则」

本节核心解决思路:

  1. 升级架构:从「单例」升级为「多实例池」,每个存储文件对应一个独立实例;
  2. 解耦封装:剥离业务依赖,封装为独立 HAR 库;
  3. 简化使用:保留「静态快捷方法+实例方法」双调用模式,兼顾便捷性与灵活性。

二、工程结构(API18+ 核心目录)

复制代码
PreferenceLibDemo/                  # 工程根目录
├── AppScope/                       # 应用全局配置目录
├── entry/                          # 主应用模块(用于验证HAR库功能)
├── preferences/                     # HAR库模块(核心封装模块)
│   ├── src/
│   │   ├── main/
│   │   │   ├── ets/
│   │   │   │   ├── components/    # HAR库组件目录(本节暂未使用)
│   │   │   │   ├── utils/         # 工具类核心目录
│   │   │   │   │   ├── PreferencesConfig.ets  # 类型/枚举/接口/常量定义
│   │   │   │   │   └── PreferencesUtil.ets    # 多实例池核心工具类
│   │   │   │   ├── resources/     # HAR库资源目录
│   │   │   │   └── module.json5   # 模块基础配置
│   │   ├── ohosTest/              # 鸿蒙测试目录
│   │   └── test/                  # 单元测试目录
│   ├── build-profile.json5        # HAR库构建版本配置
│   ├── consumer-rules.txt         # 编译依赖该模块的应用时生效 作用于依赖方的混淆流程 
│   ├── hvigorfile.ts              # HAR库构建脚本
│   ├── Index.ets                  # HAR库对外统一导出入口
│   ├── obfuscation-rules.txt      # 编译当前模块时生效 | 仅作用于自身代码 
│   └── oh-package.json5           # 重要内容:HAR库核心配置(元信息/依赖/兼容版本)
├── build-profile.json5            # 工程级构建配置
├── hvigorfile.ts                  # 工程级构建脚本
├── oh-package.json5               # 工程根级依赖配置
└── oh-package-lock.json5          # 工程依赖版本锁定文件

2.1 静态库创建

在 DevEco Studio 中创建 HAR 库模块步骤:

  1. 右键工程根目录 → NewModule
  2. 选择 Static Library(静态库)→ 填写模块名称 preferences 完成创建;
  3. 手动在 preferences/src/main/ets/ 下创建 utils 目录,用于存放核心工具类。

三、用户首选项配置文件

3.1 类型/枚举/常量定义

preferences/src/main/ets/utils/PreferencesConfig.ets

javascript 复制代码
/**
 * 通用操作结果回调
 * @param err 错误信息:null=成功,非null=失败
 */
export type PrefCallback = (err: Error | null) => void;

/**
 * 数据变更回调
 * @param key 发生变更的存储键
 */
export type ChangeCallback = (key: string) => void;

/**
 * 存储模式枚举
 */
export enum StorageMode {
  /** XML模式:默认,全平台兼容,需手动刷盘,单进程安全 */
  XML = 0,
  /** GSKV模式:API 18+支持,自动刷盘,多进程安全 */
  GSKV = 1
}

/**
 * 初始化配置接口
 */
export interface PreferencesOptions {
  /** 存储文件名(必填,空字符串会兜底为default_prefs) */
  fileName: string;
  /** 存储模式(默认XML) */
  storageMode: StorageMode;
}

/**
 * 默认配置
 */
export const DEFAULT_OPTIONS: PreferencesOptions = {
  fileName: "default_prefs",
  storageMode: StorageMode.XML
};

3.2 用户首选项核心工具类

路径:preferences/src/main/ets/utils/PreferencesUtil.ets

javascript 复制代码
// PreferencesUtil 多实例池核心代码


import { preferences, ValueType } from '@kit.ArkData';
import type {
  PreferencesOptions,
  ChangeCallback,
  PrefCallback
} from './PreferencesConfig';

import { DEFAULT_OPTIONS, StorageMode } from './PreferencesConfig';

/**
 * Preferences 存储工具类(多实例池模式)
 * 核心特性:
 * 1. 多文件隔离 - 每个存储文件对应独立实例,避免配置冲突
 * 2. 自动刷盘 - XML模式下写入/删除后自动刷盘,保证数据持久化
 * 3. 兼容性强 - GSKV模式不支持时自动降级为XML模式
 * 4. 资源管理 - 支持监听清理、实例销毁,避免内存泄漏
 * 5. 便捷切换 - use()返回实例,支持静态/实例双调用模式
 */
export class PreferencesUtil {
  /** 实例缓存池:key=存储文件名,value=对应实例(实现多文件隔离) */
  private static pool = new Map<string, PreferencesUtil>();
  /** 当前选中的默认实例(用于简化读写操作) */
  private static currentInstance: PreferencesUtil | null = null;

  /** 应用上下文(必传,用于创建Preferences实例) */
  private appContext: Context;
  /** 当前实例对应的存储文件名 */
  private fileName: string;
  /** 当前实例对应的存储模式(XML/GSKV) */
  private storageMode: StorageMode;
  /** 鸿蒙原生Preferences实例(缓存,避免重复创建) */
  private prefs: preferences.Preferences | null = null;
  /** 数据变更监听回调缓存:key=监听ID,value=回调函数 */
  private listeners = new Map<string, ChangeCallback>();

  /**
   * 私有构造函数(禁止外部直接实例化)
   * @param context 应用上下文
   * @param options 初始化配置
   */
  private constructor(context: Context, options: PreferencesOptions) {
    // 上下文非空校验
    if (!context) {
      throw new Error('[PreferencesUtil] 上下文不能为空');
    }
    this.appContext = context;
    this.fileName = options.fileName.trim() || DEFAULT_OPTIONS.fileName;
    this.storageMode = options.storageMode ?? DEFAULT_OPTIONS.storageMode;
    this.checkGskvSupport();
  }

  /**
   * 获取指定存储文件的实例(核心入口方法)
   * @param context 应用上下文(必传,不能为空)
   * @param options 初始化配置
   * @param options.fileName 存储文件名(必填,空字符串会兜底为default_prefs)
   * @param options.storageMode 存储模式(可选,默认XML)
   * @returns 对应文件的PreferencesUtil实例(单例,重复调用返回同一实例)
   */
  public static getInstance(
    context: Context,
    options: PreferencesOptions
  ): PreferencesUtil {
    // 入参校验
    if (!context) {
      throw new Error('[PreferencesUtil] getInstance: 上下文不能为空');
    }
    if (!options?.fileName) {
      throw new Error('[PreferencesUtil] getInstance: fileName 不能为空');
    }

    const key = options.fileName.trim();
    if (!PreferencesUtil.pool.has(key)) {
      PreferencesUtil.pool.set(key, new PreferencesUtil(context, options));
    }
    const instance = PreferencesUtil.pool.get(key)!;
    // 首次初始化时,默认选中该实例
    if (!PreferencesUtil.currentInstance) {
      PreferencesUtil.currentInstance = instance;
    }
    return instance;
  }

  /**
   * 通过文件名获取已初始化的实例(内部快捷方法)
   * @param fileName 存储文件名(必填,需与初始化时的文件名一致)
   * @returns 已初始化的实例(未初始化返回undefined)
   */
  private static getInstanceByName(fileName: string): PreferencesUtil | undefined {
    // 校验文件名非空
    if (!fileName) {
      console.error('[PreferencesUtil] getInstanceByName: fileName 不能为空');
      return undefined;
    }
    // 去除首尾空格,保证与初始化时的key一致
    const key = fileName.trim();
    // 从实例池获取对应实例
    const instance = PreferencesUtil.pool.get(key);
    // 未找到实例时给出友好提示
    if (!instance) {
      console.warn(`[PreferencesUtil] getInstanceByName: 文件${key}的实例未初始化,请先调用getInstance初始化`);
    }
    return instance;
  }

  /**
   * 切换默认操作的文件(核心切换方法)
   * @param fileName 要切换到的存储文件名
   * @returns 切换后的实例(null=文件未初始化)
   */
  public static use(fileName: string): PreferencesUtil | null {
    const instance = PreferencesUtil.getInstanceByName(fileName);
    if (instance) {
      PreferencesUtil.currentInstance = instance;
      console.log(`[PreferencesUtil] 已切换到文件:${fileName}`);
      return instance; // 优化:直接返回找到的实例,逻辑更简洁
    }
    PreferencesUtil.currentInstance = null;
    return null;
  }

  /**
   * 获取当前选中的默认实例(补充:方便外部直接获取)
   * @returns 当前默认实例(null=未选中)
   */
  public static getCurrentInstance(): PreferencesUtil | null {
    return PreferencesUtil.currentInstance;
  }

  /**
   * 获取当前选中的默认文件名
   * @returns 当前默认文件名(未选中返回空字符串)
   */
  public static getCurrentFileName(): string {
    return PreferencesUtil.currentInstance?.fileName || '';
  }

  /**
   * 私有方法:转换自定义存储模式为鸿蒙原生枚举类型
   * @param mode 自定义存储模式(XML/GSKV)
   * @returns 鸿蒙原生StorageType枚举值
   */
  private convertType(mode: StorageMode): preferences.StorageType {
    return mode === StorageMode.GSKV
      ? preferences.StorageType.GSKV
      : preferences.StorageType.XML;
  }

  /**
   * 私有方法:检查GSKV模式是否支持,不支持则自动降级为XML模式
   */
  private checkGskvSupport() {
    const type = this.convertType(this.storageMode);
    if (
      type === preferences.StorageType.GSKV &&
        !preferences.isStorageTypeSupported(preferences.StorageType.GSKV)
    ) {
      this.storageMode = StorageMode.XML;
      console.warn(`[PreferencesUtil] GSKV not supported, use XML for ${this.fileName}`);
    }
  }

  /**
   * 私有方法:获取鸿蒙原生Preferences实例(带缓存)
   * @returns 原生Preferences实例(失败返回null)
   */
  private getPrefs(): preferences.Preferences | null {
    if (this.prefs) return this.prefs;

    try {
      this.prefs = preferences.getPreferencesSync(this.appContext, {
        name: this.fileName,
        storageType: this.convertType(this.storageMode),
      });
      return this.prefs;
    } catch (err) {
      console.error(`[PreferencesUtil] init failed: ${(err as Error).message}`);
      return null;
    }
  }

  /**
   * 私有方法:刷盘操作(仅XML模式有效)
   * @param callback 刷盘完成回调(err为null表示成功)
   */
  private flush(callback?: PrefCallback) {
    const prefs = this.getPrefs();
    if (!prefs) {
      callback?.(new Error("prefs not ready"));
      return;
    }

    if (this.storageMode === StorageMode.GSKV) {
      callback?.(null);
      return;
    }

    prefs.flush((err) => {
      callback?.(err ? new Error(err.message) : null);
    });
  }

  // ==================== 实例方法(操作当前文件) ====================
  /**
   * 写入数据(同步)
   * @param key 存储键(非空)
   * @param value 存储值(支持string/number/boolean/object/array等ValueType类型)
   * @param callback 操作完成回调(仅XML模式返回刷盘结果,GSKV模式直接返回成功)
   * @returns true=写入成功,false=写入失败(实例未初始化/键为空/写入异常)
   */
  public put(key: string, value: ValueType, callback?: PrefCallback): boolean {
    // 入参校验
    if (!key) {
      callback?.(new Error("key 不能为空"));
      return false;
    }

    const prefs = this.getPrefs();
    if (!prefs) {
      callback?.(new Error("prefs not ready"));
      return false;
    }

    try {
      prefs.putSync(key, value);
      this.flush(callback);
      return true;
    } catch (err) {
      callback?.(err as Error);
      return false;
    }
  }

  /**
   * 读取数据(同步)
   * @param key 存储键(非空)
   * @param defValue 默认值(读取失败/键不存在时返回)
   * @returns 存储值(成功)/默认值(失败)
   */
  public get<T extends ValueType>(key: string, defValue: T): T {
    // 入参校验
    if (!key) {
      return defValue;
    }

    const prefs = this.getPrefs();
    if (!prefs) return defValue;

    try {
      return prefs.getSync(key, defValue) as T;
    } catch (err) {
      return defValue;
    }
  }

  /**
   * 检查指定键是否存在(同步)
   * @param key 存储键(非空)
   * @returns true=存在,false=不存在/实例未初始化/检查异常
   */
  public has(key: string): boolean {
    // 入参校验
    if (!key) return false;

    const prefs = this.getPrefs();
    if (!prefs) return false;

    try {
      return prefs.hasSync(key);
    } catch (error) {
      console.error(`[PreferencesUtil] check key [${key}] failed: ${(error as Error).message}`);
      return false;
    }
  }

  /**
   * 删除指定键的数据(同步)
   * @param key 存储键(非空)
   * @param callback 操作完成回调(仅XML模式返回刷盘结果)
   * @returns true=删除成功,false=删除失败(实例未初始化/键为空/删除异常)
   */
  public delete(key: string, callback?: PrefCallback): boolean {
    // 入参校验
    if (!key) {
      callback?.(new Error("key 不能为空"));
      return false;
    }

    const prefs = this.getPrefs();
    if (!prefs) {
      callback?.(new Error("prefs not ready"));
      return false;
    }

    try {
      prefs.deleteSync(key);
      this.flush(callback);
      return true;
    } catch (err) {
      callback?.(err as Error);
      return false;
    }
  }

  /**
   * 订阅数据变更事件
   * @param callBack 数据变更回调(参数为变更的存储键)
   * @returns 监听ID(用于取消监听,空字符串表示订阅失败)
   */
  public onDataChange(callBack: ChangeCallback): string {
    // 回调非空校验
    if (!callBack) {
      console.error('[PreferencesUtil] onDataChange: 回调函数不能为空');
      return "";
    }

    const prefs = this.getPrefs();
    if (!prefs) return "";

    const id = Date.now().toString();
    this.listeners.set(id, callBack);
    try {
      // 直接绑定原始回调,避免嵌套函数导致解绑失败
      prefs.on("change", callBack);
    } catch (error) {
      console.error(`[PreferencesUtil] subscribe change failed: ${(error as Error).message}`);
      this.listeners.delete(id);
      return "";
    }
    return id;
  }

  /**
   * 取消指定的数据变更监听
   * @param id 监听ID(onDataChange返回的值)
   */
  public offDataChange(id: string): void {
    // 入参校验
    if (!id) return;

    const prefs = this.getPrefs();
    if (!prefs || !this.listeners.has(id)) return;

    const callBack = this.listeners.get(id)!;
    try {
      // 解绑原始回调函数
      prefs.off("change", callBack);
    } catch (error) {
      console.error(`[PreferencesUtil] unsubscribe change failed: ${(error as Error).message}`);
    }
    this.listeners.delete(id);
  }

  /**
   * 取消当前文件的所有数据变更监听(避免内存泄漏)
   */
  public removeAllListeners(): void {
    const prefs = this.getPrefs();
    if (!prefs) return;

    this.listeners.forEach((callBack) => {
      try {
        prefs.off("change", callBack);
      } catch (error) {
        console.error(`[PreferencesUtil] unsubscribe all listeners failed: ${(error as Error).message}`);
      }
    });
    this.listeners.clear();
  }

  /**
   * 删除指定的存储文件
   * @param callback 操作完成回调(err为null表示成功)
   * @param fileName 要删除的文件名(可选,不传则删除当前实例对应的文件)
   * @returns true=触发删除操作,false=上下文未初始化(失败)
   */
  public deleteFile(callback?: PrefCallback, fileName?: string): boolean {
    // 1. 校验上下文
    if (!this.appContext) {
      callback?.(new Error('上下文未初始化,无法删除文件'));
      return false;
    }

    // 2. 确定目标文件
    const target = fileName?.trim() || this.fileName;
    const type = this.convertType(this.storageMode);

    // 3. 执行删除
    preferences.deletePreferences(this.appContext, {
      name: target,
      storageType: type,
    }, (err) => {
      if (err) {
        // 错误回调:直接传 Error 对象
        callback?.(new Error(err.message));
      } else {
        // 成功回调:
        // - 清空当前实例的 prefs 缓存(如果删除的是当前文件)
        // - 从实例池移除已删除的文件实例
        // - 回调传 null 表示成功
        if (target === this.fileName) {
          this.prefs = null;
          // 如果删除的是当前默认文件,清空默认实例
          if (PreferencesUtil.currentInstance?.fileName === target) {
            PreferencesUtil.currentInstance = null;
          }
        }
        PreferencesUtil.pool.delete(target);
        callback?.(null);
      }
    });

    // 4. 返回 true 表示触发删除操作
    return true;
  }

  /**
   * 销毁当前实例(释放所有资源)
   * 1. 取消所有监听
   * 2. 清空Preferences缓存
   * 3. 从实例池移除当前实例
   */
  public destroy(): void {
    this.removeAllListeners();
    this.prefs = null;
    // 如果销毁的是当前默认文件,清空默认实例
    if (PreferencesUtil.currentInstance?.fileName === this.fileName) {
      PreferencesUtil.currentInstance = null;
    }
    PreferencesUtil.pool.delete(this.fileName);
  }

  // ==================== 静态快捷方法(操作默认文件) ====================
  /**
   * 快捷写入(写入当前默认文件)
   * @param key 存储键
   * @param value 存储值
   * @param callback 操作完成回调
   * @returns true=写入成功,false=失败(未选中默认文件/其他错误)
   */
  public static put(key: string, value: ValueType, callback?: PrefCallback): boolean {
    if (!PreferencesUtil.currentInstance) {
      console.error('[PreferencesUtil] 请先调用use()切换到具体文件');
      callback?.(new Error('未选中默认文件'));
      return false;
    }
    return PreferencesUtil.currentInstance.put(key, value, callback);
  }

  /**
   * 快捷读取(读取当前默认文件)
   * @param key 存储键
   * @param defValue 默认值
   * @returns 存储值/默认值(未选中默认文件返回默认值)
   */
  public static get<T extends ValueType>(key: string, defValue: T): T {
    if (!PreferencesUtil.currentInstance) {
      console.error('[PreferencesUtil] 请先调用use()切换到具体文件');
      return defValue;
    }
    return PreferencesUtil.currentInstance.get(key, defValue);
  }

  /**
   * 快捷删除(删除当前默认文件的指定键)
   * @param key 存储键
   * @param callback 操作完成回调
   * @returns true=删除成功,false=失败(未选中默认文件/其他错误)
   */
  public static delete(key: string, callback?: PrefCallback): boolean {
    if (!PreferencesUtil.currentInstance) {
      console.error('[PreferencesUtil] 请先调用use()切换到具体文件');
      callback?.(new Error('未选中默认文件'));
      return false;
    }
    return PreferencesUtil.currentInstance.delete(key, callback);
  }

  /**
   * 快捷检查键是否存在(检查当前默认文件)
   * @param key 存储键
   * @returns true=存在,false=不存在/未选中默认文件
   */
  public static has(key: string): boolean {
    if (!PreferencesUtil.currentInstance) {
      console.error('[PreferencesUtil] 请先调用use()切换到具体文件');
      return false;
    }
    return PreferencesUtil.currentInstance.has(key);
  }

  /**
   * 快捷订阅数据变更(订阅当前默认文件)
   * @param callBack 变更回调
   * @returns 监听ID(未选中默认文件返回空字符串)
   */
  public static onDataChange(callBack: ChangeCallback): string {
    if (!PreferencesUtil.currentInstance) {
      console.error('[PreferencesUtil] 请先调用use()切换到具体文件');
      return "";
    }
    return PreferencesUtil.currentInstance.onDataChange(callBack);
  }

  /**
   * 快捷取消监听(取消当前默认文件的指定监听)
   * @param id 监听ID
   */
  public static offDataChange(id: string): void {
    if (!PreferencesUtil.currentInstance) {
      console.error('[PreferencesUtil] 请先调用use()切换到具体文件');
      return;
    }
    PreferencesUtil.currentInstance.offDataChange(id);
  }

  /**
   * 快捷取消所有监听(补充:覆盖实例方法removeAllListeners)
   */
  public static removeAllListeners(): void {
    if (!PreferencesUtil.currentInstance) {
      console.error('[PreferencesUtil] 请先调用use()切换到具体文件');
      return;
    }
    PreferencesUtil.currentInstance.removeAllListeners();
  }

  /**
   * 快捷删除文件(补充:支持指定/当前文件删除)
   * @param callback 操作完成回调
   * @param fileName 要删除的文件名(可选,不传则删除当前默认文件)
   */
  public static deleteFile(callback?: PrefCallback, fileName?: string): boolean {
    // 兼容参数顺序:第一个参数为回调时,删除当前文件
    let targetFileName: string | undefined;
    let realCallback: PrefCallback | undefined;
    if (typeof fileName === 'function') {
      realCallback = fileName;
      targetFileName = PreferencesUtil.getCurrentFileName();
    } else {
      targetFileName = fileName;
      realCallback = callback;
    }

    const targetInstance = targetFileName
      ? PreferencesUtil.getInstanceByName(targetFileName)
      : PreferencesUtil.currentInstance;

    if (!targetInstance) {
      console.error('[PreferencesUtil] 删除文件失败:文件未初始化或未选中默认文件');
      realCallback?.(new Error('文件未初始化或未选中默认文件'));
      return false;
    }

    return targetInstance.deleteFile(realCallback, targetFileName);
  }

  /**
   * 快捷销毁实例(支持指定/当前实例销毁)
   * @param fileName 要销毁的文件名(可选,不传则销毁当前默认文件)
   */
  public static destroyFile(fileName?: string): boolean {
    const targetInstance = fileName
      ? PreferencesUtil.getInstanceByName(fileName)
      : PreferencesUtil.currentInstance;

    if (!targetInstance) {
      console.error('[PreferencesUtil] 销毁实例失败:文件未初始化或未选中默认文件');
      return false;
    }

    targetInstance.destroy();
    return true;
  }
}

3.3 库对外统一导出入口

路径:preferences/src/main/ets/Index.ets

javascript 复制代码
export  {PreferencesUtil} from './src/main/ets/utils/PreferencesUtil';
export { StorageMode, ChangeCallback, PreferencesOptions} from './src/main/ets/utils/PreferencesConfig';

3.4 HAR库核心配置

路径:preferences/oh-package.json5

json 复制代码
{
  "name": "@happy/preferences", // 包名规范:@作者组织/库名称,用于发布到三方仓库
  "version": "1.0.0",
  "description": "Preferences多实例池工具库,支持多文件隔离、静态+实例双调用、XML/GSKV自动降级",
  "main": "Index.ets",
  "deviceTypes": [
    "default",
    "phone",
    "tablet"],
  "compatibleSdkVersion": "5.1.0(18)",
  "author": "happy", // 作者
  "license": "Apache-2.0",
  "dependencies": {}
}

四、HAR库打包流程

4.1 执行打包操作

  1. 打开 DevEco Studio,选中工程根目录下的 preferences 模块;
  2. 点击顶部菜单栏:Build > Make Module 'preferences'
  3. 等待打包完成会多出一个build文件在preferences目录下;

4.2 打包产物路径

复制代码
build/default/outputs/default/preferences.har

本节内容只完成har的打包关于构建HAR,混淆等详细内容,以及发布到第三方仓库放在一下节。

五、测试工程集成HAR库(验证可用性)

5.1 引入HAR依赖

修改 /entry/oh-package.json5,添加HAR库依赖:

json 复制代码
{
  "name": "entry",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {
    "@happy/preferences": "file:../preferences" // 本地依赖库har

  }
}

点击右上角 Sync Now 同步依赖;

5.2 EntryAbility初始化PreferencesUtil

entry/src/main/ets/entryability/EntryAbility.ets

javascript 复制代码
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import PreferencesUtil from '@happy/preferences';
import  { StorageMode } from '@happy/preferences';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    try {
      this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);

      // 1. 创建两个独立文件实例(多文件隔离)
      PreferencesUtil.getInstance(this.context, {
        fileName: 'test1',
        storageMode: StorageMode.XML
      });

     PreferencesUtil.getInstance(this.context, {
        fileName: 'test2',
        storageMode: StorageMode.XML
      });

    } catch (err) {
      hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
    }
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy(): void {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
    // 应用销毁时清理资源
    PreferencesUtil.getCurrentInstance()?.removeAllListeners();
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
    });
  }
}

5.3 封装测试功能 PreferencesTest.ets

src/main/ets/PreferencesTest.ets

javascript 复制代码
import PreferencesUtil from "@happy/preferences";

/**
 * 完整功能测试
 */
export function runPreferencesUtilTest() {
  console.log('==================== PreferencesUtil 功能测试开始 ====================');
  // 2. 切换到 test1
  const inst1 = PreferencesUtil.use('test1');
  if (inst1) {
    console.log('✅ 切换到 test1 成功');
  } else {
    console.error('❌ 切换 test1 失败');
  }

  // 3. 静态 put / get
  PreferencesUtil.put('name', 'test-value');
  const val = PreferencesUtil.get('name', '');
  console.log('✅ get(name) =', val);

  // 4. has
  const hasName = PreferencesUtil.has('name');
  console.log('✅ has(name) =', hasName);

  // 5. 监听测试
  const listenerId = PreferencesUtil.onDataChange((key) => {
    console.log('📢 监听到变化:', key);
  });
  console.log('✅ 监听ID:', listenerId);

  // 6. 触发一次修改,看监听
  PreferencesUtil.put('age', 20);

  // 7. 取消单个监听
  if (listenerId) {
    PreferencesUtil.offDataChange(listenerId);
    console.log('✅ 取消单个监听成功');
  }

  // 8. 再次切换到 test2
  const inst2 = PreferencesUtil.use('test2');
  if (inst2) {
    console.log('✅ 切换到 test2 成功');
  }

  // 9. test2 写入不同值
  PreferencesUtil.put('mode', 'dark');
  const mode = PreferencesUtil.get('mode', '');
  console.log('✅ test2 mode =', mode);

  // 10. 验证多文件隔离:test1 的 name 还在
  PreferencesUtil.use('test1');
  const nameAgain = PreferencesUtil.get('name', '');
  console.log('✅ test1 name 隔离后 =', nameAgain);

  // 11. 删除 test1 中的 key
  PreferencesUtil.delete('name');
  const afterDelete = PreferencesUtil.has('name');
  console.log('✅ 删除 name 后 has =', afterDelete);

  // 12. 取消所有监听
  PreferencesUtil.removeAllListeners();
  console.log('✅ 取消所有监听成功');

  // 13. 删除文件 test2
  PreferencesUtil.use('test2');
  PreferencesUtil.deleteFile((err) => {
    if (err) {
      console.error('❌ 删除文件失败:', err);
    } else {
      console.log('✅ 删除 test2 文件成功');
    }
  });

  // 14. 销毁实例 test1
  PreferencesUtil.use('test1');
  PreferencesUtil.destroyFile();
  console.log('✅ 销毁 test1 实例成功');

  // 15. 异常测试:use 不存在的文件
  const bad = PreferencesUtil.use('not-exist');
  if (!bad) {
    console.log('✅ use 不存在文件返回 null,符合预期');
  }

  // 16. 异常测试:未 use 直接 put
  PreferencesUtil.use(''); // 清空 current
  const res = PreferencesUtil.put('any', 'value');
  console.log('✅ 未切换文件 put 返回:', res);

  console.log('==================== PreferencesUtil 功能测试完成 ====================');
}

5.4 主页面Index

javascript 复制代码
// 导入测试内容
import { runPreferencesUtilTest } from '../PreferencesTest';
// 调用测试内容
 aboutToAppear(): void {
   runPreferencesUtilTest()
 }

5.5 测试日志结果

复制代码
 ==================== PreferencesUtil 功能测试开始 ====================
[PreferencesUtil] 已切换到文件:test1
✅ 切换到 test1 成功
✅ get(name) = test-value
✅ has(name) = true
✅ 监听ID: 1773300369641
✅ 取消单个监听成功
[PreferencesUtil] 已切换到文件:test2
✅ 切换到 test2 成功
✅ test2 mode = dark
[PreferencesUtil] 已切换到文件:test1
✅ test1 name 隔离后 = test-value
✅ 删除 name 后 has = false
✅ 取消所有监听成功
[PreferencesUtil] 已切换到文件:test2
[PreferencesUtil] 已切换到文件:test1
✅ 销毁 test1 实例成功
[PreferencesUtil] getInstanceByName: 文件not-exist的实例未初始化,请先调用getInstance初始化
✅ use 不存在文件返回 null,符合预期
[PreferencesUtil] getInstanceByName: fileName 不能为空
[PreferencesUtil] 请先调用use()切换到具体文件
✅ 未切换文件 put 返回: false
✅ 删除 test2 文件成功
==================== PreferencesUtil 功能测试完成 ====================

六、PreferencesUtil核心能力说明

能力点 说明
多文件实例池 通过 getInstance 创建不同文件的实例,实例池保证每个文件唯一实例
默认实例切换 通过 use 切换默认操作文件,静态方法基于默认实例简化调用
存储模式自动降级 GSKV模式不支持时,自动降级为XML模式,无需外部处理
自动刷盘 XML模式下写入/删除数据后自动刷盘,保证数据持久化
资源安全管理 支持监听解绑、实例销毁、文件删除后自动清理缓存,避免内存泄漏
静态+实例双调用 静态方法操作默认文件,实例方法操作指定文件,兼顾便捷性与灵活性

七、注意事项

  1. 上下文传递 :初始化 PreferencesUtil 时必须传入 AbilityContext在程序加载创建时只初始化一次;
  2. 文件名规范:文件名仅支持字母、数字、下划线,避免特殊字符,否则可能导致文件创建失败;
  3. 静态方法依赖 :调用静态方法(如PreferencesUtil.put)前,必须先通过 use 切换到有效文件,否则返回默认值/失败;
  4. 资源清理
    • 页面销毁时调用 offDataChange 取消单个监听;
    • 应用销毁时调用 removeAllListeners 取消所有监听;
  5. 数据持久化验证 :重启应用后,通过 get 方法读取数据,确认数据未丢失(验证刷盘/自动持久化生效)。

八、代码仓库

九、下节预告

下一节我们将聚焦 HAR 库的工程级落地,重点学习三大核心内容:

  1. HAR 构建:掌握 debug/release 构建模式差异,以及字节码 HAR 的打包配置与流程;
  2. 代码混淆:配置 HAR 混淆规则,保护核心代码安全,同时保证对外接口正常调用;
  3. HAR 发布:按照官方流程完成 Preferences 工具类 HAR 库的发布,实现跨项目复用与共享。
相关推荐
Lethehong3 小时前
想掌握全球实时态势?手把手教你部署开源情报工具 World Monitor
人工智能·开源
bkspiderx3 小时前
MQTT 开源库:Eclipse Paho C 详解,特性、交叉编译与实战示例
c语言·mqtt·开源·eclipse paho c
饕餮争锋4 小时前
Supabase使用演示
后端·开源
国医中兴4 小时前
Flutter 三方库 ngrouter 鸿蒙适配指南 - 实现高性能扁平化路由导航管理实战
flutter·harmonyos·鸿蒙·openharmony
2301_764441334 小时前
ProjectAIRI:是一个开源的AI虚拟数字人伴侣
人工智能·目标检测·自然语言处理·开源·视觉检测·语音识别
大雷神4 小时前
HarmonyOS APP<玩转React>开源教程十:组件化开发概述
前端·react.js·开源·harmonyos
国医中兴5 小时前
Flutter 三方库 inject_generator 的鸿蒙化适配指南 - 自动化依赖注入注入生成器、驱动鸿蒙大型工程解耦实战
flutter·harmonyos·鸿蒙·openharmony·inject_generator
Easonmax5 小时前
ReactNative for OpenHarmony项目鸿蒙化三方库:react-native-pager-view — 流畅的页面滑动体验
react native·react.js·harmonyos
❀͜͡傀儡师5 小时前
从“养虾”到数据分析:OpenClaw与DeepAnalyze等开源AI项目全景
人工智能·数据分析·开源