鸿蒙卡片为什么是 Flutter 项目做系统级触达的关键

适合谁看

  • 想让 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

常见坑

  • 把卡片当成比赛展示项,不考虑内容价值 --- 卡片要有真实内容,不能只是空壳

  • 卡片内容和应用内内容完全断开 --- 用户点进应用后应该看到一致的内容

  • 只有样式,没有数据更新策略 --- 动态卡片必须配置 updateEnabledscheduledUpdateTime

  • 做了卡片,却没有考虑点击后的落点 --- 卡片点击应该跳转到有意义的页面

  • 图片资源名不做校验 --- 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 项目更像真正消费鸿蒙入口能力的应用。

食界探味的"今日探味"卡片展示了完整的动态卡片实现:

  1. 配置层 --- daily_recommend_form_config.json 定义卡片名称、更新频率、尺寸

  2. 数据层 --- RecommendData.ets 用日期轮询算法每天推荐不同菜品

  3. 生命周期层 --- DailyRecommendFormAbility 处理创建、更新、移除

  4. UI 层 --- DailyRecommendCard.ets 声明式 UI + 数据绑定

  5. 跳转层 --- 复用 Intents Kit 的 pageId 机制

对内容型项目来说,卡片尤其适合承接"今日推荐"这类内容。它不需要用户打开 App,就能在桌面上展示推荐,缩短了从"有需求"到"看到内容"的路径。