Preferences 数据持久化(单例模式 + ESObject 类型实战)

本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第 12 篇,对应 Git Tag v0.1.2。承接第 11 篇的日期组件,本篇接入鸿蒙 Preferences KV 存储,封装 PreferenceUtil 单例工具类,持久化 Setting 设置与内置分类数据,并深入讲解 ArkTS 中 ESObject 类型限制与 ValueType 不兼容的实战解决方案。

前言

记账应用的 设置项 必须持久化——用户切换深色模式、调整币种、修改首日起点后,下次打开应保持。鸿蒙提供 Preferences 轻量级 KV 存储,适合保存设置、单例、首启动标记等少量数据。本章封装 PreferenceUtil 工具类,并持久化 Setting 模型。

本文将带你:

  1. 封装 PreferenceUtil 单例模式 工具类(getInstance / init / get / set / delete / clear)
  2. 深入理解 ArkTS 中 ESObject 类型与 ValueType 不兼容问题及显式转换方案
  3. 用 setObject / getObject 实现 JSON 序列化存储,支撑 BaseRepository 全量数据持久化
  4. 集入 EntryAbility 完成 Preferences 初始化与内置分类初始化
  5. 为后续 PersistenceV2 大数据存储预留接口

企业级核心原则:设置存储必须 抽象封装、异步不阻塞、默认值兜底、空值保护。参考 鸿蒙 Preferences 文档 了解官方约定。


一、Preferences 基础

1.1 Preferences vs PersistenceV2

项PreferencesPersistenceV2
定型KV 存储(键值对)关系型数据库(ORM)
适合少量设置、单例、标记大量结构化实体
APIpreferences.getPreferencespersistenceV2.save
异步全异步(Promise)全异步
容量KB 级MB 级
场景Setting、首启动Bill、Category、Budget

1.2 基础用法

import { preferences } from '@kit.ArkData';

const PREF_NAME = 'harmonyledger';
const KEY_THEME = 'setting_theme';

// 写
const pref = await preferences.getPreferences(context, PREF_NAME);
await pref.put(KEY_THEME, 'dark');
await pref.flush();

// 读
const value = await pref.get(KEY_THEME, 'auto');

1.3 核心 API 速查

API说明返回类型
getPreferences(context, name)获取 Preferences 实例(name 隔离)Promise<Preferences>
put(key, value)写入键值(需 flush 才持久化)Promise<void>
get(key, default)读取,无则返回 defaultPromise<ValueType>
delete(key)删除键Promise<void>
flush()持久化到磁盘Promise<void>
has(key)是否存在Promise<boolean>
clear()清空所有键值Promise<void>

注意:get 方法返回的是 Promise<ValueType>,而非具体类型。这是后续 ESObject 类型问题的根源,详见第三章。


二、PreferenceUtil 单例封装

2.1 为什么用单例模式

最初的 PreferenceUtil 采用全静态方法设计(static getString、static setString),虽然调用简单,但存在以下问题:

  • 静态属性 pref 在类加载时初始化,无法控制初始化时机
  • 静态方法无法被子类覆写,扩展性差
  • 无法实现接口依赖注入,不利于单元测试
  • 多个静态属性共享类级状态,容易产生竞态条件

企业级最佳实践:工具类持有可变状态(如 pref 实例)时,应优先使用 单例模式 而非全静态方法。单例模式提供了更好的封装性、可测试性和生命周期控制能力。

2.2 完整源码

以下是项目中 PreferenceUtil 的实际源码,采用 单例模式 + ESObject 显式类型转换 + 空值保护:

// utils/PreferenceUtil.ets
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

const PREF_NAME = 'harmonyledger';

export class PreferenceUtil {
  private pref: preferences.Preferences | null = null;
  private static instance: PreferenceUtil | null = null;

  static getInstance(): PreferenceUtil {
    if (PreferenceUtil.instance === null) {
      PreferenceUtil.instance = new PreferenceUtil();
    }
    return PreferenceUtil.instance;
  }

