HarmonyOS NEXT 企业级记账APP:Preferences 数据持久化(单例模式 + ESObject 类型实战)
Preferences 数据持久化(单例模式 + ESObject 类型实战)
本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第 12 篇,对应 Git Tag v0.1.2。承接第 11 篇的日期组件,本篇接入鸿蒙 Preferences KV 存储,封装 PreferenceUtil 单例工具类,持久化 Setting 设置与内置分类数据,并深入讲解 ArkTS 中
ESObject类型限制与ValueType不兼容的实战解决方案。
前言
记账应用的 设置项 必须持久化——用户切换深色模式、调整币种、修改首日起点后,下次打开应保持。鸿蒙提供 Preferences 轻量级 KV 存储,适合保存设置、单例、首启动标记等少量数据。本章封装 PreferenceUtil 工具类,并持久化 Setting 模型。
本文将带你:
- 封装 PreferenceUtil 单例模式 工具类(getInstance / init / get / set / delete / clear)
- 深入理解 ArkTS 中 ESObject 类型与
ValueType不兼容问题及显式转换方案 - 用
setObject/getObject实现 JSON 序列化存储,支撑 BaseRepository 全量数据持久化 - 集入 EntryAbility 完成 Preferences 初始化与内置分类初始化
- 为后续 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 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_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(联合类型)断言为 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 三种类型转换方式对比
| 方法 | 旧写法(报错) | 新写法(正确) | 说明 |
|---|---|---|---|
| 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 初始化流程
EntryAbility.onWindowStageCreate调用PreferenceUtil.getInstance().init(context)CategoryRepository.getInstance().initBuiltinCategories()被调用BaseRepository.ensureLoaded()通过PreferenceUtil.getInstance().getString('categories', '[]')读取已有数据- 若已有数据(
all.length > 0),跳过初始化 - 若无数据,创建内置分类列表,通过
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 功能验证
- 首次启动应用,hilog 输出
"onCreate: HarmonyLedger 启动"和"loadContent success" - 首次启动后内置分类已初始化(餐饮、交通、购物等)
- 修改设置后关闭应用,重开应保持设置
- 删除应用数据后重开,内置分类重新初始化


十一、常见问题
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 类型转换中还遇到过哪些坑,我会针对性地分享更多实战经验。
相关资源
- 本篇源码:GitHub Tag v0.1.2
- 鸿蒙 Preferences 文档:data-preferences
- ArkData 模块:ark-data
- 数据存储选型指南:data-storage-guide
- Preferences 最佳实践:preferences-best-practice
- Ability 生命周期:ability-lifecycle
- AppStorage 响应式:appstorage-reactive
- ArkTS 类型系统:arkts-type-system
- ESObject 使用指南:esobject-guide
更多推荐


所有评论(0)