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(头像/昵称)

#mermaid-svg-7IKKLMQUOvkqzgWj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7IKKLMQUOvkqzgWj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7IKKLMQUOvkqzgWj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7IKKLMQUOvkqzgWj .error-icon{fill:#552222;}#mermaid-svg-7IKKLMQUOvkqzgWj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7IKKLMQUOvkqzgWj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7IKKLMQUOvkqzgWj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7IKKLMQUOvkqzgWj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7IKKLMQUOvkqzgWj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7IKKLMQUOvkqzgWj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7IKKLMQUOvkqzgWj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7IKKLMQUOvkqzgWj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7IKKLMQUOvkqzgWj .marker.cross{stroke:#333333;}#mermaid-svg-7IKKLMQUOvkqzgWj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7IKKLMQUOvkqzgWj p{margin:0;}#mermaid-svg-7IKKLMQUOvkqzgWj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7IKKLMQUOvkqzgWj .cluster-label text{fill:#333;}#mermaid-svg-7IKKLMQUOvkqzgWj .cluster-label span{color:#333;}#mermaid-svg-7IKKLMQUOvkqzgWj .cluster-label span p{background-color:transparent;}#mermaid-svg-7IKKLMQUOvkqzgWj .label text,#mermaid-svg-7IKKLMQUOvkqzgWj span{fill:#333;color:#333;}#mermaid-svg-7IKKLMQUOvkqzgWj .node rect,#mermaid-svg-7IKKLMQUOvkqzgWj .node circle,#mermaid-svg-7IKKLMQUOvkqzgWj .node ellipse,#mermaid-svg-7IKKLMQUOvkqzgWj .node polygon,#mermaid-svg-7IKKLMQUOvkqzgWj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7IKKLMQUOvkqzgWj .rough-node .label text,#mermaid-svg-7IKKLMQUOvkqzgWj .node .label text,#mermaid-svg-7IKKLMQUOvkqzgWj .image-shape .label,#mermaid-svg-7IKKLMQUOvkqzgWj .icon-shape .label{text-anchor:middle;}#mermaid-svg-7IKKLMQUOvkqzgWj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7IKKLMQUOvkqzgWj .rough-node .label,#mermaid-svg-7IKKLMQUOvkqzgWj .node .label,#mermaid-svg-7IKKLMQUOvkqzgWj .image-shape .label,#mermaid-svg-7IKKLMQUOvkqzgWj .icon-shape .label{text-align:center;}#mermaid-svg-7IKKLMQUOvkqzgWj .node.clickable{cursor:pointer;}#mermaid-svg-7IKKLMQUOvkqzgWj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7IKKLMQUOvkqzgWj .arrowheadPath{fill:#333333;}#mermaid-svg-7IKKLMQUOvkqzgWj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7IKKLMQUOvkqzgWj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7IKKLMQUOvkqzgWj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7IKKLMQUOvkqzgWj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7IKKLMQUOvkqzgWj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7IKKLMQUOvkqzgWj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7IKKLMQUOvkqzgWj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7IKKLMQUOvkqzgWj .cluster text{fill:#333;}#mermaid-svg-7IKKLMQUOvkqzgWj .cluster span{color:#333;}#mermaid-svg-7IKKLMQUOvkqzgWj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7IKKLMQUOvkqzgWj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7IKKLMQUOvkqzgWj rect.text{fill:none;stroke-width:0;}#mermaid-svg-7IKKLMQUOvkqzgWj .icon-shape,#mermaid-svg-7IKKLMQUOvkqzgWj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7IKKLMQUOvkqzgWj .icon-shape p,#mermaid-svg-7IKKLMQUOvkqzgWj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7IKKLMQUOvkqzgWj .icon-shape .label rect,#mermaid-svg-7IKKLMQUOvkqzgWj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7IKKLMQUOvkqzgWj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7IKKLMQUOvkqzgWj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7IKKLMQUOvkqzgWj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🖥️ UI 层
🧠 内存层
💾 持久化层
initLocalAuth() 加载
updateProfile() 写入
直接调用
写入
读取
读取(修复前:未加载)
间接依赖
修复前:从未同步
RelationalStore