  async init(context: common.Context): Promise<void> {
    this.pref = await preferences.getPreferences(context, PREF_NAME);
  }

  async setString(key: string, value: string): Promise<void> {
    if (this.pref === null) return;
    await this.pref.put(key, value);
    await this.pref.flush();
  }

  async getString(key: string, defaultValue: string = ''): Promise<string> {
    if (this.pref === null) return defaultValue;
    let result: ESObject = await this.pref.get(key, defaultValue);
    return '' + result;
  }

  async setBoolean(key: string, value: boolean): Promise<void> {
    if (this.pref === null) return;
    await this.pref.put(key, value);
    await this.pref.flush();
  }

  async getBoolean(key: string, defaultValue: boolean = false): Promise<boolean> {
    if (this.pref === null) return defaultValue;
    let result: ESObject = await this.pref.get(key, defaultValue);
    return result === true || result === 'true';
  }

  async setNumber(key: string, value: number): Promise<void> {
    if (this.pref === null) return;
    await this.pref.put(key, value);
    await this.pref.flush();
  }

  async getNumber(key: string, defaultValue: number = 0): Promise<number> {
    if (this.pref === null) return defaultValue;
    let result: ESObject = await this.pref.get(key, defaultValue);
    return Number('' + result);
  }

  async setObject<T>(key: string, value: T): Promise<void> {
    if (this.pref === null) return;
    const json = JSON.stringify(value);
    await this.pref.put(key, json);
    await this.pref.flush();
  }

  async getObject(key: string, defaultValue: ESObject | null = null): Promise<ESObject | null> {
    if (this.pref === null) return defaultValue;
    let json: ESObject = await this.pref.get(key, '');
    let jsonStr: string = '' + json;
    if (jsonStr.length === 0) {
      return defaultValue;
    }
    try {
      return JSON.parse(jsonStr);
    } catch (e) {
      return defaultValue;
    }
  }

  async delete(key: string): Promise<void> {
    if (this.pref === null) return;
    await this.pref.delete(key);
    await this.pref.flush();
  }

  async clear(): Promise<void> {
    if (this.pref === null) return;
    await this.pref.clear();
    await this.pref.flush();
  }
}

2.3 单例 vs 静态类对比

对比项单例模式(getInstance)全静态方法(static)
初始化时机显式调用 init() 控制类加载时隐式
空值保护this.pref === null 检查需 ! 非空断言
可测试性可注入 Mock 实例难以 Mock
扩展性可实现接口、可继承无法覆写
调用方式PreferenceUtil.getInstance().getString()PreferenceUtil.getString()
推荐度推荐不推荐

2.4 Key 命名规范

全局键命名:{module}_{type}_{name}
示例:
- setting_theme          (设置项:主题)
- setting_language       (设置项:语言)
- setting_currency       (设置项:币种)
- setting_dark_mode      (设置项:深色模式)
- categories             (内置分类列表 JSON)
- budgets                (预算列表 JSON)
Key 类别示例存储方式用途
setting_*setting_themesetString应用设置
setting_*setting_dark_modesetBoolean布尔设置
实体列表categoriessetObjectJSON 序列化
实体列表budgetssetObjectJSON 序列化

三、ArkTS 类型陷阱:ESObject 与 ValueType

3.1 问题背景

在最初的静态类版本中,getString / getNumber / getBoolean 使用 as 进行类型断言:

// ❌ 旧版写法(编译报错)
static async getString(key: string, defaultValue: string = ''): Promise<string> {
  return await this.pref!.get(key, defaultValue) as string;
}

static async getNumber(key: string, defaultValue: number = 0): Promise<number> {
  return await this.pref!.get(key, defaultValue) as number;
}

static async getBoolean(key: string, defaultValue: boolean = false): Promise<boolean> {
  return await this.pref!.get(key, defaultValue) as boolean;
}

3.2 ValueType 不兼容错误

preferences.Preferences.get() 的返回类型是 Promise<ValueType>,其中 ValueType 定义为:

type ValueType = number | string | boolean
  | Array<number> | Array<string> | Array<boolean>
  | Uint8Array | BigInt64Array | BigUint64Array;

