一、call 事件是什么?可以做什么?
场景: 比如音乐卡片、下载卡片、计时器卡片——用户在桌面点一下卡片上的按钮,就希望:
- 不一定打开 App 界面;
- 但能让 App 在后台执行某个功能:播放、暂停、继续下载、同步数据等。华为开发者+1
这时就用到 动态卡片 + postCardAction 的 call 能力:
postCardAction(..., { action: 'call', ... })- 将 卡片提供方应用的某个 UIAbility 拉到后台运行;
- 同时可以传递参数,并指定要调用的“方法名”(字符串);
- UIAbility 在后台执行对应逻辑(比如 funA / funB)。
⚠️ 注意:
- 本文说的是 动态卡片(WidgetCard + EntryFormAbility + postCardAction)。
- 静态卡片用的是 FormLink,
二、整体流程一图理解
-
卡片创建时:
EntryFormAbility.onAddForm把formId等信息通过formBindingData传给卡片组件(LocalStorageProp 接收)。博客园+1 -
用户点击卡片按钮(如「按钮A」/「按钮B」):
- 在
WidgetEventCall.ets中,通过postCardAction(this, { action: 'call', abilityName: 'XXXEntryAbility', params: {...} })发送call事件; params.method表示要调用哪个方法(如'funA'、'funB'),是 必填,必须是string。CSDN博客+1
- 在
-
UIAbility 被后台拉起:
- 如果此前没启动过 → 触发
onCreate; - 如果已经在后台 → 触发
onNewWant; - 在
onCreate中通过this.callee.on('funA', handler)监听 call 事件对应的方法。知乎专栏+1
- 如果此前没启动过 → 触发
-
UIAbility 处理参数并执行逻辑:
- 通过
rpc.MessageSequence拿到卡片传来的参数; - 做完业务逻辑后,可以通过
formProvider.updateForm回写卡片内容; - 若需要返回值给调用方,可以返回一个实现了
rpc.Parcelable的对象。
- 通过
-
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;- 其他参数(如
formId、num)根据业务自定义。
步骤 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"
}
]
}
四、开发时容易踩的坑小总结(方便你记)
-
method 必须是 string 且必填
- 卡片里:
params: { method: 'funA', ... } - UIAbility:
this.callee.on('funA', handler)中的'funA'必须完全一致。
- 卡片里:
-
只能拉起自己应用的 UIAbility
abilityName必须是当前模块module.json5.abilities中已有的名称。CSDN博客+1
-
记得加 KEEP_BACKGROUND_RUNNING 权限
- 不然 call 很可能不生效 / 或 UIAbility 立刻被系统回收。
-
formId 传递链要打通
onAddForm→formBindingData→ 卡片的@LocalStorageProp('formId')→postCardAction params.formId→ UIAbility 解析后再通过updateForm回写卡片。
-
调试时可以先不返回 Parcelable
- 最简单版本可以
return null;,先确认 call 能走通、参数能够打印出来,再加复杂返回值。
- 最简单版本可以