一、背景说明

鸿蒙生态正在加速扩张——微信鸿蒙版安装量近日突破 9000 万次,HarmonyOS 终端用户数预计本月突破 1 亿,桌面卡片作为鸿蒙生态最具辨识度的入口之一,是每个原生应用都值得掌握的能力。本文不谈生态数据,聚焦一个具体技术点:使用 Form Kit(@kit.FormKit)开发一个 ArkTS 桌面卡片,实现卡片页面布局、数据绑定、定时刷新和事件交互的完整闭环。

二、技术原理

鸿蒙桌面卡片本质是一个由应用提供方渲染、桌面(卡片使用方)展示的轻量 UI 单元,核心机制如下:

  • FormExtensionAbility:卡片提供方的生命周期载体,在独立的 Extension 进程中运行。卡片被添加到桌面时回调 onAddForm,移除时回调 onRemoveForm,定时/主动刷新时回调 onUpdateForm。
  • FormBindingData:卡片数据绑定对象,通过 createFormBindingData(obj) 携带键值对数据,卡片页面用 @LocalStorageProp('key') 声明式接收。
  • formProvider:提供方主动操作卡片的 API,updateForm() 更新卡片内容,setFormNextRefreshTime() 设置下次定时刷新(最小间隔 5 分钟)。
  • 卡片页面:ArkTS 卡片使用受限的声明式语法(Chart、Image、Text 等少量组件),与主应用 UI 隔离运行。

数据流:桌面添加卡片 → onAddForm 返回 FormBindingData → 卡片页面 @LocalStorageProp 绑定渲染 → 定时/事件触发 onUpdateForm → formProvider.updateForm 推送新数据。

三、实现步骤

工程为 Stage 模型(API 12+),DevEco Studio 中右键 src/main/ets → New → Service Widget 即可生成骨架,本文给出手工实现的完整文件。

1. 卡片配置文件 WidgetCard.ets

卡片页面文件位于 entry/src/main/ets/widget/pages/:

let storage = new LocalStorage();

@Entry(storage)
@Component
struct WidgetWeatherCard {
  @LocalStorageProp('city') city: string = '北京';
  @LocalStorageProp('temperature') temperature: string = '--';
  @LocalStorageProp('updateTime') updateTime: string = '';

  build() {
    Column() {
      Row() {
        Text(this.city)
          .fontSize(16)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
        Blank()
        Text(this.updateTime)
          .fontSize(10)
          .fontColor('#B3FFFFFF')
      }
      .width('100%')

      Text(`${this.temperature}°C`)
        .fontSize(40)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
        .margin({ top: 8 })

      Text('点击卡片查看详情')
        .fontSize(11)
        .fontColor('#B3FFFFFF')
        .margin({ top: 12 })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .linearGradient({
      angle: 135,
      colors: [['#3A7BD5', 0.0], ['#00D2FF', 1.0]]
    })
    .onClick(() => {
      postCardAction(this, {
        'action': 'router',
        'abilityName': 'EntryAbility',
        'params': { 'from': 'widget' }
      })
    })
  }
}
2. 卡片提供方 EntryFormAbility.ets
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = 'EntryFormAbility';
const DOMAIN: number = 0xFF00;

export default class EntryFormAbility extends FormExtensionAbility {
  // 添加卡片时回调,返回初始数据
  onAddForm(want: Want): formBindingData.FormBindingData {
    const formId: string = want.parameters?.[formInfo.FormParam.IDENTITY_KEY] as string;
    hilog.info(DOMAIN, TAG, `onAddForm, formId=${formId}`);

    const obj: Record<string, string> = {
      'city': '北京',
      'temperature': '23',
      'updateTime': this.getCurrentTime()
    };
    // 定时刷新:下一次刷新时间为 30 分钟后
    formProvider.setFormNextRefreshTime(formId, 30, (err: BusinessError) => {
      if (err) {
        hilog.error(DOMAIN, TAG, `setFormNextRefreshTime failed: ${err.code}`);
      }
    });
    return formBindingData.createFormBindingData(obj);
  }

  // 定时刷新/使用方主动刷新时回调
  onUpdateForm(formId: string): void {
    hilog.info(DOMAIN, TAG, `onUpdateForm, formId=${formId}`);
    // 真实场景这里请求天气接口,此处演示用模拟数据
    const obj: Record<string, string> = {
      'city': '北京',
      'temperature': `${18 + Math.floor(Math.random() * 10)}`,
      'updateTime': this.getCurrentTime()
    };
    const data: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
    formProvider.updateForm(formId, data).catch((err: BusinessError) => {
      hilog.error(DOMAIN, TAG, `updateForm failed, code=${err.code}, msg=${err.message}`);
    });
  }

