【鸿蒙心迹】互动卡片全链路——把核心功能放到桌面(FormKit 实战)(HarmonyOS 7.x)

摘要: 做番茄钟应用时,产品要求"把番茄钟放到桌面,点击就能用,不用打开 App"。这就是鸿蒙的互动卡片(FormKit)------桌面卡片可交互、数据可推送、与主应用联动。本文用番茄钟贯穿全链路:卡片生命周期、数据推送、事件交互、主应用联动,附 5 个真实踩坑。

适用版本: HarmonyOS NEXT 7.x / API 14+ / FormKit(2026 年稳定版)

开篇:把核心功能搬到桌面卡片上

"把番茄钟放到桌面上,点一下就开始计时,不用打开 App。"

2026 年 8 月,番茄钟 App 做用户调研,最高频需求是"桌面快捷启动"。我查文档发现鸿蒙的**互动卡片(FormKit)**不是静态小组件------它可以交互、可以实时推送数据、可以和主应用双向联动。但接入链路比想象长:卡片模板、数据更新、事件处理、跨进程通信。踩了 5 个坑,这篇文章把全链路讲透。

一、互动卡片是什么:不止是桌面小组件

维度 静态卡片 互动卡片
数据更新 定时拉取 实时推送 + 主动更新
用户交互 仅跳转 卡片内直接操作
状态同步 单向 双向(卡片 ↔ App)
能力范围 展示 展示 + 交互 + 联动

二、卡片生命周期:从添加到移除的 5 个状态

互动卡片和普通组件最大的不同:它的生命周期由系统托管,而不是由页面路由控制。理解这 5 个状态,是排查"卡片不更新""卡片数据丢了"这类问题的前提。

状态 触发时机 回调方法 开发者要做什么
已创建 用户把卡片添加到桌面 onAddForm 返回首屏 FormBindingData,卡片据此渲染初始 UI
已渲染 首屏数据绑定完成 --- 卡片 UI 可见,用户可交互
已更新 updateForm 推送新数据 onUpdateForm 重新渲染,只改数据不重建卡片
已移除 用户从桌面删除卡片 onRemoveForm 清理该卡片关联的状态/资源(定时器、KV key 等)
已销毁 系统回收卡片进程 --- 系统行为,无需处理;状态应已持久化到 KV/Preferences

关键点 :卡片进程可能被系统随时回收,绝不能把状态只放在内存里。计时、番茄钟的"是否运行中"这类状态,必须落到分布式 KV 或 Preferences,否则进程一回收,卡片和主应用都读不到。

三、全链路接入

步骤 1:创建卡片模板

text 复制代码
DevEco Studio → New → Form → 卡片模板
  → 命名: TomatoCard
  → 类型: 互动卡片
  → 尺寸: 2x2

步骤 2:module.json5 声明 FormExtensionAbility

json5 复制代码
{
  "module": {
    "extensionAbilities": [
      {
        "name": "TomatoFormAbility",
        "srcEntry": "./ets/tomatoform/TomatoFormAbility.ets",
        "type": "form",
        "metadata": [
          { "name": "ohos.extension.form", "resource": "$profile:form_config" }
        ]
      }
    ]
  }
}

步骤 3:卡片配置

json 复制代码
// resources/base/profile/form_config.json
{
  "forms": [
    {
      "name": "tomatoCard",
      "displayName": "番茄钟",
      "description": "桌面番茄钟计时",
      "src": "./ets/tomatoform/pages/TomatoCardPage.ets",
      "window": { "designWidth": 720, "autoDesignWidth": true },
      "isDynamic": true,
      "supportDimensions": ["2*2"],
      "defaultDimension": "2*2",
      "updateEnabled": true,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1
    }
  ]
}

避坑 1 :isDynamic: true 是互动卡片的关键,false 就是静态卡片不能交互。

步骤 4:实现 FormExtensionAbility

