鸿蒙开发ArkData用户首选项实战:从夜间模式到多进程配置的完整案例

用户首选项(Preferences)是 ArkData 里最轻量的存储,但用得最频繁——几乎每个应用都要存配置。下面用一个"应用设置中心"的完整案例,把读写、订阅、存储模式选择、容易出问题的点全过一遍。

一、案例背景:一个设置中心

假设我们在做一个阅读应用,设置中心要持久化这些配置:

  • 夜间模式开关(boolean)
  • 字体大小(number,14-32)
  • 阅读偏好(string,“翻页”/“滚动”)
  • 自定义特殊字符(含非 UTF-8 字符的字符串)

这些都是典型的"轻量级、读多写少、需持久化"数据,正好是 Preferences 的主场。

二、第一步:导入模块和获取实例

import { preferences } from '@kit.ArkData';
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

let dataPreferences: preferences.Preferences | null = null;

class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    // 获取应用上下文
    const context = this.context;
    // 配置选项,name 是持久化文件名
    let options: preferences.Options = { name: 'myStore' };
    // 同步获取 Preferences 实例
    dataPreferences = preferences.getPreferencesSync(context, options);
  }
}

几个要点:

  • name: 'myStore' 是持久化文件名,每个文件唯一对应一个 Preferences 实例
  • 系统通过静态容器把实例存在内存里,直到主动移除或删除文件
  • 持久化文件保存在应用沙箱内,路径通过 context 获取

三、第二步:选择存储模式(API 18 起)

Preferences 默认用 XML 格式存储。从 API 18 开始,多了 GSKV 模式。两种模式的差别很关键:

模式 格式 落盘时机 并发安全 跨平台
XML(默认) XML 文本 调用 flush 时 不安全 支持
GSKV 二进制 实时落盘 支持多进程并发 不支持

选型建议:单进程小数据量用 XML;多进程并发读写用 GSKV。

选 GSKV 前要先判断平台是否支持:

let isGskvSupported = preferences.isStorageTypeSupported(preferences.StorageType.GSKV);
if (isGskvSupported) {
  let options: preferences.Options = { 
    name: 'myStore', 
    storageType: preferences.StorageType.GSKV 
  };
  dataPreferences = preferences.getPreferencesSync(context, options);
}

注意:选定一种存储模式后不允许切换。所以这个决策要在最开始做对。

四、第三步:写入数据(含非 UTF-8 字符处理)

// 检查键是否存在,避免重复写入
if (dataPreferences.hasSync('nightMode')) {
  console.info('nightMode 已存在');
} else {
  // 写入基本类型
  dataPreferences.putSync('nightMode', false);
  dataPreferences.putSync('fontSize', 18);
  dataPreferences.putSync('readMode', '翻页');
  
  // 处理非 UTF-8 字符串:转成 Uint8Array 再存
  let specialChars = '~!@#¥%……&*()——+?';
  let uInt8Array = new util.TextEncoder().encodeInto(specialChars);
  dataPreferences.putSync('specialChars', uInt8Array);
}

这里有个容易出问题的点:XML 模式下,字符串包含非 UTF-8 字符时,必须转成 Uint8Array 存储,否则持久化文件会格式错误、文件损坏。

写入后,XML 模式需要调用 flush() 才真正落盘;GSKV 模式实时落盘,不需要 flush。

五、第四步:读取数据

// 读取基本类型,第二个参数是默认值(键不存在时返回)
let nightMode = dataPreferences.getSync('nightMode', false) as boolean;
let fontSize = dataPreferences.getSync('fontSize', 18) as number;
let readMode = dataPreferences.getSync('readMode', '翻页') as string;

// 读取 Uint8Array 并转回字符串
let uInt8Array = dataPreferences.getSync('specialChars', new Uint8Array(0)) as Uint8Array;
let textDecoder = util.TextDecoder.create('utf-8');
let originalStr = textDecoder.decodeToString(uInt8Array);

getSync 的第二个参数是默认值——当键不存在或值为 null 时返回它。这个设计很实用,省去了先 hasSyncgetSync 的两步操作。

六、第五步:订阅数据变更(设置页和阅读页联动)

设置中心和阅读页通常不在同一个组件,怎么让阅读页在用户改了字号后实时响应?用订阅。

// 订阅数据变更
let observer = (key: string) => {
  console.info(`配置变更: ${key}`);
  if (key === 'fontSize') {
    // 读取新值并通知 UI 刷新
    let newSize = dataPreferences.getSync('fontSize', 18) as number;
    AppStorage.setOrCreate('currentFontSize', newSize);
  }
};
dataPreferences.on('change', observer);

