HarmonyOS 本地持久化实战Preferences、schema version 与空状态初始化这个项目是一个桌面卡片工具用户的卡片、收藏、回收站、提醒、主题、样式、备份信息和桌面 Form 选择都需要保存到本地。项目没有引入复杂数据库而是使用 HarmonyOSpreferences做轻量持久化。这篇讲AppDataService.ets里的持久化设计重点是 schema version、空状态初始化、快照备份和页面刷新。状态边界所有业务状态收进一个 snapshot共享模型中定义了完整状态export interface AppStateSnapshotModel { profile: UserProfileModel; cards: CardRecordModel[]; recycleBin: CardRecordModel[]; desktopCardId: string; desktopFormIds: string[]; reminder: ReminderSettingsModel; selectedThemeId: string; selectedStyleId: string; backupMeta: BackupMetaModel; activityLog: ActivityRecordModel[]; }这不是唯一选择但对一个轻量工具应用很合适。原因是状态量不大。大部分写操作都需要刷新多个摘要。备份和恢复可以直接处理整份 JSON。桌面 Form 只需要读取其中一部分。Preferences key 设计AppDataService.ets中集中定义 keyconst PREFS_NAME project028_card_tool; const STATE_KEY app_state_v1; const STATE_SCHEMA_VERSION_KEY app_state_schema_version; const STATE_SCHEMA_VERSION 2; const LAST_BACKUP_KEY last_backup_snapshot_v1; const SYSTEM_BACKUP_FILE project028-backup.json;这里有一个细节STATE_KEY仍然叫app_state_v1但真正控制迁移的是STATE_SCHEMA_VERSION。这样做可以避免 key 名频繁变化同时保留清晰的版本边界。初始化时先判断版本项目的发布态策略是“无版本即空状态”。如果本地没有 schema version或者版本不匹配就不继续沿用旧 demo 数据而是清空为正式发布态的空状态。伪代码可以理解为this.preferences preferences.getPreferencesSync(context, { name: PREFS_NAME }); const storedVersion this.preferences.getSync(STATE_SCHEMA_VERSION_KEY, ) as string; if (storedVersion ! STATE_SCHEMA_VERSION) { this.state this.createDefaultState(); this.persistState(); return; } const rawState this.preferences.getSync(STATE_KEY, ) as string; this.state this.normalizeState(JSON.parse(rawState));这个策略适合上架前从 demo 数据切到真实用户状态。否则用户第一次打开应用可能会看到开发阶段预置的样例卡片。createDefaultState 不等于塞满示例数据项目早期为了展示页面效果会构造一些默认卡片。后面发布态改成真实用户数据默认空首页和分类页通过内置模板目录撑起首屏。这意味着cards可以为空。recycleBin可以为空。backupMeta是空备份信息。首页推荐和分类概览不依赖真实用户数据。这样既能保证首屏完整又不会把 demo 数据当成用户数据持久化。normalizeState让旧数据不直接污染页面持久化数据来自本地文件必须防御字段缺失、类型变化和旧版本状态。项目用normalizeState()、normalizeCard()、normalizeReminder()等方法兜底。例如草稿和卡片保存时页面只提交当前字段服务层负责补齐private normalizeDraft(draft: CardDraftModel): CardDraftModel { return { id: draft.id, templateId: draft.templateId, title: draft.title.trim(), subtitle: draft.subtitle.trim(), detail: draft.detail.trim(), value: draft.value.trim(), footer: draft.footer.trim(), badge: draft.badge.trim(), tone: this.normalizeTone(draft.tone, brand), categoryId: this.normalizeCategory(draft.categoryId, tool), favorite: draft.favorite }; }这类 normalize 不只是为了“代码好看”而是为了避免页面直接拿到非法状态。每次写操作都记录 activity项目有统计页所以创建、编辑、归档、恢复、备份、主题切换都会写活动日志private recordActivity(type: ActivityType, title: string, weight: number, date: Date): void { const record: ActivityRecordModel { id: this.generateId(activity), type: type, title: title, createdAt: formatDateTime(date), dayKey: createDateKey(date), weight: Math.max(1, weight) }; this.state.activityLog.unshift(record); if (this.state.activityLog.length 60) { this.state.activityLog this.state.activityLog.slice(0, 60); } }这里限制最多 60 条是为了让轻量 Preferences 不无限增长。统计页需要的是趋势不是完整审计日志。持久化写入putSync flushSync服务层的写入方式是同步写入this.preferences.putSync(STATE_SCHEMA_VERSION_KEY, STATE_SCHEMA_VERSION); this.preferences.putSync(STATE_KEY, JSON.stringify(this.state)); this.preferences.flushSync();对这个项目来说同步写更直接写操作来自按钮点击数据量小页面需要立刻刷新摘要。如果是大量数据或高频写入就需要改成异步和节流。备份快照复用同一份状态本地“立即备份”不是另起一套数据结构而是导出当前 snapshotprivate exportSnapshot(): string { return JSON.stringify(this.state); }恢复时再走importSnapshot()和normalizeState()。这样备份恢复和版本兼容可以共享一套逻辑。页面刷新时机各页面常见写法是aboutToAppear(): void { this.refreshData(); } onPageShow(): void { this.refreshData(); }这是因为卡片可能从详情页、编辑页、管理页、备份页等不同入口被修改。页面重新显示时从服务层取最新视图模型比跨页传一堆状态更稳。常见坑不要把 demo 数据直接当默认用户状态。不要只在STATE_KEY上做版本判断最好单独存 schema version。不要让页面直接修改全局 state写操作集中到服务层。恢复备份后要重新计算摘要、统计和桌面卡片数据。旧数据字段缺失时必须 normalize不能直接断言类型正确。小结这个项目的本地持久化思路可以概括为一句话Preferences保存整份轻量状态schema version 控制迁移边界服务层统一 normalize 和写入。对卡片工具这类应用来说这种方案足够简单也足够稳定。真正要注意的不是 API 调用而是“默认状态、旧数据、页面刷新、备份恢复”这四件事是否在同一套规则里。
HarmonyOS 本地持久化实战:Preferences、schema version 与空状态初始化
发布时间:2026/7/2 5:59:42
HarmonyOS 本地持久化实战Preferences、schema version 与空状态初始化这个项目是一个桌面卡片工具用户的卡片、收藏、回收站、提醒、主题、样式、备份信息和桌面 Form 选择都需要保存到本地。项目没有引入复杂数据库而是使用 HarmonyOSpreferences做轻量持久化。这篇讲AppDataService.ets里的持久化设计重点是 schema version、空状态初始化、快照备份和页面刷新。状态边界所有业务状态收进一个 snapshot共享模型中定义了完整状态export interface AppStateSnapshotModel { profile: UserProfileModel; cards: CardRecordModel[]; recycleBin: CardRecordModel[]; desktopCardId: string; desktopFormIds: string[]; reminder: ReminderSettingsModel; selectedThemeId: string; selectedStyleId: string; backupMeta: BackupMetaModel; activityLog: ActivityRecordModel[]; }这不是唯一选择但对一个轻量工具应用很合适。原因是状态量不大。大部分写操作都需要刷新多个摘要。备份和恢复可以直接处理整份 JSON。桌面 Form 只需要读取其中一部分。Preferences key 设计AppDataService.ets中集中定义 keyconst PREFS_NAME project028_card_tool; const STATE_KEY app_state_v1; const STATE_SCHEMA_VERSION_KEY app_state_schema_version; const STATE_SCHEMA_VERSION 2; const LAST_BACKUP_KEY last_backup_snapshot_v1; const SYSTEM_BACKUP_FILE project028-backup.json;这里有一个细节STATE_KEY仍然叫app_state_v1但真正控制迁移的是STATE_SCHEMA_VERSION。这样做可以避免 key 名频繁变化同时保留清晰的版本边界。初始化时先判断版本项目的发布态策略是“无版本即空状态”。如果本地没有 schema version或者版本不匹配就不继续沿用旧 demo 数据而是清空为正式发布态的空状态。伪代码可以理解为this.preferences preferences.getPreferencesSync(context, { name: PREFS_NAME }); const storedVersion this.preferences.getSync(STATE_SCHEMA_VERSION_KEY, ) as string; if (storedVersion ! STATE_SCHEMA_VERSION) { this.state this.createDefaultState(); this.persistState(); return; } const rawState this.preferences.getSync(STATE_KEY, ) as string; this.state this.normalizeState(JSON.parse(rawState));这个策略适合上架前从 demo 数据切到真实用户状态。否则用户第一次打开应用可能会看到开发阶段预置的样例卡片。createDefaultState 不等于塞满示例数据项目早期为了展示页面效果会构造一些默认卡片。后面发布态改成真实用户数据默认空首页和分类页通过内置模板目录撑起首屏。这意味着cards可以为空。recycleBin可以为空。backupMeta是空备份信息。首页推荐和分类概览不依赖真实用户数据。这样既能保证首屏完整又不会把 demo 数据当成用户数据持久化。normalizeState让旧数据不直接污染页面持久化数据来自本地文件必须防御字段缺失、类型变化和旧版本状态。项目用normalizeState()、normalizeCard()、normalizeReminder()等方法兜底。例如草稿和卡片保存时页面只提交当前字段服务层负责补齐private normalizeDraft(draft: CardDraftModel): CardDraftModel { return { id: draft.id, templateId: draft.templateId, title: draft.title.trim(), subtitle: draft.subtitle.trim(), detail: draft.detail.trim(), value: draft.value.trim(), footer: draft.footer.trim(), badge: draft.badge.trim(), tone: this.normalizeTone(draft.tone, brand), categoryId: this.normalizeCategory(draft.categoryId, tool), favorite: draft.favorite }; }这类 normalize 不只是为了“代码好看”而是为了避免页面直接拿到非法状态。每次写操作都记录 activity项目有统计页所以创建、编辑、归档、恢复、备份、主题切换都会写活动日志private recordActivity(type: ActivityType, title: string, weight: number, date: Date): void { const record: ActivityRecordModel { id: this.generateId(activity), type: type, title: title, createdAt: formatDateTime(date), dayKey: createDateKey(date), weight: Math.max(1, weight) }; this.state.activityLog.unshift(record); if (this.state.activityLog.length 60) { this.state.activityLog this.state.activityLog.slice(0, 60); } }这里限制最多 60 条是为了让轻量 Preferences 不无限增长。统计页需要的是趋势不是完整审计日志。持久化写入putSync flushSync服务层的写入方式是同步写入this.preferences.putSync(STATE_SCHEMA_VERSION_KEY, STATE_SCHEMA_VERSION); this.preferences.putSync(STATE_KEY, JSON.stringify(this.state)); this.preferences.flushSync();对这个项目来说同步写更直接写操作来自按钮点击数据量小页面需要立刻刷新摘要。如果是大量数据或高频写入就需要改成异步和节流。备份快照复用同一份状态本地“立即备份”不是另起一套数据结构而是导出当前 snapshotprivate exportSnapshot(): string { return JSON.stringify(this.state); }恢复时再走importSnapshot()和normalizeState()。这样备份恢复和版本兼容可以共享一套逻辑。页面刷新时机各页面常见写法是aboutToAppear(): void { this.refreshData(); } onPageShow(): void { this.refreshData(); }这是因为卡片可能从详情页、编辑页、管理页、备份页等不同入口被修改。页面重新显示时从服务层取最新视图模型比跨页传一堆状态更稳。常见坑不要把 demo 数据直接当默认用户状态。不要只在STATE_KEY上做版本判断最好单独存 schema version。不要让页面直接修改全局 state写操作集中到服务层。恢复备份后要重新计算摘要、统计和桌面卡片数据。旧数据字段缺失时必须 normalize不能直接断言类型正确。小结这个项目的本地持久化思路可以概括为一句话Preferences保存整份轻量状态schema version 控制迁移边界服务层统一 normalize 和写入。对卡片工具这类应用来说这种方案足够简单也足够稳定。真正要注意的不是 API 调用而是“默认状态、旧数据、页面刷新、备份恢复”这四件事是否在同一套规则里。