typescript 复制代码
// TomatoFormAbility.ets
import { FormExtensionAbility, formBindingData, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';

export default class TomatoFormAbility extends FormExtensionAbility {
  // 卡片创建:返回首屏数据(卡片据此渲染初始 UI)
  onAddForm(want: Want): formBindingData.FormBindingData {
    return formBindingData.createFormBindingData({
      title: '番茄钟',
      time: '25:00',
      status: 'idle'
    });
  }

  // 卡片更新(定时触发 / 手动触发):formId 由系统传入,据此定位要更新的卡片
  onUpdateForm(formId: string): void {
    formProvider.updateForm(formId, formBindingData.createFormBindingData({
      title: '番茄钟',
      time: this.getCurrentTime(),
      status: 'running'
    }));
  }

  // 卡片被移除:清理该卡片关联的状态/资源
  onRemoveForm(formId: string): void {
    // 清理该卡片的状态
  }

  // 卡片内事件(点击按钮):message 是卡片 postCardAction 透传过来的字符串,
  // 需要 JSON 反序列化后取出 params
  onFormEvent(formId: string, message: string): void {
    const data = JSON.parse(message) as Record<string, string>;
    if (data.message === 'start') {
      this.startTimer(formId);
    } else if (data.message === 'pause') {
      this.pauseTimer(formId);
    }
  }
}

步骤 5:卡片 UI 与事件

typescript 复制代码
// TomatoCardPage.ets
let storage = new LocalStorage();

@Entry(storage)
@Component
struct TomatoCardPage {
  @LocalStorageProp('title') title: string = '番茄钟';
  @LocalStorageProp('time') time: string = '25:00';
  @LocalStorageProp('status') status: string = 'idle';

  build() {
    Column() {
      Text(this.title).fontSize(14)
      Text(this.time).fontSize(32).fontWeight(FontWeight.Bold)
      Button(this.status === 'idle' ? '开始' : '暂停')
        // postCardAction 是卡片环境注入的全局函数,
        // 事件在 FormExtensionAbility.onFormEvent 中处理
        .onClick(() => {
          postCardAction(this, {
            action: 'event',
            params: { message: this.status === 'idle' ? 'start' : 'pause' }
          });
        })
    }
    .width('100%').height('100%')
    .backgroundColor('#FFEFD5')
  }
}

说明 :卡片运行在独立的卡片进程 里,不能直接访问主应用内存。卡片内只能做两件事:① 渲染 UI(读 LocalStorage 数据);② 通过 postCardAction 把用户操作(事件)或跳转意图透传给系统/主应用。真正的计时、状态变更逻辑都跑在 FormExtensionAbility(卡片服务进程)里。


四、5 个真实踩坑

1. 卡片数据不更新

现象:主应用里改了状态,桌面卡片不刷新。

根因 :updateForm 没调用,或 formId 不对。

解决:主应用通过 formProvider 主动推送:

typescript 复制代码
import { formProvider, formBindingData } from '@kit.FormKit';

// 主应用里更新卡片:把最新状态打包成 FormBindingData 推给指定 formId
async function updateCard(formId: string, data: Record<string, string>): Promise<void> {
  const bindingData = formBindingData.createFormBindingData(data);
  await formProvider.updateForm(formId, bindingData);
}

2. 卡片点击跳转失效

现象:点卡片空白区域不跳转到主应用。

解决 :用 postCardAction 显式声明跳转:

typescript 复制代码
Column() {
  // 卡片内容
}
.onClick(() => {
  // 卡片整体点击:通过 postCardAction 拉起主应用(router 动作),
  // 由系统唤起 EntryAbility 的指定页面
  postCardAction(this, {
    action: 'router',
    abilityName: 'EntryAbility',
    params: { page: 'tomatoDetail' }
  });
})

3. 卡片与主应用状态不同步

现象:卡片里启动计时,打开主应用看到的还是初始状态。

根因:卡片进程和主应用进程独立,内存不共享。

解决:用分布式数据或公共文件同步:

typescript 复制代码
import { distributedKVStore } from '@kit.dataKVStoreKit';

// 卡片启动计时时写入分布式 KV
async function syncToMain(status: string): Promise<void> {
  const kvManager = distributedKVStore.createKVManager({ /* config */ });
  const store = await kvManager.getKVStore({ /* config */ });
  await store.put('tomatoStatus', status);
}

// 主应用监听 KV 变化
store.on('dataChange', (data) => {
  this.updateUI(data.value);
});

4. 卡片尺寸适配

现象:2x2 卡片在不同设备上显示效果差异大。

解决 :用 supportDimensions 声明多尺寸,各尺寸独立 UI:

json 复制代码
{
  "supportDimensions": ["2*2", "2*4", "4*4"],
  "defaultDimension": "2*2"
}

5. 卡片更新频率限制

现象 :频繁 updateForm 报错 update too frequently。

根因:系统限制卡片更新频率(通常 5 分钟内有限次)。

解决 :合并更新,用 updateDuration 定时更新而非每次状态变都推。

五、卡片与主应用联动时序

避坑要点

  1. isDynamic: true 才能交互:静态卡片不能响应事件
  2. 卡片与主应用跨进程:用分布式 KV 同步状态
  3. 更新频率有限制:合并更新,别频繁 updateForm

边界与已知限制

限制项 具体表现 规避方式
尺寸规格 卡片尺寸受系统栅格约束,不能任意指定 只设计官方支持的几种规格
刷新频率 卡片刷新有频率与次数约束 合并更新,避免短时间高频刷新
交互能力 卡片可承载的交互控件有限 复杂交互落地到应用内,卡片只做入口
数据量 卡片不适合承载大量数据 只放核心状态,详情进应用看
进程独立 卡片运行在独立进程,不共享应用内存状态 数据落存储后共享,不要依赖内存变量
机型差异 部分桌面/机型不支持或展示异常 提供兜底布局与降级方案
生命周期 卡片可能被系统回收后重建 状态必须可重建,不能只靠内存

小结

桌面卡片的设计原则是**"一屏内给出最关键的那一个信息"**:把它当成应用的快捷入口,而不是应用的缩略版。塞太多内容既超出规格,也会让卡片失去"一眼看完"的价值。

技术上最容易踩的是进程独立这一点------卡片和应用不共享内存,所有要展示的数据都必须先落盘再读取。想明白这一点,卡片数据不同步的问题就解决了一大半。

评论区交流: 你的互动卡片接入遇到什么问题?评论区交流。

相关推荐
贾伟康1 个月前
【HarmonyOS 7新能力|008】互动卡片入门实战:从能力边界到最小可运行链路
harmonyos·arkts·arkui·harmonyos 7·互动卡片
大师兄66685 个月前
卡片状态驱动刷新——让不同卡片实例显示不同内容的正确方式
服务卡片·harmonyos6·formkit·状态驱动
大师兄66685 个月前
从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解
harmonyos·服务卡片·harmonyos6·formkit
大师兄66685 个月前
HarmonyOS 服务卡片开发之JS 卡片开发
javascript·华为·harmonyos·harmonyos6·formkit
大师兄66685 个月前
卡片数据不丢失:Preferences 在 FormKit 里的正确用法
持久化存储·preferences·harmonyos6·formkit·formid管理
大师兄66685 个月前
HarmonyOS 卡片 UI 三种玩法:普通卡片、动效卡片、Canvas 卡片
harmonyos·arkts·formkit·动效卡片·canvas卡片
柱子jason1 年前
使用IOT-Tree Server通过FINS协议连接欧姆龙CP2E-N系列PLC
物联网·mqtt·工业自动化·欧姆龙plc·iot-tree·数据推送
柱子jason1 年前
使用IOT-Tree消息流MQTT模块节点实现监测数据推送功能
物联网·mqtt·边缘计算·iot-tree·数据推送
sinat_333518871 年前
如何用定时功能提升专注力?
番茄钟·定时调光·色温调节