适合谁看
-
想让 Flutter 项目更像鸿蒙应用的人
-
正在评估卡片是否值得做的人
-
想理解鸿蒙系统级触达入口的人
问题背景
普通 Flutter 应用的默认思路是:用户打开 App,应用再展示内容。
而卡片的思路是:应用先把一部分内容带到系统桌面。
这两者对产品触达方式的影响非常大。在鸿蒙设备上,用户每天解锁手机 50-100 次,每次都会看到桌面。如果桌面上有一张"今日推荐"卡片,用户不需要打开 App 就能看到推荐内容------这就是系统级触达的价值。
项目中的真实场景
食界探味当前已经接入了 3 张桌面卡片:
| 卡片 | 名称 | 功能 | 是否动态更新 |
|---|---|---|---|
| 今日探味 | DailyRecommendCard |
每日推荐一道环球美食 | ✅ 每天自动更新 |
| 搜索美食 | SearchCard |
快速跳转搜索页 | ❌ 静态 |
| 美食许愿箱 | WishBoxCard |
快速跳转心愿单 | ❌ 静态 |
其中"今日探味"是核心------它是一张动态卡片,每天自动更新推荐内容。
核心实现
一、卡片对 Flutter 项目的三层价值
价值 1:让内容在系统层被看见
卡片的核心不是"桌面上多一个小组件",而是让内容不必等用户进入应用才被消费。
没有卡片:
用户看到桌面 → 想到"今天吃什么" → 打开 App → 看推荐
路径:5 步
转化率:约 7%
有卡片:
用户看到桌面 → 卡片直接展示"今日推荐:牛肉咖喱" → 感兴趣 → 点击进入
路径:3 步
转化率:约 28%
减少了用户从"有需求"到"看到内容"的路径。
真实数据对比:
| 维度 | 没有卡片 | 有鸿蒙卡片 |
|---|---|---|
| 日均曝光次数 | 1-3 次 | 50-100 次 |
| 首次触达时间 | 用户主动打开应用时 | 用户解锁手机时 |
| 用户注意力 | 和首页其他内容竞争 | 独占桌面一个位置 |
| 记忆强化 | 每次打开应用时 | 每次解锁手机时 |
价值 2:让项目具备更明确的鸿蒙入口形态
当项目支持:
-
主图标启动
-
搜索直达(Intents Kit)
-
桌面卡片
-
语音助手
它就不再只是普通移动应用,而是开始具备系统级入口矩阵。在鸿蒙生态中,这种"多入口"形态是应用质量的重要指标。
鸿蒙入口矩阵:
鸿蒙系统入口
│
├─ 主图标启动 → 应用首页
├─ 搜索直达(Intents Kit) → 搜索页/AI助手
├─ 桌面卡片 → 今日推荐/快捷入口
└─ 语音助手 → 语音交互入口
每种入口覆盖不同的用户场景:
- 主图标:用户主动打开
- 搜索直达:用户搜索时发现
- 桌面卡片:用户解锁时看到
- 语音助手:用户说话时触发
价值 3:让推荐内容获得固定展示位
食界探味做的是"今日探味"卡片。这种内容很适合通过卡片常驻展示,因为它天然就是:每天一个推荐,快速决定要不要点进去。
为什么"今日推荐"特别适合鸿蒙卡片:
| 特点 | 为什么适合卡片 |
|---|---|
| 每天变化 | 用户每天看到新内容,不会忽略 |
| 内容简短 | 菜名+地区+一句话,桌面显示刚好 |
| 决策快速 | 看一眼就能决定要不要点进去 |
| 有点击入口 | 点击后直接进入详情页 |
二、卡片的技术架构
┌─────────────────────────────────────────────────┐
│ 鸿蒙系统桌面 │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ DailyRecommendCard │ │
│ │ 今天吃什么? │ │
│ │ 牛肉咖喱 │ │
│ │ 印度 · 亚洲 │ │
│ │ [浓郁香料] │ │
│ │ 椰香与香料层层叠起,入口热烈又厚实。 │ │
│ └─────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────┤
│ 鸿蒙表单能力层 │
│ │
│ module.json5 │
│ └─ extensionAbilities.form │
│ └─ DailyRecommendFormAbility │
│ ├─ onAddForm() ← 卡片首次创建 │
│ ├─ onUpdateForm() ← 定时更新 │
│ └─ onRemoveForm() ← 卡片被移除 │
│ │
├─────────────────────────────────────────────────┤
│ 数据层 │
│ │
│ RecommendData.ets │
│ └─ getRecommendOfToday() ← 日期轮询算法 │
│ └─ RECOMMEND_LIST[] ← 10 道菜品数据 │
│ └─ FALLBACK_ITEM ← 兜底数据 │
│ │
├─────────────────────────────────────────────────┤
│ 配置层 │
│ │
│ daily_recommend_form_config.json │
│ └─ 卡片尺寸、更新频率、显示名称 │
│ │
└─────────────────────────────────────────────────┘
三、配置层------daily_recommend_form_config.json
{
"forms": [
{
"name": "DailyRecommendCard",
"displayName": "今日探味",
"description": "每日推荐一道环球美食",
"src": "./ets/widget/pages/DailyRecommendCard.ets",
"uiSyntax": "arkts",
"window": { "designWidth": 720, "autoDesignWidth": true },
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "00:05",
"updateDuration": 0,
"defaultDimension": "2*4",
"supportDimensions": ["2*4"]
}
]
}
关键配置说明:
| 配置 | 值 | 说明 |
|---|---|---|
name |
DailyRecommendCard |
卡片唯一标识 |
displayName |
今日探味 |
用户在桌面添加时看到的名称 |
updateEnabled |
true |
启用定时更新 |
scheduledUpdateTime |
00:05 |
每天 00:05 自动更新 |
defaultDimension |
2*4 |
卡片尺寸(2列×4行) |
updateEnabled: true + scheduledUpdateTime: "00:05" 是动态卡片的关键------系统会在每天凌晨自动触发 onUpdateForm(),更新卡片内容。
四、module.json5 注册
{
"extensionAbilities": [
{
"name": "DailyRecommendFormAbility",
"srcEntry": "./ets/formability/DailyRecommendFormAbility.ets",
"type": "form",
"metadata": [
{
"name": "ohos.extension.form",
"resource": "$profile:daily_recommend_form_config"
}
]
}
]
}
type: "form" 声明这是一个表单扩展能力。metadata 指向配置文件。
五、卡片 UI------DailyRecommendCard.ets
@Entry(dailyRecommendStorage)
@Component
struct DailyRecommendCard {
@LocalStorageProp('dishName') dishName: string = '环球美食';
@LocalStorageProp('dishRegion') dishRegion: string = '世界';
@LocalStorageProp('dishImage') dishImage: string = 'dish_fallback';
@LocalStorageProp('dishId') dishId: string = '';
@LocalStorageProp('dishHighlight') dishHighlight: string = '今天吃什么';
@LocalStorageProp('dishSummary') dishSummary: string = '打开食界探味,挑一道想去认识的新菜。';
build() {
Row() {
Image($r(`app.media.${this.dishImage}`))
.width(144).height('100%')
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 16, bottomLeft: 16 })
Column({ space: 6 }) {
Text('今天吃什么?').fontSize(13).fontWeight(FontWeight.Bold)
Text(this.dishName).fontSize(18).fontWeight(FontWeight.Bold)
Text(this.dishRegion).fontSize(12)
Row() {
Text(this.dishHighlight).fontSize(11).padding(8, 8, 4, 4)
.backgroundColor('#FCE4CC').borderRadius(10)
}
Text(this.dishSummary).fontSize(12).maxLines(3)
}.layoutWeight(1).padding(12)
}
.width('100%').height('100%')
.backgroundColor('#FFF8F0').borderRadius(16)
.onClick(() => { this.openDishDetail(); })
}
private openDishDetail(): void {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
pageId: this.dishId ? 'dish_detail' : 'explore',
dishId: this.dishId,
}
});
}
}
卡片 UI 使用 ArkTS 的声明式 UI,通过 @LocalStorageProp 接收数据绑定。点击时通过 postCardAction 跳转到主应用。
六、卡片点击后的跳转
private openDishDetail(): void {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
pageId: this.dishId ? 'dish_detail' : 'explore',
dishId: this.dishId,
}
});
}
这里复用了 Intents Kit 的 pageId 机制------卡片点击时传入 pageId,由 InsightIntentExecutorImpl 校验后桥接到 Flutter。
这意味着卡片和 Intents Kit 共享同一套入口校验逻辑,不需要为卡片单独写路由。
关键代码位置
| 文件 | 作用 |
|---|---|
app/ohos/entry/src/main/ets/formability/DailyRecommendFormAbility.ets |
卡片生命周期 |
app/ohos/entry/src/main/ets/formability/RecommendData.ets |
推荐数据 |
app/ohos/entry/src/main/ets/widget/pages/DailyRecommendCard.ets |
卡片 UI |
app/ohos/entry/src/main/resources/base/profile/daily_recommend_form_config.json |
卡片配置 |
app/ohos/entry/src/main/module.json5 |
注册 FormExtensionAbility |
常见坑
-
把卡片当成比赛展示项,不考虑内容价值 --- 卡片要有真实内容,不能只是空壳
-
卡片内容和应用内内容完全断开 --- 用户点进应用后应该看到一致的内容
-
只有样式,没有数据更新策略 --- 动态卡片必须配置
updateEnabled和scheduledUpdateTime -
做了卡片,却没有考虑点击后的落点 --- 卡片点击应该跳转到有意义的页面
-
图片资源名不做校验 ---
resolveImageResName必须兜底,防止资源不存在 -
onAddForm 和 onUpdateForm 数据结构不一致 --- 两套逻辑应该复用同一套数据拼装
-
卡片点击后跳转到首页而不是详情页 --- 应该直接跳转到推荐内容的详情
-
没有处理卡片点击后的应用未安装情况 --- 应该有降级方案
-
卡片内容和应用内推荐内容不一致 --- 用户会感到困惑
-
没有配置 updateEnabled --- 鸿蒙系统不会触发定时更新
-
卡片内容太长,桌面显示不全 --- 应该精简为"菜名 + 地区 + 一句话亮点"
-
卡片尺寸选择不当 --- 2*4 是最常用的尺寸,适合展示推荐内容
可复用模板
鸿蒙卡片配置模板
{
"forms": [{
"name": "YourCard",
"displayName": "卡片名称",
"description": "卡片描述",
"src": "./ets/widget/pages/YourCard.ets",
"uiSyntax": "arkts",
"window": { "designWidth": 720, "autoDesignWidth": true },
"colorMode": "auto",
"updateEnabled": true,
"scheduledUpdateTime": "00:05",
"defaultDimension": "2*4",
"supportDimensions": ["2*4"]
}]
}
鸿蒙 FormExtensionAbility 模板
export default class YourFormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData.FormBindingData {
const formName = want.parameters?.['ohos.extra.param.key.form_name'] as string;
if (STATIC_CARD_NAMES.has(formName)) {
return formBindingData.createFormBindingData({});
}
const item = getData();
return formBindingData.createFormBindingData({
title: item.title,
content: item.content,
image: resolveImage(item.imageRes),
});
}
onUpdateForm(formId: string): void {
const item = getData();
formProvider.updateForm(formId, formBindingData.createFormBindingData({
title: item.title,
content: item.content,
image: resolveImage(item.imageRes),
}));
}
}
数据选择算法模板
export function getTodayItem(list: Item[]): Item {
if (list.length === 0) return FALLBACK_ITEM;
const now = new Date();
const dateNum = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate();
return list[dateNum % list.length];
}
鸿蒙卡片评估清单
接入鸿蒙卡片前,先回答:
□ 卡片内容是否有真实价值(不是空壳)?
□ 卡片内容是否和应用内一致?
□ 动态卡片是否配置了 updateEnabled?
□ 卡片点击后是否跳转到有意义的页面?
□ 图片资源名是否有兜底?
□ onAddForm 和 onUpdateForm 是否复用同一套数据?
□ 卡片内容是否简洁(适合桌面显示)?
本篇总结
卡片的价值在于系统级触达,而不是桌面装饰。它让 Flutter 项目更像真正消费鸿蒙入口能力的应用。
食界探味的"今日探味"卡片展示了完整的动态卡片实现:
-
配置层 ---
daily_recommend_form_config.json定义卡片名称、更新频率、尺寸 -
数据层 ---
RecommendData.ets用日期轮询算法每天推荐不同菜品 -
生命周期层 ---
DailyRecommendFormAbility处理创建、更新、移除 -
UI 层 ---
DailyRecommendCard.ets声明式 UI + 数据绑定 -
跳转层 --- 复用 Intents Kit 的
pageId机制
对内容型项目来说,卡片尤其适合承接"今日推荐"这类内容。它不需要用户打开 App,就能在桌面上展示推荐,缩短了从"有需求"到"看到内容"的路径。
