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

Preferences PersistenceV2
定型 KV 存储(键值对) 关系型数据库(ORM)
适合 少量设置、单例、标记 大量结构化实体
API preferences.getPreferences persistenceV2.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) 读取,无则返回 default Promise<ValueType>
delete(key) 删除键 Promise<void>
flush() 持久化到磁盘 Promise<void>
has(key) 是否存在 Promise<boolean>
clear() 清空所有键值 Promise<void>

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


二、PreferenceUtil 单例封装

2.1 为什么用单例模式

最初的 PreferenceUtil 采用全静态方法设计(static getStringstatic 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_theme setString 应用设置
setting_* setting_dark_mode setBoolean 布尔设置
实体列表 categories setObject JSON 序列化
实体列表 budgets setObject JSON 序列化

三、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(联合类型)断言为 stringnumber,编译器会报错:

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

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

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 三种类型转换方式对比

方法 旧写法(报错) 新写法(正确) 说明
getString result as string '' + result 字符串拼接强制转换
getBoolean result as boolean result === true || result === 'true' 兼容布尔和字符串 "true"
getNumber result as number Number('' + 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 存储类型选型

数据类型 存储方法 读取方法 示例
字符串 setString getString 主题、语言、币种
布尔 setBoolean getBoolean 深色模式开关
数字 setNumber getNumber 首日起点(1~7)
对象/数组 setObject getObject 分类列表、账单列表
复杂 JSON setObject getObject 预算列表、统计缓存

选型建议:单值设置用 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. 若无数据,创建内置分类列表,通过 saveAllpersistsetObject 持久化到 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.prefnull。没有空值检查时,调用 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 中 ESObjectValueType 不兼容的编译错误
  • setObject / getObject 实现 JSON 序列化存储,支撑 BaseRepository 全量持久化
  • 处理首次启动初始化内置分类,保证幂等性
  • 理解 Preferences vs PersistenceV2 的选型

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


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


相关资源

Logo

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

更多推荐