Harmony os ArkTS 卡片生命周期管理:我怎么把 EntryFormAbility 用顺手的
前一篇我刚把 ArkTS 卡片的"进程模型"捋了一遍,这次来啃另外一块核心:卡片生命周期。
因为只要你一写 ArkTS 卡片,就一定会遇到 FormExtensionAbility,也一定会和那几个生命周期打交道:
onAddForm
onUpdateForm
onFormEvent
onRemoveForm
......
这篇就当是我给自己写的一份"生命周期攻略",顺便把官方那段示例代码拆开讲一遍,加上一些我自己的理解和使用习惯。
- 卡片生命周期的核心角色:EntryFormAbility
ArkTS 卡片这条线,真正负责生命周期的,是继承自 FormExtensionAbility 的类。
官方示例里的名字是 EntryFormAbility,大致结构是这样的:
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { Configuration, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'EntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class EntryFormAbility extends FormExtensionAbility {
// 一堆生命周期回调都写在这里
}
这段 import 基本就是 ArkTS 卡片生命周期的"必选包":
FormExtensionAbility:生命周期基类;
formBindingData:用来构造卡片要绑定的数据;
formInfo:包含一些枚举和参数 key,比如 FormParam、FormState;
formProvider:提供方更新卡片的主要出口;
Configuration:系统配置(横竖屏、语言等)变化时会用到;
Want:携带参数过来的那一坨东西;
BusinessError + hilog:异常处理 + 打 log 的标配。
接下来就轮到一个一个生命周期出场了。
- onAddForm:卡片"出生"的那一刻
触发场景:
卡片使用方(比如桌面)创建一张新的卡片实例时,系统会回调 onAddForm。
onAddForm(want: Want): formBindingData.FormBindingData {
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onAddForm');
hilog.info(DOMAIN_NUMBER, TAG, want.parameters?.[formInfo.FormParam.NAME_KEY] as string);
// 卡片使用方创建卡片时触发,提供方需要返回卡片数据绑定类
let obj: Record<string, string> = {
'title': 'titleOnAddForm',
'detail': 'detailOnAddForm'
};
let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
return formData;
}
几个重点:
卡片相关信息从 want.parameters 里取
比如卡片名称、规格、表单 ID 等信息,都可以通过 FormParam 相关的 key 从 want 里拿出来,用来区分不同的卡片实例。
必须返回 FormBindingData
这就是卡片渲染时绑定的初始数据:
你在 ArkTS 卡片 UI 里写的 @State title、detail,最后都要对应到这里的字段名;
一般我会把这里当作"卡片第一帧"的数据初始化入口。
建议在这里做的事情:
根据卡片类型 / 参数,决定初始展示什么内容;
把卡片实例相关的信息(formId、一些业务参数)持久化一下,后续更新和删除会用到。
- onUpdateForm:卡片"刷新"的主战场
触发场景:
支持定时更新 / 定点更新;
或者卡片使用方主动请求更新;
总之:只要是"刷新数据"的时机,一般都会回调到这里。
onUpdateForm(formId: string): void {
// 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onUpdateForm');
let obj: Record<string, string> = {
'title': 'titleOnUpdateForm',
'detail': 'detailOnUpdateForm'
};
let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
formProvider.updateForm(formId, formData).catch((error: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] updateForm, error:' + JSON.stringify(error));
});
}
这里有几件事要记牢:
formId 是关键
系统通过 formId 来标识每一个卡片实例;
你要更新哪一张卡片,就把对应的 formId 和数据一起传给 formProvider.updateForm。
数据更新流程是"先拼数据,再调用 updateForm"
和 onAddForm 一样,先用 createFormBindingData 组一份数据;
然后通过 formProvider.updateForm(formId, formData) 推过去。
不要在这里做长耗时操作
官方已经提醒了:FormExtensionAbility 进程不会常驻,生命周期回调结束后只多给你大概 10 秒缓冲。超过这个级别的任务应该:
拉起主应用(UIAbility)去干活;
做完之后再通过 updateForm() 来通知卡片刷新。
我自己在脑子里是把 onUpdateForm 当成一个"调度+组装数据"的地方,而不是把所有业务逻辑都塞这里。
- onFormEvent:卡片"被点了"之后的故事
触发场景:
卡片里某个可以交互的区域(比如按钮)触发了事件,
你在配置里把事件传出来,就会走到 onFormEvent。
onFormEvent(formId: string, message: string): void {
// 若卡片支持触发事件,则需要重写该方法并实现对事件的触发
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onFormEvent');
// ...
}
典型用法:
message 里自定义一套"小协议",比如:
"REFRESH" → 重新拉数据并更新卡片;
"OPEN_DETAIL:12345" → 拉起主应用并打开 ID 为 12345 的详情页;
这里面可以根据不同 formId,做不同实例的区分。
一个我比较喜欢的模式是:
onFormEvent 里尽量做轻量操作 + 跳转主应用;
真正复杂的逻辑都放在 UIAbility 里做;
完成后再反向更新卡片内容,保持卡片和应用数据一致。
- onRemoveForm:卡片"被删掉"要收尾
触发场景:
用户删除卡片,或者宿主把卡片移除了,对应实例就要清理。
onRemoveForm(formId: string): void {
// 删除卡片实例数据
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onRemoveForm');
// 删除之前持久化的卡片实例数据
// 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例
}
建议在这里做的是:
删除和这个 formId 关联的所有持久化数据(比如本地数据库、文件中的记录);
做一些资源回收的统计(如果你有埋点系统的话);
确保下次 onAddForm 新建的时候,不会被旧数据干扰。
简单来说:卡片被删掉了,你就把它在你这边的"户口本"也注销掉。
- onChangeFormVisibility:卡片"看得见/看不见"这回事
onChangeFormVisibility(newStatus: Record<string, number>): void {
// 卡片使用方发起可见或者不可见通知触发,提供方需要做相应的处理,仅系统应用生效
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onChangeFormVisibility');
}
这个回调更多是系统应用会用到(文档也写了"仅系统应用生效");
但理解它的含义还是有价值的:
宿主可以告诉你某些卡片当前是"可见"还是"不可见"。
如果有一天三方应用这一块放开,对节省资源会非常有帮助,比如:
卡片不可见时,可以减少刷新频率;
可见时再恢复正常更新。
- onCastToNormalForm:一般场景可以先放一边
onCastToNormalForm(formId: string): void {
// 当前卡片使用方不会涉及该场景,无需实现该回调函数
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onCastToNormalForm');
}
文档已经写得很明白:当前卡片使用方不会涉及该场景。
我个人的处理就是:
保留回调,写个日志方便以后排查问题;
逻辑上不用太纠结它,等未来有新版本或新场景用到再说。
- onConfigurationUpdate:配置变更别"装没看见"
onConfigurationUpdate(config: Configuration) {
// 当前formExtensionAbility存活时更新系统配置信息时触发的回调。
// 需注意:formExtensionAbility创建后10秒内无操作将会被清理。
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onConfigurationUpdate:' + JSON.stringify(config));
}
这里主要针对的是系统配置变化,例如:
语言切换;
深浅色模式切换;
屏幕方向等。
需要注意两点:
只有当 FormExtensionAbility 还活着时,这个回调才会触发;
再次提醒那句:创建后 10 秒如果没有新生命周期回调,进程就会被清掉。
实际开发中,如果卡片对多语言敏感,或者你想根据系统深浅色模式动态调整卡片展示,可以考虑利用这一点做一些处理。
- onAcquireFormState:返回卡片当前的"状态"
onAcquireFormState(want: Want) {
// 卡片提供方接收查询卡片状态通知接口,默认返回卡片初始状态。
return formInfo.FormState.READY;
}
宿主 / 系统可能会来问:"这张卡片现在是什么状态?"
默认给的是 FormState.READY(准备就绪);
如果需要,你也可以根据自身业务做一些区分。
现在场景下,这个回调一般不会写特别复杂的逻辑,但最好还是保留并按需调整。
- 关于 FormExtensionAbility 进程那 10 秒的事
文档最后那段说明其实非常关键,我单独拿出来说一下:
FormExtensionAbility 进程不能常驻后台
生命周期调度完成后,会继续存在约 10 秒,
如果这 10 秒内没有新的生命周期回调触发,进程就会被自动退出。
这句话背后的设计思路,很明显:
卡片是轻量入口,不应该像一个完整 App 一样长期常驻;
卡片相关逻辑的执行应该短平快;
真正复杂的业务,让主应用去干,然后通过 updateForm 把结果同步回卡片。
所以我写 ArkTS 卡片生命周期的"心态"是这样的:
生命周期回调只做三件事:
快速读入参数 / 状态;
组装一份卡片需要的绑定数据;
调用 updateForm 或返回 FormBindingData 就收工。
复杂逻辑、耗时操作全部丢给 UIAbility:
比如网络请求、数据聚合、文件读写;
需要用户交互的流程也应该直接拉起主应用界面去处理。
把 FormExtensionAbility 当成一个"调度与数据拼装中心",而不是一个"大而全的业务中心"。
- 小结:把这些生命周期变成自己的"工具箱"
整理下来,其实 ArkTS 卡片的生命周期就像一套挂钩:
onAddForm:初始化挂钩(第一次出生);
onUpdateForm:刷新挂钩(定时 / 主动更新);
onFormEvent:交互挂钩(用户点了卡片);
onRemoveForm:清理挂钩(实例被删);
onChangeFormVisibility / onConfigurationUpdate:状态变化挂钩;
onAcquireFormState:状态查询挂钩。
理解每个 hook 对应的"时机 + 责任",再结合前一篇的进程模型一起看,整条链路就会很清晰:
谁负责管生命周期(FormExtensionAbility 进程);
谁负责渲染(卡片渲染服务进程);
谁对用户可见(卡片使用方进程);
这些回调触发的时候,进程处在什么状态、能做什么、不能做什么。