一个应用多个卡片——多 FormAbility 注册与 call 事件后台唤起完整指南
文章目录一个应用注册多个卡片call 事件静默后台唤起call 事件卡片 UI 端call 事件UIAbility 端注册 Calleecall 事件 vs router 事件通过卡片刷新数据的完整对比多 FormAbility 组织原则call 事件使用前提权限配置常见坑写在最后一个应用只有一个卡片那太简单了。真实项目里一个应用往往需要提供多种卡片日程卡片、天气卡片、快捷操作卡片……而且卡片点击后可能需要在不打扰用户的前提下唤起后台逻辑。这篇基于StageServiceWidgetCards这个综合示例讲两件事如何在一个应用里注册多个 FormAbility多卡片类型call事件如何静默唤起 UIAbility 后台方法一个应用注册多个卡片StageServiceWidgetCards是一个综合示例里面有十几种卡片类型每种都有独立的 FormAbility 和配置文件。module.json5 的关键结构// entry/src/main/module.json5 { module: { abilities: [ // 主 UIAbility { name: EntryAbility, srcEntry: ./ets/entryability/EntryAbility.ts, ... }, // 卡片 router 事件的目标 UIAbility { name: WidgetEventRouterEntryAbility, srcEntry: ./ets/widgetevententryability/WidgetEventRouterEntryAbility.ts, ... }, // 卡片 call 事件的目标 UIAbilityCallee 模式 { name: WidgetCalleeEntryAbility, srcEntry: ./ets/widgetcalleeentryability/WidgetCalleeEntryAbility.ts, ... }, // 注册了 call 事件的 Ability { name: WidgetEventCallEntryAbility, srcEntry: ./ets/widgeteventcallentryability/WidgetEventCallEntryAbility.ets, ... } ], extensionAbilities: [ // 卡片1基础卡片事件 { name: EntryFormAbility, srcEntry: ./ets/entryformability/EntryFormAbility.ts, type: form, metadata: [{ name: ohos.extension.form, resource: $profile:form_config }] }, // 卡片2图片更新卡片 { name: WgtImgUpdateEntryFormAbility, srcEntry: ./ets/wgtimgupdateentryformability/WgtImgUpdateEntryFormAbility.ts, type: form, metadata: [{ name: ohos.extension.form, resource: $profile:form_imgupdate_config }] }, // 卡片3定时刷新卡片 { name: UpdateByTimeFormAbility, srcEntry: ./ets/updatebytimeformability/UpdateByTimeFormAbility.ts, type: form, metadata: [{ name: ohos.extension.form, resource: $profile:form_updatebytime_config }] }, // 卡片4状态驱动刷新卡片 { name: UpdateByStatusFormAbility, srcEntry: ./ets/updatebystatusformability/UpdateByStatusFormAbility.ts, type: form, metadata: [{ name: ohos.extension.form, resource: $profile:form_updatebystatus_config }] }, // 卡片5JS 卡片 { name: JsCardFormAbility, srcEntry: ./ets/jscardformability/JsCardFormAbility.ts, type: form, metadata: [{ name: ohos.extension.form, resource: $profile:form_jscard_config }] }, // 卡片6Callee 卡片call 事件刷新 { name: WidgetCalleeFormAbility, srcEntry: ./ets/widgetcalleeformability/WidgetCalleeFormAbility.ts, type: form, metadata: [{ name: ohos.extension.form, resource: $profile:form_widgetcallee_config }] }, // 卡片7持久化数据卡片 { name: PersistentDataFormAbility, srcEntry: ./ets/persistentdataformability/PersistentDataFormAbility.ts, type: form, metadata: [{ name: ohos.extension.form, resource: $profile:form_persistentdata_config }] } // ... 更多卡片 ], requestPermissions: [ { name: ohos.permission.KEEP_BACKGROUND_RUNNING }, // 后台任务权限 { name: ohos.permission.START_ABILITIES_FROM_BACKGROUND }, // 后台启动权限 { name: ohos.permission.INTERNET, ... } // 网络权限图片更新卡片需要 ] } }关键结论多少种卡片类型就需要多少个 FormExtensionAbility各自独立配置各自的卡片配置文件profile。call 事件静默后台唤起call事件和router事件的最大区别事件目标应用是否到前台用途routerUIAbility是拉到前台打开应用查看详情callUIAbilityCallee 方法否在后台运行静默执行操作、刷新卡片数据大白话版router是打电话对方得接听call是发短信对方不需要看手机就能处理。call 事件卡片 UI 端// entry/src/main/ets/widgeteventcall/pages/WidgetEventCallCard.etsletstorageEventCallnewLocalStorage();Entry(storageEventCall)Componentstruct WidgetEventCallCard{LocalStorageProp(formId)formId:string12400633174999288;privatefunA:Resource$r(app.string.ButtonA_label);privatefunB:Resource$r(app.string.ButtonB_label);build(){RelativeContainer(){// 按钮 A触发 call 事件调用后台 funA 方法Button(this.funA).id(funA__).fontSize(14).fontWeight(FontWeight.Bold).alignRules({center:{anchor:__container__,align:VerticalAlign.Center},middle:{anchor:__container__,align:HorizontalAlign.Center}}).onClick((){postCardAction(this,{action:call,// call 事件abilityName:WidgetEventCallEntryAbility,// 目标 UIAbility必须在同一应用内params:{formId:this.formId,method:funA// 要调用的方法名}});})// 按钮 B触发另一个方法Button(this.funB).id(funB__).fontSize(14).fontWeight(FontWeight.Bold).margin({top:10}).alignRules({top:{anchor:funA__,align:VerticalAlign.Bottom},middle:{anchor:__container__,align:HorizontalAlign.Center}}).onClick((){postCardAction(this,{action:call,abilityName:WidgetEventCallEntryAbility,params:{formId:this.formId,method:funB,num:1// 额外参数}});})}.height(100%).width(100%)}}call 事件UIAbility 端注册 Calleecall事件的接收方是 UIAbility 里的 Callee 机制需要在onCreate里注册// entry/src/main/ets/widgeteventcallentryability/WidgetEventCallEntryAbility.etsimport{AbilityConstant,UIAbility,Want}fromkit.AbilityKit;import{formBindingData,formProvider}fromkit.FormKit;import{rpc}fromkit.IPCKit;import{hilog}fromkit.PerformanceAnalysisKit;import{BusinessError}fromkit.BasicServicesKit;constTAGWidgetEventCallEntryAbility;constDOMAIN_NUMBER0xFF00;// funA 方法的实现constfunACallback(data:rpc.MessageSequence):rpc.Parcelable{hilog.info(DOMAIN_NUMBER,TAG,funA called);// 从 MessageSequence 里读取 formIdconstformIddata.readString();hilog.info(DOMAIN_NUMBER,TAG,funA called with formId:${formId});// 通过 formProvider.updateForm 刷新卡片数据constformData:Recordstring,string{calleeDetail:FunACall param// 对应卡片 LocalStorageProp(calleeDetail)};formProvider.updateForm(formId,formBindingData.createFormBindingData(formData)).catch((error:BusinessError){hilog.error(DOMAIN_NUMBER,TAG,updateForm 失败:${JSON.stringify(error)});});// 必须返回 Parcelable 对象returnnewMyParcelable(0,funA success);};constfunBCallback(data:rpc.MessageSequence):rpc.Parcelable{hilog.info(DOMAIN_NUMBER,TAG,funB called);constformIddata.readString();constnumdata.readInt();// 读取额外参数hilog.info(DOMAIN_NUMBER,TAG,funB called: formId${formId}, num${num});constformData:Recordstring,string{calleeDetail:FunBCall param:${num}};formProvider.updateForm(formId,formBindingData.createFormBindingData(formData));returnnewMyParcelable(0,funB success);};// Parcelable 实现类必须实现 marshalling 和 unmarshallingclassMyParcelableimplementsrpc.Parcelable{num:number0;str:string;constructor(num:number,str:string){this.numnum;this.strstr;}marshalling(messageSequence:rpc.MessageSequence):boolean{messageSequence.writeInt(this.num);messageSequence.writeString(this.str);returntrue;}unmarshalling(messageSequence:rpc.MessageSequence):boolean{this.nummessageSequence.readInt();this.strmessageSequence.readString();returntrue;}}exportdefaultclassWidgetEventCallEntryAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{hilog.info(DOMAIN_NUMBER,TAG,onCreate);// 注册 Callee 方法// 方法名要和卡片端 postCardAction 里的 method 字段一致try{this.callee.on(funA,funACallback);this.callee.on(funB,funBCallback);hilog.info(DOMAIN_NUMBER,TAG,Callee 注册成功);}catch(error){consterrerrorasBusinessError;hilog.error(DOMAIN_NUMBER,TAG,Callee 注册失败:${err.code}${err.message});}}onDestroy():void{// 销毁时注销 Calleetry{this.callee.off(funA);this.callee.off(funB);}catch(error){hilog.error(DOMAIN_NUMBER,TAG,Callee 注销失败);}}}call 事件 vs router 事件通过卡片刷新数据的完整对比多 FormAbility 组织原则管理多个卡片时建议遵循这套组织方式entry/src/main/ets/ ├── 卡片类型1/ │ ├── widgetabilify/ │ │ └── Card1FormAbility.ets ← FormExtensionAbility │ └── pages/ │ └── Card1.ets ← 卡片 UI ├── 卡片类型2/ │ ├── widgetabilify/ │ │ └── Card2FormAbility.ets │ └── pages/ │ └── Card2.ets ├── 公共工具/ │ └── CommonFormUtils.ets ← 抽取公共逻辑Preferences读写、错误处理 └── entryability/ └── EntryAbility.ets ← 主UIAbility每种卡片类型对应一个目录FormAbility 和 UI 放在一起方便维护。公共逻辑Preferences 工具类、错误处理抽取出来复用。call 事件使用前提权限配置call事件的目标 UIAbility 在后台启动需要声明权限// entry/src/main/module.json5 { module: { requestPermissions: [ // call 事件从后台启动 Ability 需要此权限 { name: ohos.permission.KEEP_BACKGROUND_RUNNING }, { name: ohos.permission.START_ABILITIES_FROM_BACKGROUND } ] } }少了这两个权限call事件会失败但不一定报显眼的错误排查起来比较麻烦。常见坑坑1call 事件的 abilityName 必须是同一应用内的 UIAbility不能跨应用这是系统限制。如果填了其他应用的 abilityNamepostCardAction会默默失败。坑2Callee.on 要在 onCreate 里注册而不是 onForegroundcall事件启动的 UIAbility 不会走onForeground只走onCreate或onNewWant。如果注册在onForeground里call 事件会收不到。坑3MessageSequence 读写顺序必须严格对应发送端用params传递接收端用MessageSequence.readString()/readInt()读取读写顺序必须严格一致否则数据错乱。建议先传 formIdString再传其他参数并在代码里注释清楚。坑4多个 FormAbility 的 form_config.json 文件名不能重复每个 FormAbility 的metadata.resource指向不同的配置文件比如$profile:form_config、$profile:form_imgupdate_config。文件名重复了会互相覆盖导致某些卡片类型找不到配置。写在最后多卡片开发是 FormKit 的进阶场景主要考验的是项目结构的组织能力和各种配置的准确性。StageServiceWidgetCards这个项目展示了几乎所有卡片开发模式如果你遇到了某个具体场景直接去里面找对应的示例。call事件是一个被低估的功能。很多开发者习惯用router做所有交互但对于点卡片触发刷新这类场景call明显更合适——用户不会被带离桌面体验更流畅。