local_users 表
Preferences

profile_store
AuthViewModel

@Trace 属性
ProfileViewModel

healthProfile + preference
ProfileEditPage

头像/昵称编辑
ProfileTabContent

档案 + 偏好展示
HealthTabContent

营养数据展示

图一解读 :问题就出在 ProfileViewModelAuthViewModel 之间的那条虚线------它们各自维护了一套"用户数据",但从未同步。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 HealthDashboardViewModelauthViewModel.toUserHealthProfile() 读取 ✅ 正确(修复后)

三个页面对同一份用户数据,有三条不同的读取路径。其中两条路径经过 AuthViewModel(正确),一条路径经过 ProfileViewModel(错误)。这就是为什么编辑页和健康页正常,但"我的"Tab 异常的原因。


四、修复方案:三重保障确保数据一致

修复 1:ProfileViewModel 新增 loadFromAuthViewModel() 方法

ProfileViewModel 在初始化时从 AuthViewModel 加载数据:

typescript 复制代码
// 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 同步数据');
}

关键设计 :使用 makeHealthProfilemakeUserPreference 工厂函数创建全新对象引用 ------这是 @ObservedV2 检测变化并触发 UI 刷新的必要条件。

修复 2:save() 中同步更新 AuthViewModel

ProfileViewModel.save() 中,确保数据库写入成功后立即同步更新 AuthViewModel 的内存状态:

typescript 复制代码
// 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()

typescript 复制代码
// MainContainer.ets → ProfileTabContent.aboutToAppear 新增一行
async aboutToAppear(): Promise<void> {
  const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
  // ... 加载头像和昵称 ...
  
  // ★ 关键修复:从 authViewModel 加载健康档案和偏好设置
  this.vm.loadFromAuthViewModel();
  
  // ... 监听事件 ...
}

五、修复后的完整数据流

SQLite AuthViewModel ProfileViewModel ProfileTabContent 👤 用户 SQLite AuthViewModel ProfileViewModel ProfileTabContent 👤 用户 #mermaid-svg-4IpCUbCDubAQG6LU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4IpCUbCDubAQG6LU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4IpCUbCDubAQG6LU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4IpCUbCDubAQG6LU .error-icon{fill:#552222;}#mermaid-svg-4IpCUbCDubAQG6LU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4IpCUbCDubAQG6LU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4IpCUbCDubAQG6LU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4IpCUbCDubAQG6LU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4IpCUbCDubAQG6LU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4IpCUbCDubAQG6LU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4IpCUbCDubAQG6LU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4IpCUbCDubAQG6LU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4IpCUbCDubAQG6LU .marker.cross{stroke:#333333;}#mermaid-svg-4IpCUbCDubAQG6LU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4IpCUbCDubAQG6LU p{margin:0;}#mermaid-svg-4IpCUbCDubAQG6LU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4IpCUbCDubAQG6LU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-4IpCUbCDubAQG6LU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4IpCUbCDubAQG6LU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-4IpCUbCDubAQG6LU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-4IpCUbCDubAQG6LU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-4IpCUbCDubAQG6LU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-4IpCUbCDubAQG6LU .sequenceNumber{fill:white;}#mermaid-svg-4IpCUbCDubAQG6LU #sequencenumber{fill:#333;}#mermaid-svg-4IpCUbCDubAQG6LU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-4IpCUbCDubAQG6LU .messageText{fill:#333;stroke:none;}#mermaid-svg-4IpCUbCDubAQG6LU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4IpCUbCDubAQG6LU .labelText,#mermaid-svg-4IpCUbCDubAQG6LU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-4IpCUbCDubAQG6LU .loopText,#mermaid-svg-4IpCUbCDubAQG6LU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-4IpCUbCDubAQG6LU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4IpCUbCDubAQG6LU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4IpCUbCDubAQG6LU .noteText,#mermaid-svg-4IpCUbCDubAQG6LU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-4IpCUbCDubAQG6LU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4IpCUbCDubAQG6LU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4IpCUbCDubAQG6LU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4IpCUbCDubAQG6LU .actorPopupMenu{position:absolute;}#mermaid-svg-4IpCUbCDubAQG6LU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-4IpCUbCDubAQG6LU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4IpCUbCDubAQG6LU .actor-man circle,#mermaid-svg-4IpCUbCDubAQG6LU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-4IpCUbCDubAQG6LU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} === 冷启动 === === 用户修改 === @Trace 触发 UI 刷新 === 再次冷启动 === initLocalAuth() 加载 最新数据 (height=175) loadFromAuthViewModel() 读取 @Trace 属性 height=175 ✅ UI 显示 175cm ✅ 修改身高为 180cm updateHeight(180) autoSave() updateProfile({height:180}) UPDATE local_users SET height=180 同步内存状态 (height=180) UI 显示 180cm ✅ initLocalAuth() 加载 height=180 loadFromAuthViewModel() height=180 ✅ UI 显示 180cm ✅

