【三国志 App 实战系列 05】Preferences 实现收藏、笔记与本地持久化:单机 App 的数据闭环
讲解 HarmonyOS 单机应用如何使用 Preferences 保存 JSON 数据,实现收藏、笔记、主题模式、听书队列和播放进度持久化。
系列第 5 篇。本文讲 HarmonyOS 单机应用中如何使用 Preferences 保存收藏、笔记、听书进度等用户数据。

一、为什么首版选择 Preferences
项目初期不一定需要上 RDB。对于收藏、笔记、主题设置、听书队列这类数据,Preferences 足够支撑首个版本:
- API 简单
- 数据规模可控
- 适合保存 JSON 字符串
- 迁移成本低
项目中使用的 key 示例:
const PREF_NAME: string = 'records_three_kingdoms';
const PREF_FAVORITES: string = 'favorites_json';
const PREF_NOTES: string = 'notes_json';
const PREF_AUDIOS: string = 'audios_json';
const PREF_THEME_MODE: string = 'theme_mode';
二、保存 JSON,而不是保存 class 实例
Preferences 适合保存纯数据。不要直接把带方法、Resource、复杂引用的 class 实例塞进去,而是转换成 JSON:
interface NoteJson {
id: string;
targetId: string;
targetType: string;
title: string;
content: string;
createdAt: string;
updatedAt: string;
}
保存时:
private async persistNotes(): Promise<void> {
const pref = await preferences.getPreferences(this.getAbilityContext(), PREF_NAME);
await pref.put(PREF_NOTES, JSON.stringify(this.notes));
await pref.flush();
}
恢复时再补默认值,避免旧版本数据缺字段导致页面异常。
三、数组更新的 ArkUI 注意点
ArkUI 状态刷新最常见坑:直接 push 原数组,页面不一定刷新。
不推荐:
this.favoriteRecords.push(record);
推荐:
const next: FavoriteRecord[] = this.favoriteRecords.slice();
next.unshift(record);
this.favoriteRecords = next;
this.persistFavorites();
核心原则是:复制数组,在副本上修改,再整体赋值。
四、收藏即时刷新
收藏按钮点击后,详情页、收藏页、我的统计都可能需要同步变化。因此收藏状态不能只靠旧缓存。
private isFavorite(targetId: string, targetType: string): boolean {
return this.favoriteRecords.some((item: FavoriteRecord) =>
item.targetId === targetId && item.targetType === targetType);
}
页面每次构建时从源数据计算状态,减少缓存失真。
五、笔记编辑策略
笔记必须记录 createdAt 和 updatedAt。列表排序建议使用 updatedAt,这样编辑后的笔记能回到顶部。
private updateNote(id: string, content: string): void {
const next = this.noteRecords.map((item: NoteRecord) => {
if (item.id !== id) {
return item;
}
item.content = content;
item.updatedAt = new Date().toISOString();
return item;
});
this.noteRecords = next;
this.persistNotes();
}
六、听书进度同样属于用户数据
听书进度不是静态模板字段,而是用户状态:
private setAudioProgress(audioId: string, seconds: number): void {
const next: AudioRecord[] = this.audioRecords.map((item: AudioRecord) => {
if (item.id === audioId) {
return new AudioRecord(item.id, item.targetId, item.title,
item.durationText, seconds, item.durationSeconds);
}
return item;
});
this.audioRecords = next;
this.persistAudios();
}
七、Preferences 读写边界怎么收口
在内容型 App 里,Preferences 真正麻烦的不是 API 数量,而是“谁负责读写”。如果页面自己直接决定 key、序列化格式和默认值,时间一长一定会变成维护负担。
更稳的分层方式是:
- 页面:只关心用户动作和显示结果
- service/store:定义 key、容错、迁移和写回时机
- model:定义 JSON 结构,不直接依赖页面组件
例如收藏和笔记可以共用一层轻量 JSON store:
export class LocalJsonStore {
constructor(private storeName: string) {}
async readRecord<T>(key: string, fallback: T): Promise<T> {
const store = await preferences.getPreferences(getContext(), this.storeName);
const raw = await store.get(key, JSON.stringify(fallback)) as string;
try {
return JSON.parse(raw) as T;
} catch (_) {
return fallback;
}
}
}
把默认值和异常兜底收在这里,页面层就不用每次都重复 try/catch。
八、版本迁移与脏数据恢复
Preferences 很适合首版,但前提是你承认“历史数据会变脏”。最常见的来源有三种:
- 调试阶段写入过旧结构
- 后续版本给记录加了新字段
- 用户侧数据被意外截断或写成非 JSON
所以我更建议从一开始就给用户侧快照加版本号,而不是等升级时再补:
interface UserDataSnapshot {
version: number;
favorites: FavoriteRecord[];
notes: NoteRecord[];
audioProgress: AudioRecord[];
}
读取时按版本做最小迁移:
private normalizeSnapshot(raw: Partial<UserDataSnapshot>): UserDataSnapshot {
return {
version: raw.version ?? 1,
favorites: raw.favorites ?? [],
notes: raw.notes ?? [],
audioProgress: raw.audioProgress ?? []
};
}
这样即便以后从单独的 favorites_json 迁移到整包 snapshot,也不会让页面直接吃到脏数据。
九、调试命令与排错清单
Preferences 相关问题经常表现为“页面看着没错,但重启后状态丢了”。这种问题一定要走重启链路验证,而不是只看当前页面刷新。
发布前我会跑这一组命令:
hdc list targets
hdc install -r .\entry\build\default\outputs\default\entry-default-signed.hap
hdc shell aa force-stop com.example.recordofthreekingdoms
hdc shell aa start -a EntryAbility -b com.example.recordofthreekingdoms
hdc shell hilog | Select-String -Pattern "Preferences|Favorite|Note|Audio"
如果收藏或笔记丢失,优先排查:
put()后是否执行了flush()- 写入的是不是 class 实例而不是纯 JSON
- 页面刷新时是不是直接复用了旧数组引用
最小日志建议至少保留写入 key 和记录数量:
private async persistFavorites(): Promise<void> {
const pref = await preferences.getPreferences(this.getAbilityContext(), PREF_NAME);
hilog.info(0x0000, 'FavoriteStore', 'persist favorites size=%{public}d', this.favoriteRecords.length);
await pref.put(PREF_FAVORITES, JSON.stringify(this.favoriteRecords));
await pref.flush();
}
十、工程实现与验收清单
Preferences 的难点不在 API 调用,而在数据格式、异常恢复和页面刷新。建议把读写逻辑封装在服务层,页面不要直接操作 Preferences。
export class LocalJsonStore {
constructor(private storeName: string) {}
async readArray<T>(key: string): Promise<T[]> {
const store = await preferences.getPreferences(getContext(), this.storeName);
const raw = await store.get(key, '[]') as string;
try {
return JSON.parse(raw) as T[];
} catch (_) {
return [];
}
}
async writeArray<T>(key: string, records: T[]): Promise<void> {
const store = await preferences.getPreferences(getContext(), this.storeName);
await store.put(key, JSON.stringify(records));
await store.flush();
}
}
如果 App 后续会升级,建议给用户数据加版本字段:
interface UserDataSnapshot {
version: number;
favorites: FavoriteRecord[];
notes: NoteRecord[];
audioProgress: AudioRecord[];
}
验收清单:
| 场景 | 检查点 |
|---|---|
| 首次安装 | 读取空数据不崩溃 |
| 收藏后重启 | 收藏状态仍存在 |
| 笔记修改 | 更新时间正确刷新 |
| JSON 异常 | 页面回退为空数组 |
| 多页面刷新 | 详情页和收藏页状态一致 |
| 清空播放队列后重启 | 听书进度仍能恢复到对应条目 |
| 旧版本缺少新字段 | normalize 后页面不崩溃 |
Preferences 非常适合单机应用的首版用户数据闭环。下一篇会进入离线势力地图:如何在 ArkUI 中做可交互地图与年份切换。
更多推荐


所有评论(0)