ArkTS 的 严格类型系统 不允许直接将 ValueType(联合类型)断言为 string 或 number,编译器会报错:

arkts-no-untyped-obj-literals
Type 'ValueType' is not assignable to type 'string'.

核心问题:as string 在 ArkTS 中不安全,因为 ValueType 是联合类型,运行时实际类型可能是 number 或 boolean,直接断言会绕过类型检查。

3.3 ESObject 显式转换方案

解决方案是先用 ESObject 接收返回值,再通过 显式字符串转换 得到目标类型:

// ✅ 正解:ESObject + 显式转换
async getString(key: string, defaultValue: string = ''): Promise<string> {
  if (this.pref === null) return defaultValue;
  let result: ESObject = await this.pref.get(key, defaultValue);
  return '' + result;  // 字符串拼接,确保返回 string
}

async getBoolean(key: string, defaultValue: boolean = false): Promise<boolean> {
  if (this.pref === null) return defaultValue;
  let result: ESObject = await this.pref.get(key, defaultValue);
  return result === true || result === 'true';  // 兼容布尔与字符串
}

async getNumber(key: string, defaultValue: number = 0): Promise<number> {
  if (this.pref === null) return defaultValue;
  let result: ESObject = await this.pref.get(key, defaultValue);
  return Number('' + result);  // 先转字符串再转数字
}

3.4 三种类型转换方式对比

方法旧写法(报错)新写法(正确)说明
getStringresult as string'' + result字符串拼接强制转换
getBooleanresult as booleanresult === true || result === 'true'兼容布尔和字符串 "true"
getNumberresult as numberNumber('' + result)先转字符串再 Number()

为什么 getBoolean 要兼容字符串 "true"? 因为 Preferences 在某些场景下会将布尔值序列化为字符串,result === true || result === 'true' 确保两种形式都能正确识别为 true。

为什么 getNumber 要 Number('' + result) 而非 Number(result)? 因为 ESObject 类型不能直接传给 Number() 构造函数,需要先通过 '' + result 转为 string,再由 Number() 解析为数字。


四、JSON 序列化存储:setObject 与 getObject

4.1 setObject 源码解析

Preferences 原生只支持基本类型(string / number / boolean),无法直接存储对象或数组。setObject 通过 JSON.stringify 将任意对象序列化为 JSON 字符串后存储:

async setObject<T>(key: string, value: T): Promise<void> {
  if (this.pref === null) return;
  const json = JSON.stringify(value);
  await this.pref.put(key, json);
  await this.pref.flush();
}

4.2 getObject 源码解析

getObject 读取 JSON 字符串并通过 JSON.parse 反序列化,包含 空值检测 和 异常兜底:

async getObject(key: string, defaultValue: ESObject | null = null): Promise<ESObject | null> {
  if (this.pref === null) return defaultValue;
  let json: ESObject = await this.pref.get(key, '');
  let jsonStr: string = '' + json;
  if (jsonStr.length === 0) {
    return defaultValue;
  }
  try {
    return JSON.parse(jsonStr);
  } catch (e) {
    return defaultValue;
  }
}

设计要点:getObject 使用 try-catch 包裹 JSON.parse,当存储的 JSON 数据损坏时返回 defaultValue 而非抛出异常,保证应用不崩溃。

4.3 实战:BaseRepository 复用

项目中的 BaseRepository 正是通过 setObject / getString 实现全量数据持久化的。所有 Repository(Bill、Category、Budget)都继承自它:

// repository/BaseRepository.ets
import { PreferenceUtil } from '../utils/PreferenceUtil';

export abstract class BaseRepository<T> {
  private cache: T[] = [];
  private loaded: boolean = false;
  protected abstract getStorageKey(): string;
  protected abstract getEntityId(entity: T): string;
  protected abstract createEntity(data: ESObject): T;

