HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(三十二):【数据一致性】个人档案的“三重持久化”修复——让偏好、健康与头像真正同步
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(三十二):【数据一致性】个人档案的“三重持久化”修复——让偏好、健康与头像真正同步
摘要:你的 App 在“我的”Tab 修改了身高体重,切换到健康 Tab 也确实刷新了营养数据。但当你完全关掉 App 再重启——所有档案恢复默认,头像回到灰色占位。你怀疑是 Preferences 没存上,或数据库没写进去。但真正的根源更隐蔽:数据不是没存,而是存错了地方——ProfileViewModel 和 AuthViewModel 各有一套独立的“事实版本”,ProfileViewModel 启动时从未从 AuthViewModel 加载数据,导致 UI 始终展示默认值。本文将复盘这场数据一致性追查的全过程:从数据源梳理到内存同步修复,再到冷启动恢复验证。最终,我们用“单一数据源 + 内存同步 + 持久化兜底”三重保障,彻底解决档案持久化问题。
一、引言:一个“幽灵”般的 Bug
在第 31 篇发布后,一个用户在测试群反馈了一个诡异的现象:
| 操作步骤 | 预期 | 实际 |
|---|---|---|
| 在“我的”Tab 修改身高为 175cm | 保存成功 | ✅ 保存成功 |
| 切换到健康 Tab | 营养数据按身高 175cm 重新计算 | ✅ 正确刷新 |
| 完全关掉 App 再重启 | 身高仍然是 175cm | ❌ 身高恢复默认 170cm |
| 进入编辑页查看头像 | 头像显示正常 | ✅ 正常 |
| 返回“我的”Tab | 头像显示正常 | ❌ 头像变成灰色默认图标 |
两个表面症状指向同一个底层问题:数据持久化了,但恢复时没读对地方。
二、数据源审计:同一个数据,三套“事实版本”
要理解这个 Bug,必须先理清系统中到底有多少套“用户数据”:
| 数据源 | 存储位置 | 读写接口 | 使用者 |
|---|---|---|---|
| RelationalStore | local_users 表 |
storeHelper.executeSql |
AuthViewModel |
| AuthViewModel | 内存(@Trace 属性) |
initLocalAuth() / updateProfile() |
ProfileEditPage |
| ProfileViewModel | 内存(@Trace 属性) |
healthProfile / preference |
ProfileTabContent |
| Preferences | 本地 KV 文件 | preferences.get/put |
ProfileTabContent(头像/昵称) |
图一解读:问题就出在 ProfileViewModel 和 AuthViewModel 之间的那条虚线——它们各自维护了一套“用户数据”,但从未同步。AuthViewModel 在冷启动时从数据库加载了最新数据,但 ProfileViewModel 始终使用自己的默认值。UI 从 ProfileViewModel 读取数据展示,自然看不到最新值。
三、根本原因:ProfileViewModel 的“数据孤岛”
3.1 问题链路推演
冷启动流程:
AuthViewModel.initLocalAuth()
→ 从 local_users 表加载最新数据
→ this.height = 175 ✅
→ this.age = 36 ✅
ProfileTabContent.aboutToAppear()
→ 创建 ProfileViewModel(默认值)
→ this.vm.healthProfile.height = 170 ❌
→ this.vm.healthProfile.age = 30 ❌
→ UI 展示 170cm / 30 岁 ❌
ProfileViewModel 在初始化时使用了 defaultHealthProfile,从未尝试从 AuthViewModel 或数据库中加载真实数据。它像一个“数据孤岛”——保存时能把数据写入数据库,但加载时完全忽略数据库中已有的数据。
3.2 为什么编辑页正常而展示页异常?
| 页面 | 数据来源 | 冷启动后 |
|---|---|---|
ProfileEditPage |
直接从 Preferences 读取 |
✅ 正确 |
ProfileTabContent |
从 ProfileViewModel 读取 |
❌ 显示默认值 |
HealthTabContent |
从 HealthDashboardViewModel → authViewModel.toUserHealthProfile() 读取 |
✅ 正确(修复后) |
三个页面对同一份用户数据,有三条不同的读取路径。其中两条路径经过 AuthViewModel(正确),一条路径经过 ProfileViewModel(错误)。这就是为什么编辑页和健康页正常,但“我的”Tab 异常的原因。
四、修复方案:三重保障确保数据一致
修复 1:ProfileViewModel 新增 loadFromAuthViewModel() 方法
让 ProfileViewModel 在初始化时从 AuthViewModel 加载数据:
// ProfileViewModel.ets(新增方法)
loadFromAuthViewModel(): void {
// 1. 从 authViewModel 同步健康档案
const gender = authViewModel.gender === 'female' ? Gender.FEMALE : Gender.MALE;
const activityLevel = this.parseActivityLevelFromString(authViewModel.activityLevel);
this.healthProfile = makeHealthProfile(
gender, authViewModel.age, authViewModel.height,
authViewModel.weight, activityLevel
);
// 2. 从 authViewModel 同步偏好设置
this.preference = makeUserPreference(
[...authViewModel.favoriteTags],
[...authViewModel.allergies],
authViewModel.maxCalories
);
console.info('[ProfileVM] 已从 AuthViewModel 同步数据');
}
关键设计:使用 makeHealthProfile 和 makeUserPreference 工厂函数创建全新对象引用——这是 @ObservedV2 检测变化并触发 UI 刷新的必要条件。
修复 2:save() 中同步更新 AuthViewModel
在 ProfileViewModel.save() 中,确保数据库写入成功后立即同步更新 AuthViewModel 的内存状态:
// ProfileViewModel.ets —— save() 方法修正部分
async save(): Promise<void> {
// ... 原有云端同步逻辑 ...
if (success) {
// ★ 关键修复:同步更新 authViewModel 内存状态
const genderStr = this.healthProfile.gender === Gender.MALE ? 'male' : 'female';
let activityLevelStr = 'light';
switch (this.healthProfile.activityLevel) {
case ActivityLevel.SEDENTARY: activityLevelStr = 'sedentary'; break;
case ActivityLevel.LIGHT: activityLevelStr = 'light'; break;
case ActivityLevel.MODERATE: activityLevelStr = 'moderate'; break;
case ActivityLevel.ACTIVE: activityLevelStr = 'active'; break;
case ActivityLevel.VERY_ACTIVE: activityLevelStr = 'veryActive'; break;
}
authViewModel.gender = genderStr;
authViewModel.age = this.healthProfile.age;
authViewModel.height = this.healthProfile.height;
authViewModel.weight = this.healthProfile.weight;
authViewModel.activityLevel = activityLevelStr;
authViewModel.favoriteTags = [...this.preference.favoriteTags];
authViewModel.allergies = [...this.preference.allergies];
authViewModel.maxCalories = this.preference.maxCalories;
}
}
设计考量:为什么需要在
save()中同步两次(数据库 + 内存)?因为数据库写入是异步的,如果其他组件(如HealthTabContent)在数据库写入完成前读取AuthViewModel,可能读到旧值。内存同步是即时生效的,填补了数据库写入的延迟窗口。
修复 3:ProfileTabContent 初始化时加载数据
在 ProfileTabContent.aboutToAppear 中调用 loadFromAuthViewModel():
// MainContainer.ets → ProfileTabContent.aboutToAppear 新增一行
async aboutToAppear(): Promise<void> {
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
// ... 加载头像和昵称 ...
// ★ 关键修复:从 authViewModel 加载健康档案和偏好设置
this.vm.loadFromAuthViewModel();
// ... 监听事件 ...
}
五、修复后的完整数据流
图二解读:修复后的数据流实现了三重保障——冷启动时从 AuthViewModel 加载(保障1)、保存时同步 AuthViewModel 内存(保障2)、数据库持久化(保障3)。无论 App 如何重启,UI 始终展示最新数据。
六、代码交付清单
| 文件 | 新增/修改 | 行数 | 说明 |
|---|---|---|---|
ProfileViewModel.ets |
新增 loadFromAuthViewModel() |
+40 | 从 AuthViewModel 同步健康档案和偏好 |
ProfileViewModel.ets |
修改 save() |
+15 | 保存后同步 AuthViewModel 内存状态 |
ProfileViewModel.ets |
新增 parseActivityLevelFromString() |
+10 | 活动等级字符串→枚举转换 |
MainContainer.ets |
修改 ProfileTabContent.aboutToAppear() |
+1 | 初始化时加载数据 |
七、设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| 数据加载时机 | aboutToAppear 中主动调用 |
每次组件出现时都重新同步,确保任何场景下数据最新 |
| 同步方式 | 对象引用替换(makeHealthProfile 等工厂函数) |
@ObservedV2 检测引用变化,而非属性变化 |
save() 中同步 AuthViewModel |
数据库写入后立即同步 | 填补异步写入的延迟窗口,其他组件读取 AuthViewModel 时数据已最新 |
| 活动等级转换 | 独立的 parseActivityLevelFromString() 方法 |
避免魔法字符串散落各处,集中管理转换逻辑 |
八、验证方法
修复后,按以下步骤验证:
- 在“我的”Tab 修改身高为 175cm,年龄为 36 岁
- 切换到健康 Tab,确认营养数据按新值计算
- 完全关掉 App(从最近任务中划掉)
- 重新打开 App,切换到“我的”Tab
- 确认身高显示 175cm,年龄显示 36 岁(而非默认的 170/30)
九、本阶段总结
这次修复的本质是统一数据源。系统中的用户数据原本存在“三份事实版本”——数据库、AuthViewModel、ProfileViewModel。修复前,只有前两者在冷启动时同步,ProfileViewModel 是数据孤岛。修复后,ProfileViewModel 通过 loadFromAuthViewModel() 成为 AuthViewModel 的“镜像”,不再独立维护数据。
核心收获:
- 单一数据源原则:同一份数据只在一个地方维护,其他地方通过同步获取
- 内存同步不可省略:数据库持久化是异步的,内存同步填补了延迟窗口
- 冷启动恢复测试:每次修改数据持久化逻辑后,必须测试“杀进程→重启→读取”的完整链路
📚 本系列持续更新中:下一篇将进入更深入的系统能力探索。
🔗 专栏入口:[《HarmonyOS6.1全场景实战》合集]
📦 获取基线版本源码包:包括第1-15篇所有代码 + 架构文档 + Flask 后端
**如果你发现本文还有任何不严谨之处,欢迎随时指出,我们一起共建最优质的 HarmonyOS 6.1 学习内容!如果觉得有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬!
纯血鸿蒙,用心造厨。我们下一篇见!
更多推荐

所有评论(0)