第4.8篇:数据持久化——preferences 存储与作品管理

难度:⭐⭐ 进阶
前置知识:第 1.3 篇 @State 与状态管理
涉及源文件products/default/src/main/ets/services/WorkRepository.etsproducts/default/src/main/ets/creationformability/CreationFormAbility.ets


在这里插入图片描述

概述

在"画伴梦工厂"中,用户通过 AI 生成视频作品后,这些作品需要被持久保存,以便用户随时回顾和展示。HarmonyOS 提供了多种数据持久化方案,其中最轻量、最便捷的就是 preferences(首选项) API。

preferences 是鸿蒙提供的一种轻量级键值(Key-Value)数据库,适用于存储配置信息、用户偏好、小型业务数据等场景。它的核心特点包括:

  • 同步 API:提供 getPreferencesSyncputSyncgetSync 等同步接口,调用简单直接
  • 自动持久化:数据写入后自动落盘到应用的沙箱存储目录
  • 键值存储:以键值对形式组织数据,适合小型结构化数据
  • 应用级隔离:每个应用有独立的 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();
}

写入流程分为三步:

  1. 读取现有数据WorkRepository.list() 获取当前所有作品列表
  2. 合并新数据:将新作品放在数组首位,并过滤掉同 ID 的旧数据(防重复)
  3. 序列化写入JSON.stringify 将对象数组转为字符串,putSync 写入存储
  4. 强制落盘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 '智能问答动画';
}

标题生成逻辑:

  1. 如果用户提供了 prompt,优先使用 prompt 作为标题(超过 14 字则截断加省略号)
  2. 如果 prompt 为空,根据创作来源分配默认标题
  3. 三种来源: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() 的核心逻辑完全一致:

  1. 使用 getPreferencesSync 获取同名 Preferences 存储(PREFERENCE_NAME = 'video_work_repository'
  2. 使用 getSync 读取 WORKS_KEY = 'works' 键对应的 JSON 字符串
  3. 使用 JSON.parse 反序列化为 VideoWork[]
  4. createdAt 降序排序,取第一个(最新)作品

共享存储的要点CreationFormAbilityWorkRepository 虽然处于不同的上下文(一个在 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?

  1. 数据量小:用户的已保存作品通常在几十到几百条,每条数据为小型 JSON 对象,总大小远小于 1MB
  2. 数据结构简单VideoWork[] 是平面数组,无需复杂的 SQL 查询或关联操作
  3. 同步 API 简洁WorkRepository 是纯数据访问层,同步方法让调用方无需关心异步状态
  4. 无需跨设备:当前版本的作品管理不涉及跨设备同步,无需引入分布式 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 异常安全

getSyncJSON.parse,每一层都有异常保护:

  1. getSync 的默认值:避免首次读取时返回 undefined
  2. 类型守卫typeof rawValue !== 'string' 防止非字符串数据导致后续操作失败
  3. try-catch 包裹 JSON.parse:防止非法 JSON 字符串导致崩溃
  4. 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 消费方
Logo

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

更多推荐