  private async ensureLoaded(): Promise<void> {
    if (this.loaded) return;
    const json = await PreferenceUtil.getInstance().getString(this.getStorageKey(), '[]');
    this.cache = [];
    if (json.length > 2) {
      const parsed: ESObject[] = JSON.parse(json);
      if (parsed && parsed.length > 0) {
        for (let i = 0; i < parsed.length; i++) {
          this.cache.push(this.createEntity(parsed[i]));
        }
      }
    }
    this.loaded = true;
  }

  private async persist(): Promise<void> {
    const plainArray: ESObject[] = [];
    for (let i = 0; i < this.cache.length; i++) {
      plainArray.push(this.toPlainObject(this.cache[i]));
    }
    await PreferenceUtil.getInstance().setObject(this.getStorageKey(), plainArray);
  }

  protected toPlainObject(entity: T): ESObject {
    return entity;
  }

  async findAll(): Promise<T[]> {
    await this.ensureLoaded();
    return [...this.cache];
  }

  async save(entity: T): Promise<boolean> {
    await this.ensureLoaded();
    const id = this.getEntityId(entity);
    let found = false;
    for (let i = 0; i < this.cache.length; i++) {
      if (this.getEntityId(this.cache[i]) === id) {
        this.cache[i] = entity;
        found = true;
        break;
      }
    }
    if (!found) {
      this.cache.push(entity);
    }
    await this.persist();
    return true;
  }
}

4.4 存储类型选型

数据类型存储方法读取方法示例
字符串setStringgetString主题、语言、币种
布尔setBooleangetBoolean深色模式开关
数字setNumbergetNumber首日起点(1~7)
对象/数组setObjectgetObject分类列表、账单列表
复杂 JSONsetObjectgetObject预算列表、统计缓存

选型建议:单值设置用 setString / setBoolean / setNumber;集合数据用 setObject / getObject。不要用多个 key 存数组元素,应整体序列化。


五、SettingRepository 持久化

5.1 完整源码

SettingRepository 通过 PreferenceUtil.getInstance() 调用实例方法,持久化各项设置:

// repository/SettingRepository.ets
import { PreferenceUtil } from '../utils/PreferenceUtil';

export class SettingRepository {
  private static instance: SettingRepository;
  static getInstance(): SettingRepository {
    if (!SettingRepository.instance) {
      SettingRepository.instance = new SettingRepository();
    }
    return SettingRepository.instance;
  }

  async saveTheme(mode: string): Promise<void> {
    await PreferenceUtil.getInstance().setString('setting_theme', mode);
  }

  async loadTheme(): Promise<string> {
    return await PreferenceUtil.getInstance().getString('setting_theme', 'auto');
  }

  async saveLanguage(lang: string): Promise<void> {
    await PreferenceUtil.getInstance().setString('setting_language', lang);
  }

  async saveCurrency(currency: string): Promise<void> {
    await PreferenceUtil.getInstance().setString('setting_currency', currency);
  }

  async saveDarkMode(enabled: boolean): Promise<void> {
    await PreferenceUtil.getInstance().setBoolean('setting_dark_mode', enabled);
  }

  async loadDarkMode(): Promise<boolean> {
    return await PreferenceUtil.getInstance().getBoolean('setting_dark_mode', false);
  }
}

5.2 调用对比

// ❌ 旧版调用(静态方法,已废弃)
const theme = await PreferenceUtil.getString('setting_theme', 'auto');
await PreferenceUtil.setBoolean('setting_dark_mode', true);

// ✅ 新版调用(单例实例方法)
const theme = await PreferenceUtil.getInstance().getString('setting_theme', 'auto');
await PreferenceUtil.getInstance().setBoolean('setting_dark_mode', true);

迁移要点:全局搜索 PreferenceUtil. 替换为 PreferenceUtil.getInstance().,所有调用点统一改为实例方法调用。


六、EntryAbility 集入

6.1 完整源码

Preferences 的初始化在 onWindowStageCreate 生命周期中完成,而非 onCreate:

// entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { ThemeManager } from '../theme/ThemeManager';
import { PreferenceUtil } from '../utils/PreferenceUtil';
import { CategoryRepository } from '../repository/CategoryRepository';

