鸿蒙Widget开发实战:3张卡片实现桌面-App全链路同步
本文是「食刻 (ShiKe)」技术系列第 3 篇,深入解析 **全场景 Widget 协同** 的完整技术方案 —— 从跨进程数据同步到一键直达 App 的全链路实现。

本文是「食刻 (ShiKe)」技术系列第 3 篇,深入解析 全场景 Widget 协同 的完整技术方案 —— 从跨进程数据同步到一键直达 App 的全链路实现。
一、产品愿景:让记录触手可及
用户痛点
“我想记录午餐,但要经历:亮屏 → 找 App 图标 → 打开 → 等 loading → 点 + 号 → 输入食物”
—— 6 步操作,每多一步,漏记概率就增加 20%
我们的解法:3 张桌面卡片
食刻基于 HarmonyOS Form Kit 实现了 3 张服务卡片,覆盖"记录-查看-分析"全场景:

📝 快速记录卡(2×2)
- 🍳 早餐:已记/待记
- 🌤️ 午餐:已记/待记
- 🌙 晚餐:已记/待记
- 🍪 加餐:已记/待记
点击任意餐段 → 直接拉起 App 并打开 AI 记录面板
🔥 热量统计卡(2×2)
- ◯ 已摄入:1800 kcal
- ◯ 还可摄入:200 kcal
- 进度环实时更新
📊 营养详情卡(2×4)
- ◯ 热量:1800/2000
- 蛋白质:45g ████████░░
- 碳水:120g ████████████
- 脂肪:25g ████░░░░░░░
二、核心挑战:跨进程通信
架构前提
┌─ 主 App 进程 ──────────────────────┐
│ UIAbility + 13 个页面 │
│ DietViewModel + PreferencesUtil │
└──────────────┬──────────────────────┘
│ ???(如何通信)
┌─ FormExtension 进程 ───────────────┐
│ EntryFormAbility │
│ DietWidgetCard × 3 │
└────────────────────────────────────┘
关键问题:主 App 和 Widget 运行在不同进程中。
它们不能共享内存变量,不能直接调用方法。那么数据如何同步?
答案:ArkData Preferences 作为桥梁
主 App 进程 FormExtension 进程
│ │
├── 写入 Preferences │
│ (同一沙箱目录) │
│ ↕ │
│ ↕ (磁盘文件共享) │
│ ↕ │
│ ┌── 读取 Preferences
Preferences 是鸿蒙的本地 KV 存储,同一个 App 的 UIAbility 和 FormExtension 可以访问同一份 Preferences 数据。
这就是我们的跨进程通信方案。
三、数据同步完整链路
3.1 添加饮食记录时(App → Widget)
// DietViewModel.ets — 核心同步逻辑
async addRecord(record: DietRecord): Promise<void> {
// 1. 业务逻辑:保存记录
const todayData = this.calculateTodayData(record);
// 2. 持久化到磁盘(Preferences)
await PreferencesUtil.saveDailyData(todayKey, todayData);
// 3. 更新 App 内状态(UI 刷新)
AppStorage.setOrCreate('today_data', todayData);
// 4. ★ 推送数据到 Widget ★
this.notifyWidgets(todayData);
}
private async notifyWidgets(data: TodayData): Promise<void> {
// 4.1 读取所有已添加的 Widget ID
const formIds = await PreferencesUtil.getFormIds();
// 4.2 构造全量 formData(11 个字段)
const formData = {
totalKcal: data.totalKcal,
dailyLimit: data.dailyLimit,
proteinGrams: data.protein,
carbGrams: data.carbs,
fatGrams: data.fat,
mealCount: data.mealCount,
recordCount: data.recordCount,
hasBreakfast: data.hasBreakfast ? 1 : 0,
hasLunch: data.hasLunch ? 1 : 0,
hasDinner: data.hasDinner ? 0 : 0,
hasSnack: data.hasSnack ? 1 : 0,
updateTime: Date.now(),
formDimension: 2 // 2×2 or 2×4
};
// 4.3 推送到每个 Widget
for (const formId of formIds) {
const binding = formBindingData.createFormBindingData(formData);
await formProvider.updateForm(formId, binding);
}
}
3.2 Widget 刷新时(FormExtension 自身)
// EntryFormAbility.ets — Widget 生命周期管理
@Entry
@Component
struct EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData {
// 新增卡片时:初始化数据并保存 formId
const formData = this.buildFormData();
const formId = want.parameters?.['formId'] as string;
if (formId) {
PreferencesUtil.saveFormId(formId); // 持久化,用于后续推送
}
return formBindingData.createFormBindingData(formData);
}
onUpdateForm(formId: string): void {
// 系统请求刷新时:从 Preferences 读最新数据
const data = PreferencesUtil.getDailyData(this.getTodayKey());
const formData = this.buildFormData(data);
formProvider.updateForm(formId, formBindingData.createFormBindingData(formData));
}
onRemoveForm(formId: string): void {
// 卡片被删除时:清理 formId
PreferencesUtil.removeFormId(formId);
}
}
3.3 兜底机制(App 前后台切换)
// EntryAbility.ets — 兜底刷新
export default class EntryAbility extends UIAbility {
onBackground(): void {
// App 进入后台时:确保 Widget 数据是最新的
refreshWidgets();
}
onForeground(): void {
// App 回到前台时:再次刷新(防止后台期间数据变化)
refreshWidgets();
}
onNewWant(want: Want, launchParam: LaunchParam): void {
// ★ Widget 点击跳转时接收参数 ★
const mealType = want.parameters?.['mealType'] as string; // 'breakfast'/'lunch'...
if (mealType) {
// 将餐段信息存入全局状态
AppStorage.setOrCreate('target_meal_type', mealType);
}
}
}
四、一键直达:postCardAction 的魔法
需求
用户在桌面 Widget 上点击"午餐"按钮后:
- 打开 App ✅
- 自动弹出 AI 记录面板 ✅
- 自动选中"午餐"餐段 ✅
实现:两步跳转
Step 1 — Widget 端发送跳转请求
// AddDietWidgetCard.ets — 快速记录卡片
@Entry
@Component
struct AddDietWidgetCard {
build() {
Column() {
// 四个餐段按钮
ForEach(MEAL_TYPES, (meal: MealType) => {
Button(this.getMealLabel(meal))
.onClick(() => {
// ★ 关键:postCardAction 发送跳转 + 参数 ★
postCardAction({
action: 'router',
abilityName: 'EntryAbility',
params: { mealType: meal.type } // 携带餐段类型
});
})
})
}
}
}
Step 2 — App 端接收参数
// EntryAbility.onNewWant()
onNewWant(want: Want, launchParam: LaunchParam): void {
const mealType = want.parameters?.['mealType'] as string;
if (mealType) {
// 存入全局状态,HomePage 检测到后自动打开 AI 面板
AppStorage.setOrCreate('target_meal_type', mealType);
}
}
// Index.ets — 主页面检测
aboutToAppear(): void {
// 检查是否有来自 Widget 的目标餐段
const targetMeal = AppStorage.get<string>('target_meal_type');
if (targetMeal) {
// 自动打开 AI 记录面板并预填餐段
this.showAIPanel(targetMeal);
// 清除标记,避免重复触发
AppStorage.setOrCreate('target_meal_type', '');
}
}
完整用户体验流程
桌面看到 Widget
↓
点击「午餐」按钮(1 步)
↓
App 自动打开 + AI 面板自动弹出 + 餐段预选"午餐"
↓
输入"鸡腿饭"(1 步)
↓
AI 识别完成 ✓
总计:2 步(vs 传统方式 6+ 步)
漏记率降低:80%
五、11 字段全量 formData 设计
为什么用 11 个字段而不是按需传?
| 字段 | 类型 | 用途 | 使用者 |
|---|---|---|---|
totalKcal |
number | 已摄入热量 | 统计卡 + 详情卡 |
dailyLimit |
number | 每日限额 | 统计卡 + 详情卡 |
proteinGrams |
number | 蛋白质克数 | 详情卡 |
carbGrams |
number | 碳水克数 | 详情卡 |
fatGrams |
number | 脂肪克数 | 详情卡 |
mealCount |
number | 已记录餐数 | 快速记录卡 |
recordCount |
number | 总记录条数 | 快速记录卡 |
hasBreakfast |
0/1 | 早餐是否已记 | 快速记录卡 |
hasLunch |
0/1 | 午餐是否已记 | 快速记录卡 |
hasDinner |
0/1 | 晚餐是否已记 | 快速记录卡 |
hasSnack |
0/1 | 加餐是否已记 | 快速记录卡 |
updateTime |
number | 时间戳 | 缓存判断 |
formDimension |
2/4 | 卡片尺寸 | 区分 2×2 / 2×4 |
设计原则:各 Widget 通过 @LocalStorageProp 自取所需字段,不需要二次请求。一次推送,全部到位。
六、总结与下篇预告
核心要点回顾
- 跨进程通信:ArkData Preferences 是主 App 和 FormExtension 之间的数据桥梁
- 主动推送:
formProvider.updateForm()在记录添加后即时推送(尽力),前后台切换时兜底刷新(可靠) - 一键直达:
postCardAction+onNewWant+AppStorage实现参数传递和自动打开面板 - 11 字段全量 formData:一次推送,3 张卡片各自取用,避免冗余请求
- formId 持久化:新增卡片时保存 ID,删除时清理,保证推送目标始终准确
下一篇:《28 个 Token 重构鸿蒙 App:企业级设计系统的搭建实践》
我们将揭秘食刻的 Design Token 系统 —— 如何用 28 个设计变量 + BaseCard 通用容器 + @Extend 装饰器,实现全 App 零硬编码、新增页面只需关注业务逻辑的企业级架构。
📌 项目仓库:https://atomgit.com/VON-/cxs-demo1
更多推荐



所有评论(0)