图二解读 :修复后的数据流实现了三重保障------冷启动时从 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() 方法 避免魔法字符串散落各处,集中管理转换逻辑

八、验证方法

修复后,按以下步骤验证:

  1. 在"我的"Tab 修改身高为 175cm,年龄为 36 岁
  2. 切换到健康 Tab,确认营养数据按新值计算
  3. 完全关掉 App(从最近任务中划掉)
  4. 重新打开 App,切换到"我的"Tab
  5. 确认身高显示 175cm,年龄显示 36 岁(而非默认的 170/30)

九、本阶段总结

这次修复的本质是统一数据源 。系统中的用户数据原本存在"三份事实版本"------数据库、AuthViewModel、ProfileViewModel。修复前,只有前两者在冷启动时同步,ProfileViewModel 是数据孤岛。修复后,ProfileViewModel 通过 loadFromAuthViewModel() 成为 AuthViewModel 的"镜像",不再独立维护数据。

核心收获:

  • 单一数据源原则:同一份数据只在一个地方维护,其他地方通过同步获取
  • 内存同步不可省略:数据库持久化是异步的,内存同步填补了延迟窗口
  • 冷启动恢复测试:每次修改数据持久化逻辑后,必须测试"杀进程→重启→读取"的完整链路

📚 本系列持续更新中:下一篇将进入更深入的系统能力探索。

🔗 专栏入口《HarmonyOS6.1全场景实战》合集

📦 获取基线版本源码包包括第1-15篇所有代码 + 架构文档 + Flask 后端

**如果你发现本文还有任何不严谨之处,欢迎随时指出,我们一起共建最优质的 HarmonyOS 6.1 学习内容!如果觉得有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬!

纯血鸿蒙,用心造厨。我们下一篇见!

相关推荐
若兰幽竹1 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(三十):【社区分享】本地社区功能——让菜谱从“独享”走向“共享”
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹1 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十九):【偏好持久化】偏好设置与推荐引擎联动——让 App 越用越“懂你”
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹4 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十七):告别 UI 冻结——使用 TaskPool 实现高性能并发图像分析
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹4 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十八):【数据持久化】收藏与浏览历史——让数据在 App 重启后依然“活着”
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹5 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十六):【响应式布局】折叠屏与平板完美适配——一套代码,多端呈现
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹6 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十五):【深色模式】一键切换暗色主题——让 App 在深夜也温柔
华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹7 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十二) | 多媒体 | AVPlayer嵌入教学视频——让智慧屏真正“活”起来
音视频·华为鸿蒙系统·harmonyos6.1.0·灵犀厨房·harmonyos6.1
若兰幽竹7 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十三):【交互动效】转场、列表动画与趣味反馈——让每一次点击都有温度
交互·华为鸿蒙系统·harmonyos6.1
若兰幽竹7 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十一):【服务卡片】在桌面查看烹饪进度——主进程强推与跨进程桥接
服务卡片·华为鸿蒙系统·灵犀厨房·harmonyos6.1