const DOMAIN = 0x0000;
const TAG = 'EntryAbility';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN, TAG, 'onCreate: HarmonyLedger 启动');
    ThemeManager.init();
  }

  async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
    // 初始化偏好存储
    await PreferenceUtil.getInstance().init(this.context);
    // 初始化默认分类数据
    await CategoryRepository.getInstance().initBuiltinCategories();

    windowStage.loadContent('pages/MainView', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, TAG, 'loadContent failed: ' + err.message);
        return;
      }
      hilog.info(DOMAIN, TAG, 'loadContent success: pages/MainView');
    });
  }

  onWindowStageDestroy(): void {
    hilog.info(DOMAIN, TAG, 'onWindowStageDestroy');
  }

  onForeground(): void {
    hilog.info(DOMAIN, TAG, 'onForeground');
  }

  onBackground(): void {
    hilog.info(DOMAIN, TAG, 'onBackground');
  }
}

6.2 启动流程

应用启动
  ↓
EntryAbility.onCreate
  ├─ ThemeManager.init()(同步,应用默认主题)
  ↓
EntryAbility.onWindowStageCreate
  ├─ PreferenceUtil.getInstance().init(context)(异步,初始化 Preferences)
  ├─ CategoryRepository.getInstance().initBuiltinCategories()(异步,初始化分类)
  ↓
windowStage.loadContent('pages/MainView')
  ↓
页面渲染,全局可用

6.3 生命周期说明

生命周期是否异步Preferences 操作说明
onCreate同步无仅初始化主题,不涉及存储
onWindowStageCreate异步init(context)初始化 Preferences + 分类数据
onForeground同步无应用回到前台
onBackground同步无应用退到后台

关键设计:onCreate 是同步方法,不能调用 await。Preferences 的异步初始化放在 onWindowStageCreate 中,该方法是 async,可以正确等待初始化完成后再加载页面。


七、ThemeManager 主题管理

7.1 完整源码

ThemeManager 负责主题切换与颜色应用,通过 AppStorage 实现全局响应式:

// theme/ThemeManager.ets
import { AppColors } from './Colors';
import { AppDarkColors } from './DarkColors';

export enum ThemeMode {
  LIGHT = 'light',
  DARK = 'dark',
  AUTO = 'auto'
}

export class ThemeManager {
  private static readonly KEY_THEME_MODE = 'theme_mode';
  private static currentMode: ThemeMode = ThemeMode.AUTO;

  static init(mode: ThemeMode = ThemeMode.AUTO): void {
    ThemeManager.currentMode = mode;
    AppStorage.setOrCreate(ThemeManager.KEY_THEME_MODE, mode);
    ThemeManager.applyTheme(mode);
  }

  static switch(mode: ThemeMode): void {
    ThemeManager.currentMode = mode;
    AppStorage.set(ThemeManager.KEY_THEME_MODE, mode);
    ThemeManager.applyTheme(mode);
  }

  static applyTheme(mode: ThemeMode): void {
    if (mode === ThemeMode.DARK) {
      ThemeManager.applyDarkColors();
    } else {
      ThemeManager.applyLightColors();
    }
  }

  private static applyLightColors(): void {
    AppStorage.setOrCreate('color.background', AppColors.Background);
    AppStorage.setOrCreate('color.card', AppColors.CardBackground);
    AppStorage.setOrCreate('color.text.primary', AppColors.PrimaryText);
    AppStorage.setOrCreate('color.text.secondary', AppColors.SecondaryText);
    AppStorage.setOrCreate('color.separator', AppColors.Separator);
  }

  private static applyDarkColors(): void {
    AppStorage.setOrCreate('color.background', AppDarkColors.Background);
    AppStorage.setOrCreate('color.card', AppDarkColors.CardBackground);
    AppStorage.setOrCreate('color.text.primary', AppDarkColors.PrimaryText);
    AppStorage.setOrCreate('color.text.secondary', AppDarkColors.SecondaryText);
    AppStorage.setOrCreate('color.separator', AppDarkColors.Separator);
  }

