HarmonyOS APP《画伴梦工厂》开发第34篇-数据持久化——preferences存储与作品管理
第4.8篇:数据持久化——preferences 存储与作品管理
难度:⭐⭐ 进阶
前置知识:第 1.3 篇 @State 与状态管理
涉及源文件:products/default/src/main/ets/services/WorkRepository.ets、products/default/src/main/ets/creationformability/CreationFormAbility.ets

概述
在"画伴梦工厂"中,用户通过 AI 生成视频作品后,这些作品需要被持久保存,以便用户随时回顾和展示。HarmonyOS 提供了多种数据持久化方案,其中最轻量、最便捷的就是 preferences(首选项) API。
preferences 是鸿蒙提供的一种轻量级键值(Key-Value)数据库,适用于存储配置信息、用户偏好、小型业务数据等场景。它的核心特点包括:
- 同步 API:提供
getPreferencesSync、putSync、getSync等同步接口,调用简单直接 - 自动持久化:数据写入后自动落盘到应用的沙箱存储目录
- 键值存储:以键值对形式组织数据,适合小型结构化数据
- 应用级隔离:每个应用有独立的 Preferences 存储空间,互不干扰
本文将通过项目中的 WorkRepository 类,详细拆解 preferences API 的完整使用模式,涵盖 CRUD 操作、JSON 序列化、数据生命周期管理以及其他存储方案的对比。
一、Preferences 核心 API
1.1 获取 Preferences 实例
WorkRepository 中以静态私有方法封装了 Preferences 实例的获取逻辑:
import { common } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
const PREFERENCE_NAME: string = 'video_work_repository';
const WORKS_KEY: string = 'works';
export class WorkRepository {
private static getPreferences(): preferences.Preferences {
const context = getContext() as common.UIAbilityContext;
return preferences.getPreferencesSync(context, { name: PREFERENCE_NAME });
}
}
几个关键点:
| 要点 | 说明 |
|---|---|
| getPreferencesSync | 同步方法,传入 UIAbilityContext 和存储名称,返回 Preferences 实例 |
| PREFERENCE_NAME | 存储文件的名称(不含路径),系统会在应用的沙箱目录下创建同名文件 |
| getContext() | 获取当前组件的上下文,强制转换为 UIAbilityContext 类型 |
| 静态方法 | getPreferences 标记为 private static,仅内部调用,对外透明 |
为什么使用同步 API? 在应用启动或数据读取的关键路径上,同步调用可以简化代码流程。对于 Preferences 这种纯本地、毫秒级响应的存储,同步 API 完全够用,无需异步回调增加复杂度。
1.2 同步写入:putSync + flush
save 方法展示了完整的写入流程:
static save(work: VideoWork): void {
const works: VideoWork[] = WorkRepository.list();
const nextWorks: VideoWork[] = [work].concat(
works.filter((item: VideoWork): boolean => item.id !== work.id));
const store: preferences.Preferences = WorkRepository.getPreferences();
store.putSync(WORKS_KEY, JSON.stringify(nextWorks));
store.flush();
}
写入流程分为三步:
- 读取现有数据:
WorkRepository.list()获取当前所有作品列表 - 合并新数据:将新作品放在数组首位,并过滤掉同 ID 的旧数据(防重复)
- 序列化写入:
JSON.stringify将对象数组转为字符串,putSync写入存储 - 强制落盘:
flush()将内存中的数据立即写入磁盘
putSync(key, value) 的签名:
key:字符串类型,用于标识存储条目value:支持 string、number、boolean 及ValueType数组,不支持对象类型——所以复杂对象必须先用JSON.stringify序列化
flush() 的作用:
- 将缓存中的数据强制写入磁盘持久化
- 如果连续多次调用
putSync,可在最后一次调用后统一flush(),减少 I/O 次数 - 应用退出前未调用
flush()可能导致数据丢失
1.3 同步读取:getSync + 默认值
list 方法展示了带默认值的读取模式:
static list(): VideoWork[] {
const store: preferences.Preferences = WorkRepository.getPreferences();
const rawValue: string = store.getSync(WORKS_KEY, '[]') as string;
if (typeof rawValue !== 'string') {
return [];
}
try {
const parsed = JSON.parse(rawValue) as VideoWork[];
return parsed.sort(
(left: VideoWork, right: VideoWork): number => right.createdAt - left.createdAt
);
} catch {
return [];
}
}
getSync(key, defaultValue) 的核心特性:
- 第一参数 key:要读取的键名,即写入时使用的
WORKS_KEY - 第二参数 defaultValue:当键不存在时返回的默认值——这里传入
'[]',即使首次读取也不会报错 - 返回值类型:
getSync返回ValueType(可能是 string、number、boolean),需要as string断言后使用 - 类型防御:
typeof rawValue !== 'string'的类型检查防止脏数据导致运行时崩溃
防御式编程 在这段代码中体现得淋漓尽致:
getSync 读取 → 类型检查 → JSON.parse 解析 → try-catch 容错 → 排序后返回
每一层都做了异常兜底,确保存储的数据哪怕被手动篡改,也不会导致应用崩溃。
二、WorkRepository CRUD 设计
2.1 数据模型:VideoWork 接口
export interface VideoWork {
id: string;
title: string;
source: string;
prompt: string;
coverUri: string;
videoUri: string;
createdAt: number;
}
VideoWork 是存储的核心数据结构,涵盖了作品的所有关键信息:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
唯一标识,格式为 source + '-' + createdAt,如 photo-1718000000000 |
title |
string |
作品标题,由 prompt 自动生成 |
source |
string |
创作来源:photo(拍照识别)、doodle(自由涂鸦)、ai-chat(智能问答) |
prompt |
string |
生成时使用的提示词 |
coverUri |
string |
封面图片 URI |
videoUri |
string |
生成视频的 URI |
createdAt |
number |
创建时间戳(毫秒级),由 Date.now() 生成 |
2.2 查询全部:list()
static list(): VideoWork[] {
const store = WorkRepository.getPreferences();
const rawValue = store.getSync(WORKS_KEY, '[]') as string;
if (typeof rawValue !== 'string') return [];
try {
const parsed = JSON.parse(rawValue) as VideoWork[];
return parsed.sort((a, b) => b.createdAt - a.createdAt);
} catch { return []; }
}
list() 的排序逻辑值得注意:
- 使用
Array.prototype.sort按照createdAt降序排列 b.createdAt - a.createdAt确保最新作品在最前面- 这个排序发生在每一次
list()调用时,确保拿到的数据始终是最新在前
2.3 按 ID 查询:get()
static get(id: string): VideoWork | undefined {
const works = WorkRepository.list();
for (let i = 0; i < works.length; i++) {
if (works[i].id === id) return works[i];
}
return undefined;
}
get() 通过遍历数组查找匹配 ID 的作品。对于作品数量通常在几十到几百的场景下,线性遍历的性能完全足够。如果数据量级较大,可以考虑改为 Map 或对象索引结构。
2.4 创建新作品:createWork()
static createWork(source: string, prompt: string, coverUri: string, videoUri: string): VideoWork {
const createdAt = Date.now();
return {
id: source + '-' + createdAt.toString(),
title: WorkRepository.buildTitle(prompt, source),
source, prompt, coverUri, videoUri, createdAt
};
}
createWork 是一个工厂方法,负责创建新的 VideoWork 实例:
- 使用
source + '-' + createdAt生成唯一 ID——基于时间戳的 ID 天然有序且不冲突 - 通过
buildTitle方法自动生成标题,根据 prompt 和 source 智能选择显示文案
2.5 智能标题生成:buildTitle()
static buildTitle(prompt: string, source: string): string {
const trimmed = prompt.trim();
if (trimmed !== '') {
return trimmed.length > 14 ? trimmed.substring(0, 14) + '...' : trimmed;
}
if (source === 'photo') return '拍照识别动画';
if (source === 'doodle') return '自由涂鸦动画';
return '智能问答动画';
}
标题生成逻辑:
- 如果用户提供了 prompt,优先使用 prompt 作为标题(超过 14 字则截断加省略号)
- 如果 prompt 为空,根据创作来源分配默认标题
- 三种来源:
photo→ “拍照识别动画”、doodle→ “自由涂鸦动画”、其他 → “智能问答动画”
三、JSON 序列化与反序列化
由于 preferences 是键值存储,值类型限于基本类型和 ValueType 数组,无法直接存储对象。因此 VideoWork[] 的存储必须经过序列化转换:
写入时的序列化
store.putSync(WORKS_KEY, JSON.stringify(nextWorks));
JSON.stringify(nextWorks) 将 VideoWork[] 对象数组转换为 JSON 字符串:
- 输出格式:
[{"id":"photo-1718000000000","title":"...","source":"photo",...}] - 所有字段(包括
createdAt数值、uri字符串)都被正确编码
读取时的反序列化
const rawValue = store.getSync(WORKS_KEY, '[]') as string;
// ...
const parsed = JSON.parse(rawValue) as VideoWork[];
JSON.parse(rawValue) 将 JSON 字符串还原为 JavaScript 对象数组:
as VideoWork[]是 TypeScript 类型断言,编译期类型检查try-catch包裹避免非法 JSON 字符串导致解析异常
为什么选择 JSON 而非其他序列化方案?
| 方案 | 优点 | 缺点 |
|---|---|---|
| JSON.stringify/parse | 原生支持,零依赖,可读性强 | 不支持 Date、Map 等特殊类型 |
| protocol buffers | 体积小,解析快 | 需要定义 proto 文件,集成复杂 |
| protobufJSON 等 | 保留类型信息 | 需引入第三方库,增加包体积 |
对于存储 VideoWork[] 这种纯数据对象(只有 string、number、嵌套对象),JSON 序列化是最简洁、最安全的选择。
四、业务集成
4.1 RecognitionWaitingPage:AI 生成后自动保存
在 AI 编排流程的等待页面中,视频生成完成后会立即调用 WorkRepository 保存作品:
// RecognitionWaitingPage.ets - startGeneration 方法
const generatedVideo: GeneratedVideo = await AIGenerationService.generateVideo(
this.imageUri, this.prompt, (message: string) => {
this.statusText = message;
}
);
this.progress = 100;
this.activeStep = 3;
this.videoUri = generatedVideo.videoUri;
const finalCoverUri = this.coverUri !== '' ? this.coverUri : this.imageUri;
const work = WorkRepository.createWork(
this.workSource, generatedVideo.prompt, finalCoverUri, generatedVideo.videoUri
);
WorkRepository.save(work);
this.workId = work.id;
this.completed = true;
this.statusText = '视频已生成并保存到作品';
保存时序:
startGeneration()
│
├── AIGenerationService.generateVideo() ← AI 生成(异步)
│
├── WorkRepository.createWork() ← 构造作品对象
│
├── WorkRepository.save(work) ← 持久化写入
│
└── this.completed = true ← 更新 UI 状态
值得注意的是,保存操作没有使用 await——因为 save() 是同步方法,会在调用线程立即完成写入和落盘,无需等待异步回调。
4.2 CreationFormAbility:卡片展示最新作品
服务卡片 CreationFormAbility 也读取同一个 Preferences 存储,获取最新的作品标题用于卡片展示:
// CreationFormAbility.ets
private getLatestSavedWork(): VideoWork | undefined {
try {
const store = preferences.getPreferencesSync(this.context, { name: PREFERENCE_NAME });
const rawValue = store.getSync(WORKS_KEY, '[]') as string;
if (typeof rawValue !== 'string') {
return undefined;
}
const works = JSON.parse(rawValue) as VideoWork[];
if (works.length === 0) {
return undefined;
}
return works.sort(
(left: VideoWork, right: VideoWork): number => right.createdAt - left.createdAt
)[0];
} catch {
return undefined;
}
}
这段代码与 WorkRepository.list() 的核心逻辑完全一致:
- 使用
getPreferencesSync获取同名 Preferences 存储(PREFERENCE_NAME = 'video_work_repository') - 使用
getSync读取WORKS_KEY = 'works'键对应的 JSON 字符串 - 使用
JSON.parse反序列化为VideoWork[] - 按
createdAt降序排序,取第一个(最新)作品
共享存储的要点:CreationFormAbility 和 WorkRepository 虽然处于不同的上下文(一个在 Ability,一个在 UI 组件),但因为它们使用相同的 PREFERENCE_NAME,所以读写的是同一个存储文件,数据天然互通。
4.3 Index.ets:作品列表刷新
在首页的 WorksPage 中,通过 refreshWorks 方法从 WorkRepository 拉取最新数据:
// Index.ets
private refreshWorks() {
this.savedWorks = WorkRepository.list();
if (this.viewingSavedWork && this.selectedWorkIndex >= this.savedWorks.length) {
this.selectedWorkIndex = 0;
}
}
每次进入作品页或点击刷新时调用,WorkRepository.list() 返回经过排序的最新作品列表,展示在作品的 Grid 布局中。
4.4 完整的生命周期闭环
用户操作 → AI 生成 → WorkRepository.save() → preferences.putSync + flush
↓
磁盘持久化
↓
CreationFormAbility.getLatestSavedWork() → preferences.getSync → JSON.parse
↓
卡片显示最新作品标题
↓
Index.ets.refreshWorks() → WorkRepository.list() → JSON.parse
↓
作品列表页 UI 刷新
这是一个典型的写-存-读闭环:数据从用户操作产生,经过持久化写入磁盘,再被不同的消费方(服务卡片、作品列表页)读取展示。
五、与其他存储方案对比
HarmonyOS 提供了多种数据持久化方案,开发者应根据业务场景选择最合适的方案:
| 特性 | preferences | KVStore(分布式 KV) | 关系型数据库(RDB) | 文件存储(fileIo) |
|---|---|---|---|---|
| 数据类型 | 键值对(基本类型 + JSON 字符串) | 键值对(Uint8Array) | 结构化表(SQL) | 二进制/文本文件 |
| 同步/异步 | 同步 | 异步 | 异步 | 同步 + 异步 |
| 查询能力 | 仅按 key 查询 | 仅按 key 查询 | SQL 查询(WHERE、ORDER BY、JOIN) | 无索引查询 |
| 跨设备同步 | ❌ 不支持 | ✅ 分布式同步 | ✅ 分布式 RDB | ❌ 不支持 |
| 数据量 | 小(< 1MB) | 中(< 100MB) | 大(GB 级) | 大(无限制) |
| 适用场景 | 配置项、偏好设置、小型列表 | 跨设备共享配置 | 结构化业务数据、历史记录 | 图片、视频、日志文件 |
| API 复杂度 | ⭐ 低 | ⭐⭐ 中 | ⭐⭐⭐ 高 | ⭐⭐ 中 |
| 典型操作 | getSync / putSync / flush |
put / get / delete |
executeSql / querySql |
fileIo.readText / writeText |
为什么本项目中选择 preferences?
- 数据量小:用户的已保存作品通常在几十到几百条,每条数据为小型 JSON 对象,总大小远小于 1MB
- 数据结构简单:
VideoWork[]是平面数组,无需复杂的 SQL 查询或关联操作 - 同步 API 简洁:
WorkRepository是纯数据访问层,同步方法让调用方无需关心异步状态 - 无需跨设备:当前版本的作品管理不涉及跨设备同步,无需引入分布式 KV Store
何时应该换用其他方案?
- 作品数量超 5000 条:建议换用 RDB,利用 SQL 的 LIMIT 分页和索引查询
- 需要跨设备同步:切换为 KVStore,利用分布式数据库自动同步
- 作品包含大文件:视频文件应单独用 fileIo 存储,数据库中只保留 URI 路径引用
- 需要复杂条件过滤:如按来源、日期范围筛选,RDB 的 SQL 查询更加方便
六、最佳实践总结
6.1 存储命名规范
const PREFERENCE_NAME: string = 'video_work_repository';
const WORKS_KEY: string = 'works';
- PREFERENCE_NAME 使用有业务意义的名称,避免使用默认名
default_preferences - KEY 命名统一管理,集中定义为常量,避免字符串散落在代码各处
6.2 数据不可变性
在 save 方法中,每次写入都是全量替换而非局部更新:
const nextWorks: VideoWork[] = [work].concat(
works.filter((item: VideoWork): boolean => item.id !== work.id));
store.putSync(WORKS_KEY, JSON.stringify(nextWorks));
这种模式的好处是:
- 写入时始终保持数据结构一致
- 读取时不需要处理增量合并
- 防止重复数据积累(通过 ID 去重)
6.3 异常安全
从 getSync 到 JSON.parse,每一层都有异常保护:
- getSync 的默认值:避免首次读取时返回
undefined - 类型守卫:
typeof rawValue !== 'string'防止非字符串数据导致后续操作失败 - try-catch 包裹 JSON.parse:防止非法 JSON 字符串导致崩溃
- catch 为空数组:保证调用方总能拿到合法的数组,无需额外判空
6.4 数据变更的时效性
由于 list() 每次调用都会重新读取存储并排序,所以写操作立即可见——无需手动刷新缓存。这种"每次读取都从磁盘拉取"的设计虽然在极端高频调用下可能影响性能,但对于作品管理这种低频操作来说,是最简单且最可靠的方案。
总结
本文从"画伴梦工厂"的 WorkRepository 出发,完整剖析了 HarmonyOS preferences API 在作品管理中的全链路实践:
| 知识点 | 实现方式 |
|---|---|
| 获取实例 | preferences.getPreferencesSync(context, { name }) |
| 同步写入 | store.putSync(key, value) + store.flush() |
| 同步读取 | store.getSync(key, defaultValue) + as string 类型断言 |
| 对象序列化 | JSON.stringify(写入时)/ JSON.parse(读取时) |
| CRUD 封装 | save / list / get / createWork 四件套 |
| 自动排序 | list() 返回时按 createdAt 降序(新在前) |
| 业务集成 | RecognitionWaitingPage 保存 → CreationFormAbility 读取 → Index.ets 展示 |
| 防御式编程 | 类型检查 + try-catch + 默认值 + 降级处理 |
鸿蒙的 preferences API 以简单、可靠、同步的特点,完美契合了"画伴梦工厂"作品管理的数据持久化需求。对于小型结构化数据的存储场景,preferences 是最便捷的选择;而当数据规模增长或需要跨设备同步时,可以平滑迁移到 KVStore 或 RDB 等更强大的存储方案。
参考源码
本文所有代码均来自项目文件:
products/default/src/main/ets/services/WorkRepository.ets— 作品数据持久化封装,preferences CRUD 完整实现products/default/src/main/ets/creationformability/CreationFormAbility.ets— 服务卡片中跨上下文读取同一 Preferences 存储products/default/src/main/ets/pages/RecognitionWaitingPage.ets— AI 生成完成后自动保存作品的业务场景products/default/src/main/ets/pages/Index.ets— 通过WorkRepository.list()刷新作品列表的 UI 消费方
更多推荐



所有评论(0)