鸿蒙应用开发UI基础第二十四节,如何选择开源工具存储用户首选项数据?

摘要:【学习目标】 明确PreferencesUtil单利模式的局限,掌握「多实例池」的设计思路与实现逻辑; 完成 Preferences 工具类从「单文件单例」到「多文件实例池」的核心升级; 掌握鸿蒙静态库(HAR)的创建、配置、导出、打包全流
【学习目标】 明确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 /** * 通用操作结果回调 * @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 // 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 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 { "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库依赖: { "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 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 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 // 导入测试内容 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 库的发布,实现跨项目复用与共享。