  static getCurrentMode(): ThemeMode {
    return ThemeManager.currentMode;
  }
}

7.2 主题切换流程

当用户在设置页切换主题时,调用 ThemeManager.switch(mode),通过 AppStorage.set 全局广播颜色变更,所有绑定 @StorageLink 的组件自动刷新。


八、内置分类初始化

8.1 CategoryRepository.initBuiltinCategories

首次启动时,CategoryRepository 通过 PreferenceUtil.getInstance().setObject() 持久化内置分类列表:

// repository/CategoryRepository.ets(核心方法)
async initBuiltinCategories(): Promise<void> {
  const all = await this.findAll();
  if (all.length > 0) return;
  const builtin: Category[] = [
    new Category('food', '餐饮', BillType.EXPENSE, 'icon_category_food', AppColors.Expense),
    new Category('transport', '交通', BillType.EXPENSE, 'icon_category_transport', AppColors.Expense),
    new Category('shopping', '购物', BillType.EXPENSE, 'icon_category_shopping', AppColors.Expense),
    new Category('entertainment', '娱乐', BillType.EXPENSE, 'icon_category_entertainment', AppColors.Expense),
    new Category('housing', '房租', BillType.EXPENSE, 'icon_category_housing', AppColors.Expense),
    new Category('salary', '工资', BillType.INCOME, 'icon_category_salary', AppColors.Income),
    new Category('bonus', '奖金', BillType.INCOME, 'icon_category_bonus', AppColors.Income),
    new Category('investment', '投资', BillType.INCOME, 'icon_category_investment', AppColors.Income),
  ];
  builtin.forEach((c, index) => {
    c.builtin = true;
    c.sort = index;
  });
  await this.saveAll(builtin);
}

8.2 初始化流程

  1. EntryAbility.onWindowStageCreate 调用 PreferenceUtil.getInstance().init(context)
  2. CategoryRepository.getInstance().initBuiltinCategories() 被调用
  3. BaseRepository.ensureLoaded() 通过 PreferenceUtil.getInstance().getString('categories', '[]') 读取已有数据
  4. 若已有数据(all.length > 0),跳过初始化
  5. 若无数据,创建内置分类列表,通过 saveAll → persist → setObject 持久化到 Preferences

幂等设计:initBuiltinCategories 首先检查 all.length > 0,确保多次调用不会重复插入。这是企业级代码的 幂等性 原则。


九、最佳实践

9.1 Preferences 适合的数据

数据适合原因
Setting 设置适合单例、少量、低频改
首启动标记适合一次性、布尔
内置分类列表适合少量、JSON 序列化
Bill 账单不适合大量、结构化、需查询
Budget 预算不适合按月查询、需聚合

9.2 异步不阻塞

// ❌ 错误:同步阻塞 UI
const value = preferences.getSync(KEY, '');

// ✅ 正解:异步 Promise
const value = await PreferenceUtil.getInstance().getString(KEY, '');

9.3 默认值兜底

// 所有 get 必须传默认值,避免 undefined
await PreferenceUtil.getInstance().getString('setting_theme', 'auto');
await PreferenceUtil.getInstance().getNumber('setting_first_day', 1);
await PreferenceUtil.getInstance().getBoolean('setting_dark_mode', false);

9.4 flush 时机

// put 后必须 flush 才持久化到磁盘
await pref.put(KEY, value);
await pref.flush();  // 异步写盘

// 高频写场景可批量 put 后一次 flush
await pref.put(K1, V1);
await pref.put(K2, V2);
await pref.put(K3, V3);
await pref.flush();  // 一次写盘

9.5 空值保护

// 每个方法内部检查 pref 是否为 null
async getString(key: string, defaultValue: string = ''): Promise<string> {
  if (this.pref === null) return defaultValue;  // 未初始化时返回默认值
  let result: ESObject = await this.pref.get(key, defaultValue);
  return '' + result;
}