  // 卡片 message 事件回调(卡片内 postCardAction action=message 时触发)
  onFormEvent(formId: string, message: string): void {
    hilog.info(DOMAIN, TAG, `onFormEvent, msg=${message}`);
  }

  onRemoveForm(formId: string): void {
    // 卡片被移除,可在此清理该 formId 相关的持久化数据
    hilog.info(DOMAIN, TAG, `onRemoveForm, formId=${formId}`);
  }

  private getCurrentTime(): string {
    const d = new Date();
    return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
  }
}
3. 卡片配置 form_config.json

卡片在 resources/base/profile/form_config.json 中注册,并在 module.json5 的 extensionAbilities 里声明:

{
  "forms": [
    {
      "name": "WeatherWidget",
      "displayName": "$string:widget_name",
      "description": "$string:widget_desc",
      "src": "./ets/widget/pages/WidgetCard.ets",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "07:30",
      "updateDuration": 1,
      "supportShapes": ["rect"]
    }
  ]
}
{
  "extensionAbilities": [
    {
      "name": "EntryFormAbility",
      "srcEntry": "./ets/entryformability/EntryFormAbility.ets",
      "icon": "$media:icon",
      "label": "$string:widget_name",
      "description": "$string:widget_desc",
      "type": "form",
      "metadata": [
        {
          "name": "ohos.extension.form",
          "resource": "$profile:form_config"
        }
      ]
    }
  ]
}
4. 主应用处理卡片跳转

卡片 router 事件拉起主应用时,参数通过 want 传入,在 UIAbility 中接收:

// EntryAbility.ets 的 onCreate 中
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  if (want.parameters?.['from'] === 'widget') {
    console.info('从天气卡片拉起,跳转天气详情页');
    // 可用 windowStage.loadContent 加载对应页面
  }
}
四、应用场景示例

运行应用后长按桌面空白处 → 服务卡片 → 选择本应用卡片添加到桌面,即可看到实时气温卡片;也可从主应用主动推送一次刷新:

// 主应用内(如数据更新后)主动刷新指定卡片
import { formBindingData, formProvider } from '@kit.FormKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function pushWidgetUpdate(formId: string, temp: string): Promise<void> {
  const obj: Record<string, string> = { 'temperature': temp };
  try {
    await formProvider.updateForm(formId, formBindingData.createFormBindingData(obj));
  } catch (err) {
    const e = err as BusinessError;
    console.error(`update failed, code=${e.code}`);
  }
}
五、技术要点
1. 刷新策略与限制
  • setFormNextRefreshTime 最小间隔 5 分钟,updateDuration 取值 1/2 表示每 30/60 分钟刷新;
  • scheduledUpdateTime 支持定点刷新(如每天 07:30);
  • 定时刷新不精确,系统会合并调度,需要秒级刷新的场景应改用主进程数据推送 + updateForm。
2. Extension 进程 10 秒生命周期

FormExtensionAbility 进程非常驻:生命周期回调结束后进程保留约 10 秒即退出。超过 10 秒的工作(如网络请求重试)应拉起主应用处理,完成后由主进程调用 updateForm 推送结果,不要在回调里死等。

3. 错误处理
formProvider.updateForm(formId, data).catch((err: BusinessError) => {
  if (err.code === 16501001) {
    console.error('卡片已不存在,停止刷新并清理 formId');
  } else if (err.code === 16501003) {
    console.error('无权操作该卡片');
  } else {
    console.error(`update error code=${err.code}`);
  }
});
4. 数据限制
  • 单次 FormBindingData 序列化后过大会导致更新失败,图片建议走 formImages 携带 fd,控制在 2MB/张、共 5 张以内(API 20 起放宽至 20 张/10MB);
  • 卡片页面仅支持有限的组件与样式,复杂交互应路由到主应用完成。
六、扩展建议
  1. requestForm 主动刷新:卡片页面调用 requestForm 让提供方立即走一遍 onUpdateForm;
  2. 动态卡片(Live Form):API 20+ 支持 LiveFormExtensionAbility,可开发实况窗级常驻卡片;
  3. 卡片内直接请求数据:卡片页面可配置网络权限后用 @ohos.net.http 自取数据,减少提供方推送频次:
// 卡片页面内轻量请求(卡片可用组件/能力有限,适合简单场景)
import { http } from '@kit.NetworkKit';
结语

本文用 Form Kit 完成了鸿蒙桌面卡片从页面布局、生命周期实现、form_config 注册到定时刷新与事件跳转的完整实现,核心代码约 200 行,均可直接编译运行。掌握卡片开发后,天气、日程、播放器等高频信息都可以从"打开应用"变为"一眼可见",显著提升应用在鸿蒙桌面的存在感。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