Harmony os 卡片开发(call事件)

44 阅读5分钟

一、call 事件是什么?可以做什么?

鸿蒙第四期活动

场景: 比如音乐卡片、下载卡片、计时器卡片——用户在桌面点一下卡片上的按钮,就希望:

  • 不一定打开 App 界面;
  • 但能让 App 在后台执行某个功能:播放、暂停、继续下载、同步数据等。华为开发者+1

这时就用到 动态卡片 + postCardActioncall 能力

  • postCardAction(..., { action: 'call', ... })
  • 卡片提供方应用的某个 UIAbility 拉到后台运行;
  • 同时可以传递参数,并指定要调用的“方法名”(字符串);
  • UIAbility 在后台执行对应逻辑(比如 funA / funB)。

⚠️ 注意:

  • 本文说的是 动态卡片(WidgetCard + EntryFormAbility + postCardAction)。
  • 静态卡片用的是 FormLink

二、整体流程一图理解

  1. 卡片创建时EntryFormAbility.onAddFormformId 等信息通过 formBindingData 传给卡片组件(LocalStorageProp 接收)。博客园+1

  2. 用户点击卡片按钮(如「按钮A」/「按钮B」):

    • WidgetEventCall.ets 中,通过 postCardAction(this, { action: 'call', abilityName: 'XXXEntryAbility', params: {...} }) 发送 call 事件;
    • params.method 表示要调用哪个方法(如 'funA''funB'),是 必填,必须是 stringCSDN博客+1
  3. UIAbility 被后台拉起

    • 如果此前没启动过 → 触发 onCreate
    • 如果已经在后台 → 触发 onNewWant
    • onCreate 中通过 this.callee.on('funA', handler) 监听 call 事件对应的方法。知乎专栏+1
  4. UIAbility 处理参数并执行逻辑

    • 通过 rpc.MessageSequence 拿到卡片传来的参数;
    • 做完业务逻辑后,可以通过 formProvider.updateForm 回写卡片内容;
    • 若需要返回值给调用方,可以返回一个实现了 rpc.Parcelable 的对象。
  5. call 事件权限要求

    • module.json5 中添加后台运行权限: "ohos.permission.KEEP_BACKGROUND_RUNNING",否则无法在后台通过 call 拉起 UIAbility。CSDN博客+1

三、开发步骤

步骤 1:在 FormAbility 里把 formId 回传卡片(onAddForm)

这一步是“把卡片的 formId 写进 LocalStorageProp 里”,后面卡片里就能用 @LocalStorageProp('formId') 拿到。

 // src/main/ets/widgeteventcallcard/EntryFormAbility.ets
 import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility';
 import formBindingData from '@ohos.app.form.formBindingData';
 import formInfo from '@ohos.app.form.formInfo';
 ​
 export default class EntryFormAbility extends FormExtensionAbility {
   onAddForm(want: formInfo.Want): formBindingData.FormBindingData {
     // 系统传进来的卡片 id
     const formId = want.parameters[formInfo.FormParam.IDENTITY_KEY] as string;
 ​
     // 把 formId 放到绑定数据中,给卡片页面使用
     const data: Record<string, string> = {
       formId: formId
     };
     return formBindingData.createFormBindingData(data);
   }
 }

步骤 2:卡片页面 —— 布局按钮 + postCardAction(call)

你给的示例是用 RelativeContainer 做定位。我们用一个更清晰的写法保留原意:

  • 按钮 A:调用 method = 'funA',只传 formId
  • 按钮 B:调用 method = 'funB',传 formId + num
 // src/main/ets/widgeteventcallcard/pages/WidgetEventCall.ets
 @Entry
 @Component
 struct WidgetEventCall {
   // 从 EntryFormAbility 回传来的 formId
   @LocalStorageProp('formId') formId: string = '0';
 ​
   private funAText: string = '按钮A';
   private funBText: string = '按钮B';
 ​
   build() {
     Column() {
       Button(this.funAText)
         .fontSize(18)
         .fontWeight(FontWeight.Bold)
         .onClick(() => {
           // 通过 call 事件拉起指定 UIAbility 到后台,并调用 funA
           postCardAction(this, {
             action: 'call',
             // 只能写当前应用里的 UIAbility,名字要和 module.json5 中一致
             abilityName: 'WidgetEventCallEntryAbility',
             params: {
               formId: this.formId,
               method: 'funA'  // ⚠ 必填:要调用的方法名
             }
           });
         })
 ​
       Button(this.funBText)
         .fontSize(18)
         .fontWeight(FontWeight.Bold)
         .margin({ top: 10 })
         .onClick(() => {
           // 调用 funB,多传一个 num 参数
           postCardAction(this, {
             action: 'call',
             abilityName: 'WidgetEventCallEntryAbility',
             params: {
               formId: this.formId,
               method: 'funB',
               num: 1
             }
           });
         })
     }
     .width('100%')
     .height('100%')
   }
 }

要点总结:

  • action: 'call' 固定写法;
  • abilityName 必须是当前应用里配置过的 UIAbility 名;
  • params.method 为必须字段,类型必须是 string
  • 其他参数(如 formIdnum)根据业务自定义。

步骤 3:指定的 UIAbility 监听 call 事件(后台执行方法)