为什么需要空值保护? 如果 init() 尚未调用或调用失败,this.pref 为 null。没有空值检查时,调用 this.pref.get() 会抛出 NullPointerException,导致应用崩溃。空值保护确保即使初始化失败,应用也能优雅降级。


十、运行验证

10.1 编译检查

hvigorw assembleHap --mode module -p product=default

10.2 功能验证

  1. 首次启动应用,hilog 输出 "onCreate: HarmonyLedger 启动" 和 "loadContent success"
  2. 首次启动后内置分类已初始化(餐饮、交通、购物等)
  3. 修改设置后关闭应用,重开应保持设置
  4. 删除应用数据后重开,内置分类重新初始化
    在这里插入图片描述
    在这里插入图片描述

十一、常见问题

11.1 Preferences 未初始化

// 错误:未调用 init 就 get,pref 为 null
const v = await PreferenceUtil.getInstance().getString('k', '');
// 由于空值保护,返回默认值 '' 而非报错

// 解决:onWindowStageCreate 中先 init
await PreferenceUtil.getInstance().init(this.context);

11.2 flush 丢失

// 错误:put 后未 flush,应用崩溃数据丢失
await pref.put(KEY, value);
// 应用被杀,数据未写盘

// 解决:每次 put 后立即 flush
await pref.put(KEY, value);
await pref.flush();

11.3 ESObject 类型报错

// 错误:直接 as 断言,ArkTS 编译报错
return await this.pref.get(key, defaultValue) as string;

// 解决:用 ESObject 接收 + 显式转换
let result: ESObject = await this.pref.get(key, defaultValue);
return '' + result;

11.4 JSON 解析失败

// 错误:未用 try-catch,JSON 损坏时崩溃
const data = JSON.parse(jsonStr);

// 解决:getObject 内部已封装 try-catch
const data = await PreferenceUtil.getInstance().getObject('key', null);

十二、Git 提交

12.1 Commit Message

git add .
git commit -m "feat(persistence): 接入 Preferences 数据持久化

- 新增 PreferenceUtil 单例工具类(getInstance / init / get / set / delete / clear)
- 使用 ESObject 显式类型转换解决 ValueType 不兼容问题
- 新增 setObject/getObject 支持 JSON 序列化存储
- 新增 SettingRepository 持久化各项设置
- 新增 BaseRepository 基于 setObject 的全量数据持久化
- EntryAbility 集入 Preferences 初始化与内置分类初始化"

12.2 CHANGELOG

## [v0.1.2] - 2026-07-27
### Added
- utils/PreferenceUtil.ets:Preferences 单例封装(含 ESObject 类型转换)
- repository/SettingRepository.ets:设置数据访问
- repository/BaseRepository.ets:基于 Preferences 的通用 Repository
### Changed
- entryability/EntryAbility.ets:集入 Preferences 初始化
- repository/CategoryRepository.ets:内置分类初始化

总结

本文完整介绍了 Preferences 数据持久化,涵盖 PreferenceUtil 单例封装、ESObject 显式类型转换、setObject/getObject JSON 序列化、SettingRepository、EntryAbility 集入、内置分类初始化。通过本篇你可以:

  • 用 Preferences 持久化少量设置与标记
  • 封装 PreferenceUtil 单例 工具类,统一 API(getInstance / get / set / delete)
  • 解决 ArkTS 中 ESObject 与 ValueType 不兼容的编译错误
  • 用 setObject / getObject 实现 JSON 序列化存储,支撑 BaseRepository 全量持久化
  • 处理首次启动初始化内置分类,保证幂等性
  • 理解 Preferences vs PersistenceV2 的选型

下一篇预告:《PersistenceV2 数据库升级》将接入鸿蒙 PersistenceV2 ORM,把 Bill/Category/Budget 从内存 Map 切换到真实数据库,完成数据持久化。


如果这篇文章对你有帮助,欢迎 点赞 、 收藏 、 关注 ,你的支持是我持续创作的动力!在评论区告诉我你在 ArkTS 类型转换中还遇到过哪些坑,我会针对性地分享更多实战经验。


相关资源

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