// 修改数据触发回调
dataPreferences.put('fontSize', 22, (err) => {
  if (err) {
    console.error(`写入失败: ${err.message}`);
    return;
  }
  // XML 模式下,flush 后才会触发 observer
  dataPreferences.flush((err) => {
    if (err) {
      console.error(`flush 失败: ${err.message}`);
      return;
    }
    console.info('flush 成功,observer 已触发');
  });
});

XML 模式和 GSKV 模式的回调时机不同

  • XML 模式:订阅的 key 变更后,执行 flush 时才触发 observer
  • GSKV 模式:订阅的 key 变更后,立即触发 observer(因为实时落盘)

这个差异在跨模式迁移代码时最容易出 bug——从 XML 迁到 GSKV,回调时机变了,原来靠 flush 触发的逻辑可能提前执行。

七、第六步:删除数据

// 删除单个键值对
dataPreferences.deleteSync('specialChars');

删除后记得 flush(XML 模式)。

八、第七步:删除整个 Preferences 文件

let options: preferences.Options = { name: 'myStore' };
preferences.deletePreferences(context, options, (err) => {
  if (err) {
    console.error(`删除失败: ${err.message}`);
    return;
  }
  console.info('删除成功');
});

deletePreferences 会从内存移除实例,同时删除持久化文件(包括备份文件、损坏文件)。三个注意点:

  1. 调用后不允许再使用该实例,否则数据一致性出问题
  2. 删除后数据不可恢复
  3. GSKV 模式下不支持与其他接口并发调用

九、约束限制速查

把官方的约束整理成速查表,避免出问题:

约束 说明
Key 长度 ≤ 1024 字节,非空 string
Value(string) UTF-8 编码,≤ 16MB
数据量建议 ≤ 50MB,超过会变耗时操作
加密 不支持,需自行加密后用 Uint8Array 存
XML 模式并发 不安全,多进程会文件损坏
deletePreferences 不允许与其他接口多线程/多进程并发
订阅生命周期 removePreferencesFromCache 或 deletePreferences 后自动取消订阅

十、实战封装:一个 SettingsManager

把上面的散装 API 封装成一个可复用的管理类,实际项目里更好用:

import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { util } from '@kit.ArkTS';

export class SettingsManager {
  private static instance: SettingsManager | null = null;
  private prefs: preferences.Preferences | null = null;

  private constructor(context: common.Context) {
    let options: preferences.Options = { name: 'app_settings' };
    this.prefs = preferences.getPreferencesSync(context, options);
  }

  static getInstance(context: common.Context): SettingsManager {
    if (!SettingsManager.instance) {
      SettingsManager.instance = new SettingsManager(context);
    }
    return SettingsManager.instance;
  }

  setBoolean(key: string, value: boolean): void {
    this.prefs?.putSync(key, value);
    this.prefs?.flush();
  }

  getBoolean(key: string, defaultValue: boolean = false): boolean {
    return this.prefs?.getSync(key, defaultValue) as boolean;
  }

  setNumber(key: string, value: number): void {
    this.prefs?.putSync(key, value);
    this.prefs?.flush();
  }

  getNumber(key: string, defaultValue: number = 0): number {
    return this.prefs?.getSync(key, defaultValue) as number;
  }

  onChange(callback: (key: string) => void): void {
    this.prefs?.on('change', callback);
  }

  offChange(callback: (key: string) => void): void {
    this.prefs?.off('change', callback);
  }
}

业务侧用起来就很干净:

let settings = SettingsManager.getInstance(this.context);
settings.setBoolean('nightMode', true);
let isNight = settings.getBoolean('nightMode');
settings.onChange((key) => {
  if (key === 'nightMode') {
    // 刷新夜间模式 UI
  }
});

十一、几条经验

  1. 选对存储模式:单进程用 XML,多进程并发用 GSKV(API 18+),选定不可切换
  2. 非 UTF-8 字符串转 Uint8Array:XML 模式下不转会损坏文件
  3. XML 模式记得 flush:put 后不 flush 不落盘,订阅回调也不触发
  4. 数据量控制在 50MB 内:超过会变耗时操作,别在主线程跑
  5. 加密要自己做:Preferences 不支持加密,敏感数据先加密再存 Uint8Array

下一篇进入键值型数据库,处理数据量更大、需要分布式同步的场景。

Logo

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

更多推荐