这里用的是带 rpc.Parcelable 的高级写法:可以返回一个自定义对象回去。

 // src/main/ets/widgeteventcallcard/WidgetEventCallEntryAbility/WidgetEventCallEntryAbility.ets
 import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
 import { BusinessError } from '@kit.BasicServicesKit';
 import { rpc } from '@kit.IPCKit';
 import { hilog } from '@kit.PerformanceAnalysisKit';
 ​
 const TAG: string = 'WidgetEventCallEntryAbility';
 const DOMAIN_NUMBER: number = 0xFF00;
 const CONST_NUMBER_1: number = 1;
 const CONST_NUMBER_2: number = 2;
 ​
 // 1. 定义一个 Parcelable 类型,用来作为 call 的返回结果
 class MyParcelable implements rpc.Parcelable {
   num: number;
   str: string;
 ​
   constructor(num: number, str: string) {
     this.num = num;
     this.str = str;
   }
 ​
   // 序列化:写入到 MessageSequence 中
   marshalling(messageSequence: rpc.MessageSequence): boolean {
     messageSequence.writeInt(this.num);
     messageSequence.writeString(this.str);
     return true;
   }
 ​
   // 反序列化:从 MessageSequence 中读出
   unmarshalling(messageSequence: rpc.MessageSequence): boolean {
     this.num = messageSequence.readInt();
     this.str = messageSequence.readString();
     return true;
   }
 }
 ​
 // 2. UIAbility:被 call 事件拉起到后台执行
 export default class WidgetEventCallEntryAbility extends UIAbility {
   // 第一次通过 call 事件拉起 UIAbility 时,会触发 onCreate
   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
     try {
       // 监听方法 funA
       this.callee.on('funA', (data: rpc.MessageSequence): MyParcelable => {
         // data 是卡片那边通过 params 传来的参数内容(字符串)
         const paramsStr: string = data.readString();
         hilog.info(DOMAIN_NUMBER, TAG, `FunACall param: ${paramsStr}`);
 ​
         // 这里可以做一些业务逻辑,比如根据 formId 更新卡片、操作数据库等
         // 示例:返回一个 MyParcelable 结果
         return new MyParcelable(CONST_NUMBER_1, 'aaa');
       });
 ​
       // 监听方法 funB
       this.callee.on('funB', (data: rpc.MessageSequence): MyParcelable => {
         const paramsStr: string = data.readString();
         hilog.info(DOMAIN_NUMBER, TAG, `FunBCall param: ${paramsStr}`);
 ​
         // 同理可以做其他业务逻辑
         return new MyParcelable(CONST_NUMBER_2, 'bbb');
       });
     } catch (err) {
       hilog.error(
         DOMAIN_NUMBER,
         TAG,
         `Failed to register callee on. Cause: ${JSON.stringify(err as BusinessError)}`
       );
     }
   }
 ​
   // 进程结束时,解除监听,避免内存泄露
   onDestroy(): void | Promise<void> {
     try {
       this.callee.off('funA');
       this.callee.off('funB');
     } catch (err) {
       hilog.error(
         DOMAIN_NUMBER,
         TAG,
         `Failed to register callee off. Cause: ${JSON.stringify(err as BusinessError)}`
       );
     }
   }
 }

你可以把这里的 hilog.info(...) 换成:

  • 真正的后台业务逻辑(比如调用接口、更新数据库);
  • 再用 formProvider.updateForm(formId, data) 去刷新卡片内容(结合前面学习的 message/router/call 刷新卡片用法)。博客园+1

步骤 4:配置后台运行权限(KEEP_BACKGROUND_RUNNING)

call 事件要拉起 UIAbility 到后台,有权限限制:需要在 module.json5 里声明后台运行权限。CSDN博客+1

 // src/main/module.json5
 {
   // ...
   "requestPermissions": [
     {
       "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
     }
   ]
 }

否则:

  • 虽然卡片可以发送 call 事件;
  • 但 UIAbility 可能无法被真正保持在后台运行,业务逻辑也可能无法正确执行。

步骤 5:在 module.json5 里配置对应 UIAbility

最后,要在 abilities 数组里把 WidgetEventCallEntryAbility 配置上,名字必须和卡片调用时的 abilityName 一致。CSDN博客+1

 // src/main/module.json5
 {
   // ...
   "abilities": [
     {
       "name": "WidgetEventCallEntryAbility",
       "srcEntry": "./ets/widgeteventcallcard/WidgetEventCallEntryAbility/WidgetEventCallEntryAbility.ets",
       "description": "$string:WidgetEventCallCard_desc",
       "icon": "$media:app_icon",
       "label": "$string:WidgetEventCallCard_label",
       "startWindowIcon": "$media:app_icon",
       "startWindowBackground": "$color:start_window_background"
     }
   ]
 }

四、开发时容易踩的坑小总结(方便你记)

  1. method 必须是 string 且必填

    • 卡片里:params: { method: 'funA', ... }
    • UIAbility:this.callee.on('funA', handler) 中的 'funA' 必须完全一致。
  2. 只能拉起自己应用的 UIAbility

    • abilityName 必须是当前模块 module.json5.abilities 中已有的名称。CSDN博客+1
  3. 记得加 KEEP_BACKGROUND_RUNNING 权限

    • 不然 call 很可能不生效 / 或 UIAbility 立刻被系统回收。
  4. formId 传递链要打通

    • onAddFormformBindingData → 卡片的 @LocalStorageProp('formId')postCardAction params.formId → UIAbility 解析后再通过 updateForm 回写卡片。
  5. 调试时可以先不返回 Parcelable

    • 最简单版本可以 return null;,先确认 call 能走通、参数能够打印出来,再加复杂返回值。