适合谁看
-
正在设计多张鸿蒙卡片的人
-
不确定什么卡片该动态、什么该静态的人
-
想控制鸿蒙卡片维护成本的人
问题背景
很多人一开始做鸿蒙卡片时,会默认:既然是卡片,那就都做成动态。
但动态卡片意味着:
-
需要数据更新策略
-
需要更复杂的生命周期管理
-
需要处理内容源稳定性
-
需要定时更新配置
并不是每一种入口都值得付出这个成本。
项目中的真实场景
食界探味当前有 3 张鸿蒙卡片:
| 鸿蒙卡片 | 类型 | 内容 | 更新策略 |
|---|---|---|---|
| 今日探味 (DailyRecommendCard) | 动态 | 每日推荐一道环球美食 | 每天 00:05 自动更新 |
| 搜索美食 (SearchCard) | 静态 | 快速跳转搜索页 | 不更新 |
| 美食许愿箱 (WishBoxCard) | 静态 | 快速跳转心愿单 | 不更新 |
在 DailyRecommendFormAbility.ets 里,通过 STATIC_CARD_NAMES 实现分流:
const STATIC_CARD_NAMES: Set<string> = new Set([
'SearchCard',
'AiAssistantCard',
'WishBoxCard',
]);
export default class DailyRecommendFormAbility 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 = getRecommendOfToday();
return formBindingData.createFormBindingData({
dishName: item.name,
dishRegion: item.region,
dishImage: resolveImageResName(item.imageResName),
dishId: item.id,
dishHighlight: item.highlight,
dishSummary: item.summary,
});
}
}
核心实现
一、鸿蒙静态卡片适合做什么
更适合静态鸿蒙卡片的,是明确入口型能力:
| 静态卡片 | 用途 | 为什么静态 |
|---|---|---|
| SearchCard | 打开搜索 | 功能固定,不需要内容更新 |
| AiAssistantCard | 进入 AI 助手 | 功能固定,不需要内容更新 |
| WishBoxCard | 打开心愿单 | 功能固定,不需要内容更新 |
这类鸿蒙卡片的价值来自"更快进入",不是"展示动态内容"。
静态鸿蒙卡片的特点:
-
内容固定不变
-
不需要数据更新策略
-
生命周期简单(只需要 onAddForm)
-
维护成本低
二、鸿蒙动态卡片适合做什么
更适合动态鸿蒙卡片的,是内容型入口:
| 动态卡片 | 用途 | 为什么动态 |
|---|---|---|
| DailyRecommendCard | 今日推荐 | 内容每天变化 |
| 每日一题 | 每天一道题 | 内容每天变化 |
| 今日热榜 | 每天热榜 | 内容每天变化 |
它的价值来自"内容每天变",而不是"入口在哪里"。
动态鸿蒙卡片的特点:
-
内容定期更新
-
需要数据更新策略
-
生命周期复杂(onAddForm + onUpdateForm)
-
需要处理内容源稳定性
-
需要配置 updateEnabled + scheduledUpdateTime
三、为什么要分开------三个好处
好处 1:动态更新逻辑只服务必要卡片
如果所有鸿蒙卡片都做成动态,每张卡片都需要:
-
数据源
-
更新算法
-
容错处理
但静态卡片根本不需要这些。分开后,只有 DailyRecommendCard 需要 getRecommendOfToday() 和 resolveImageResName()。
好处 2:静态入口卡片更稳定
静态鸿蒙卡片没有数据更新,所以:
-
不会因为数据源出错而显示异常
-
不会因为定时更新失败而显示空白
-
不会因为内容过期而误导用户
好处 3:后期内容和入口的迭代成本不会绑死在一起
所有卡片都做动态时:
改搜索入口 → 可能影响数据更新逻辑
改推荐算法 → 可能影响搜索卡片
分开后:
改搜索入口 → 只改 SearchCard
改推荐算法 → 只改 DailyRecommendCard
四、在同一个 FormAbility 中实现分流
食界探味的设计很巧妙:3 张鸿蒙卡片共享同一个 DailyRecommendFormAbility,通过 STATIC_CARD_NAMES 分流。
DailyRecommendFormAbility
│
├─ onAddForm(want)
│ │
│ ├─ formName ∈ STATIC_CARD_NAMES?
│ │ ├─ 是 → 返回空数据(静态卡片)
│ │ └─ 否 → getRecommendOfToday()(动态卡片)
│ │
│ ▼
│ formBindingData.createFormBindingData({...})
│
├─ onUpdateForm(formId)
│ │
│ └─ 只有动态卡片会触发
│ → getRecommendOfToday()
│ → formProvider.updateForm(formId, data)
│
└─ onRemoveForm(formId)
└─ 日志记录
这种设计的优势:
-
一个 FormAbility 管理所有卡片,不需要为每张卡片创建单独的 Ability
-
静态卡片的
onUpdateForm不会被触发(因为配置了updateEnabled: false) -
静态卡片的
onAddForm返回空数据,由卡片 UI 自己显示默认内容
五、配置层的分工
在 daily_recommend_form_config.json 中,静态和动态卡片的配置差异:
| 配置项 | 动态卡片 (DailyRecommendCard) | 静态卡片 (SearchCard) |
|---|---|---|
updateEnabled |
true |
false |
scheduledUpdateTime |
00:05 |
无 |
isDefault |
true |
false |
这说明鸿蒙系统层面也区分了静态和动态卡片:
-
动态卡片:系统会在指定时间触发
onUpdateForm() -
静态卡片:系统只在用户添加时触发
onAddForm(),之后不再更新
六、卡片 UI 层的分工
动态鸿蒙卡片的 UI 需要处理数据绑定:
// DailyRecommendCard.ets
@LocalStorageProp('dishName') dishName: string = '环球美食';
@LocalStorageProp('dishRegion') dishRegion: string = '世界';
@LocalStorageProp('dishImage') dishImage: string = 'dish_fallback';
// ... 数据来自 FormAbility
静态鸿蒙卡片的 UI 可以完全硬编码:
// SearchCard.ets(示意)
@Component
struct SearchCard {
build() {
Row() {
Image($r('app.media.icon_search')).width(48).height(48)
Column() {
Text('搜索美食').fontSize(16).fontWeight(FontWeight.Bold)
Text('搜索全球美食与食材').fontSize(12)
}
}
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { pageId: 'search' }
});
})
}
}
静态卡片不需要 @LocalStorageProp,因为数据是固定的。
七、点击跳转的分工
两种鸿蒙卡片的点击跳转方式一致(都用 postCardAction),但跳转目标不同:
| 鸿蒙卡片 | 点击跳转 | 参数 |
|---|---|---|
| DailyRecommendCard | dish_detail / explore | pageId + dishId |
| SearchCard | /search | pageId='search' |
| WishBoxCard | /wish-box | pageId='wish_box' |
动态卡片需要传 dishId 参数(因为内容每天变),静态卡片只需要传 pageId。
关键代码位置
| 文件 | 作用 |
|---|---|
app/ohos/entry/src/main/ets/formability/DailyRecommendFormAbility.ets |
鸿蒙卡片生命周期(含分流逻辑) |
app/ohos/entry/src/main/ets/formability/RecommendData.ets |
动态卡片数据源 |
app/ohos/entry/src/main/resources/base/profile/daily_recommend_form_config.json |
卡片配置(区分静态/动态) |
静态 vs 动态鸿蒙卡片对比表
| 维度 | 静态鸿蒙卡片 | 动态鸿蒙卡片 |
|---|---|---|
| 内容 | 固定不变 | 定期更新 |
| 数据源 | 无 | getRecommendOfToday() |
| updateEnabled | false | true |
| scheduledUpdateTime | 无 | 00:05 |
| onAddForm | 返回空数据 | 返回推荐数据 |
| onUpdateForm | 不触发 | 定时触发 |
| UI 数据绑定 | 不需要 | 需要 @LocalStorageProp |
| 点击参数 | 只需 pageId | pageId + 内容参数 |
| 维护成本 | 低 | 中 |
| 适用场景 | 功能入口 | 内容推荐 |
常见坑
-
把所有鸿蒙卡片都做成动态 --- 静态入口卡片不需要数据更新
-
动态卡片没有稳定内容来源 --- 数据源出错时需要兜底
-
静态鸿蒙卡片也强行维护更新逻辑 --- 浪费资源,增加复杂度
-
卡片分工不清,导致点击后入口语义混乱 --- 静态是功能入口,动态是内容入口
-
静态卡片的 onAddForm 返回了数据 --- 应该返回空数据,由 UI 显示默认内容
-
动态卡片没有配置 updateEnabled --- 鸿蒙系统不会触发定时更新
可复用模板
鸿蒙 FormAbility 分流模板
const STATIC_CARD_NAMES: Set<string> = new Set(['SearchCard', 'WishBoxCard']);
export default class MyFormAbility 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,
});
}
onUpdateForm(formId: string): void {
// 只有动态卡片会触发
const item = getData();
formProvider.updateForm(formId, formBindingData.createFormBindingData({
title: item.title,
content: item.content,
}));
}
}
配置模板(区分静态/动态)
{
"forms": [
{
"name": "DynamicCard",
"updateEnabled": true,
"scheduledUpdateTime": "00:05",
"isDefault": true
},
{
"name": "StaticCard",
"updateEnabled": false,
"isDefault": false
}
]
}
卡片分工决策模板
判断鸿蒙卡片类型:
内容是否每天变化? → 是 → 动态卡片
是否只是功能入口? → 是 → 静态卡片
需要数据更新策略? → 是 → 动态卡片
内容固定不变? → 是 → 静态卡片
本篇总结
鸿蒙静态卡片适合做入口,动态卡片适合做内容。食界探味当前的实现展示了如何在同一个 FormAbility 中通过 STATIC_CARD_NAMES 实现分流:
-
静态鸿蒙卡片 --- SearchCard、WishBoxCard,功能固定,不需要数据更新
-
动态鸿蒙卡片 --- DailyRecommendCard,内容每天变化,需要定时更新
这层分工能明显降低鸿蒙卡片系统复杂度:动态更新逻辑只服务必要卡片,静态入口卡片更稳定,后期迭代成本不会绑死在一起。食界探味当前的实现就是一个很实用的参考。
