【学习目标】
- 明确PreferencesUtil单利模式的局限,掌握「多实例池」的设计思路与实现逻辑;
- 完成 Preferences 工具类从「单文件单例」到「多文件实例池」的核心升级;
- 掌握鸿蒙静态库(HAR)的创建、配置、导出、打包全流程;
- 将升级后的「多实例池工具类」解耦封装为独立 HAR 库,实现跨项目复用;
- 掌握库工程规范、版本管理、接口设计、测试验证的企业级实践。
一、核心背景:上一节单例模式的局限
上一节 PreferencesUtil 是单例模式,仅能操作一个固定的 Preferences 文件,在复杂业务场景下存在明显局限:
| 局限点 | 具体问题 |
|---|---|
| 单文件存储 | 所有业务配置(用户信息/应用配置/缓存数据)挤在一个文件,易出现键名冲突 |
| 无法隔离业务数据 | 不同模块/业务的配置无法隔离,删除/修改数据时易误操作其他业务的配置 |
| 复用性差 | 工具类与业务工程耦合,无法直接迁移到其他项目 |
| 扩展能力弱 | 新增存储文件需修改工具类源码,不符合「开闭原则」 |
本节核心解决思路:
- 升级架构:从「单例」升级为「多实例池」,每个存储文件对应一个独立实例;
- 解耦封装:剥离业务依赖,封装为独立 HAR 库;
- 简化使用:保留「静态快捷方法+实例方法」双调用模式,兼顾便捷性与灵活性。
二、工程结构(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 库模块步骤:
- 右键工程根目录 →
New→Module; - 选择
Static Library(静态库)→ 填写模块名称preferences完成创建; - 手动在
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 执行打包操作
- 打开 DevEco Studio,选中工程根目录下的
preferences模块; - 点击顶部菜单栏:
Build > Make Module 'preferences'; - 等待打包完成会多出一个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模式下写入/删除数据后自动刷盘,保证数据持久化 |
| 资源安全管理 | 支持监听解绑、实例销毁、文件删除后自动清理缓存,避免内存泄漏 |
| 静态+实例双调用 | 静态方法操作默认文件,实例方法操作指定文件,兼顾便捷性与灵活性 |
七、注意事项
- 上下文传递 :初始化
PreferencesUtil时必须传入AbilityContext在程序加载创建时只初始化一次; - 文件名规范:文件名仅支持字母、数字、下划线,避免特殊字符,否则可能导致文件创建失败;
- 静态方法依赖 :调用静态方法(如PreferencesUtil.put)前,必须先通过
use切换到有效文件,否则返回默认值/失败; - 资源清理 :
- 页面销毁时调用
offDataChange取消单个监听; - 应用销毁时调用
removeAllListeners取消所有监听;
- 页面销毁时调用
- 数据持久化验证 :重启应用后,通过
get方法读取数据,确认数据未丢失(验证刷盘/自动持久化生效)。
八、代码仓库
- 工程名称:PreferenceLibDemo
- 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git
九、下节预告
下一节我们将聚焦 HAR 库的工程级落地,重点学习三大核心内容:
- HAR 构建:掌握 debug/release 构建模式差异,以及字节码 HAR 的打包配置与流程;
- 代码混淆:配置 HAR 混淆规则,保护核心代码安全,同时保证对外接口正常调用;
- HAR 发布:按照官方流程完成 Preferences 工具类 HAR 库的发布,实现跨项目复用与共享。