一、卡片添加/移除的场景以及相关卡片生命周期:
目前卡片添加/移除至桌面没有直接的回调响应,可以通过onAddForm和onRemoveForm相结合记录保存到桌面的卡片。
onAddForm/onRemoveForm添加至桌面的生命周期触发流程如下:
例如:打开下面的卡片列表,卡片列表中有3张卡片,可以看到日志中onAddForm回调了3次。


例如:点击x号取消了服务卡片列表,则会触发onRemoveForm回调3次。


例如:添加一张卡片至桌面,则会onUpdateForm选中的卡片,onRemoveForm另外两张卡片。



例如:在桌面移除一张卡片,会onRemoveForm当前移除的卡片。


综上,将卡片添加至桌面的流程中,生命周期回调onAddForm/onRemoveForm会执行多次,但是最终卡片新增还是移除是确定的。
二、实现卡片添加/移除管理以及主应用获取卡片id:
本方案使用首选项通过在服务卡片中onAddForm/onRemoveForm回调函数中,对formId进行持久化缓存,并在主应用中进行跨进程读取。以下是实现步骤:
前置步骤创建服务卡片请参考官网,不再赘述。
import { preferences } from '@kit.ArkData';
// 公共模块PreferencesHelper.ets用于存取formId
export class PreferencesHelper {
private static instance: PreferencesHelper | null = null;
private constructor() {}
public static getInstance() {
if (PreferencesHelper.instance === null) {
PreferencesHelper.instance = new PreferencesHelper();
}
return PreferencesHelper.instance;
}
public get(context: Context, key: string, defaultValue: preferences.ValueType) {
preferences.removePreferencesFromCacheSync(context, 'preferences');
let preference = preferences.getPreferencesSync(context, { name: 'preferences' });
return preference.getSync(key, defaultValue);
}
public put(context: Context, key: string, defaultValue: preferences.ValueType) {
preferences.removePreferencesFromCacheSync(context, 'preferences');
let preference = preferences.getPreferencesSync(context, { name: 'preferences' });
preference.putSync(key, defaultValue);
preference.flushSync();
}
} import { ConfigurationConstant, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(): void {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
AppStorage.setOrCreate('AppContext', this.context.getApplicationContext());
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
} import { formBindingData, FormExtensionAbility, formInfo } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { commonEventManager } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { PreferencesHelper } from '../common/PreferencesHelper';
const DOMAIN = 0x0000;
export default class EntryFormAbility extends FormExtensionAbility {
// 服务卡片FormExtensionAbility.ets,添加formId
onAddForm(want: Want) {
let formList: Array<string> =
PreferencesHelper.getInstance().get(this.context.getApplicationContext(), 'formList', []) as Array<string>;
if (want.parameters) {
let formId = (want.parameters['ohos.extra.param.key.form_identity']) as string;
hilog.info(DOMAIN, 'testTag', `add formId: ${formId}`);
if (!formList.includes(formId)) {
formList.push(formId);
}
PreferencesHelper.getInstance().put(this.context.getApplicationContext(), 'formList', formList);
}
const formData = '';
return formBindingData.createFormBindingData(formData);
}
onCastToNormalForm() {
// Called when the form provider is notified that a temporary form is successfully
// converted to a normal form.
}
onUpdateForm() {
// Called to notify the form provider to update a specified form.
hilog.info(DOMAIN, 'testTag', `onUpdateForm`);
}
onFormEvent() {
// Called when a specified message event defined by the form provider is triggered.
}
// 服务卡片FormExtensionAbility.ets,移除formId
onRemoveForm(formId: string) {
// Called to notify the form provider that a specified form has been destroyed.
let formList: Array<string> =
PreferencesHelper.getInstance().get(this.context.getApplicationContext(), 'formList', []) as Array<string>;
formList = formList.filter(item => item !== formId);
hilog.info(DOMAIN, 'testTag', `del formId: ${formId}`);
PreferencesHelper.getInstance().put(this.context.getApplicationContext(), 'formList', formList);
commonEventManager.publish('delCard', { data: formId, bundleName: 'com.example.formKit_preferences' }, err => {
if (err) {
hilog.info(DOMAIN, 'testTag', `onRemoveForm publish fail, errCode: ${err.code}, errMsg: ${err.message}`);
}
hilog.info(DOMAIN, 'testTag', 'onRemoveForm publish success');
});
}
onAcquireFormState() {
// Called to return a {@link FormState} object.
return formInfo.FormState.READY;
}
}; import { commonEventManager } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { PreferencesHelper } from '../common/PreferencesHelper';
const DOMAIN = 0x0000;
@Entry
@Component
struct Index {
private subscriber: commonEventManager.CommonEventSubscriber | null = null;
private subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
events: ['delCard'],
};
aboutToAppear(): void {
this.subscriber = commonEventManager.createSubscriberSync(this.subscribeInfo);
if (this.subscriber) {
commonEventManager.subscribe(this.subscriber, (err, data) => {
if (err) {
hilog.info(DOMAIN, 'testTag', `delCard err: ${err}`);
}
hilog.info(DOMAIN, 'testTag', `delCard 传递的数据: ${data.data}`);
});
}
}
build() {
RelativeContainer() {
Text('Hello World')
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
// 主应用Index.ets,在主应用中对formId进行读取
let formList: Array<string> = PreferencesHelper.getInstance().get(AppStorage.get('AppContext') as Context,
'formList', []) as Array<string>;
hilog.info(DOMAIN, 'testTag', `formList size ${formList.length}`);
for (let index = 0; index < formList.length; index++) {
const element = formList[index];
hilog.info(DOMAIN, 'testTag', `formId ${element}`);
}
})
}
.height('100%')
.width('100%')
}
} Q:跨进程读取首选项(以主应用读取服务卡片写入的首选项为例)为何读取不到?
A:由于context不同,跨模块、多页面场景下可能无法获取数据。建议在EntryAbility中使用单例类存储全局context,或使用应用级context。
Q:是否可以使用commonEventManager来进行跨进程监听服务卡片的卡片列表中某个卡片添加/移除?
A:该方案可以监听到服务卡片变化,但是在主应用未启动或者重启等场景下,因为主应用进程不存在,导致无法实时监听变化,所以不推荐该方案。
Q:是否可以使用PersistentStorage和AppStorage在应用和卡片之间进行状态数据共享?
A:卡片和应用的主进程不是一个进程,而AppStorage是和进程绑定的,仅支持应用的主线程内多个UIAbility实例间的UI状态数据共享,所以卡片和应用主进程之间无法使用PersistentStorage和AppStorage进行状态数据共享。
Q:服务卡片生命周期里的onCastToNormalForm方法,在什么时候执行?
A:onCastToNormalForm在卡片使用方将临时卡片转换为常态卡片时触发,可能涉及数据固化、权限申请或状态同步至服务端。参考管理ArkTS卡片生命周期,onCastToNormalForm事件是否能触发,取决于是否有卡片使用方使用,当前主要卡片使用方就是桌面;由于当前桌面不会使用临时卡片,所以真机上暂时没有场景触发卡片提供方的onCastToNormalForm。
Q:删除form_config.json中的卡片配置并重新安装应用后,原卡片仍显示在桌面?
A:可以修改AppScope/app.json5文件下的versionCode版本号。
Q:为什么在添加卡片时,onUpdateForm函数有时会被触发,有时无法触发?
A:onUpdateForm函数是卡片提供方接收更新参数通知的接口。该函数的触发行为与系统版本相关:在API 20之前的版本中添加卡片时,该函数不会被触发;而在API 20及后续版本添加卡片时会正常触发。为确保兼容性,建议开发者在onAddForm函数及onUpdateForm函数中同时处理初始化(如初始化首选项)及卡片更新逻辑,这样可保证在不同系统版本下卡片功能的稳定性。
Q:卡片的formId是不是固定的?
A:卡片的formId在创建以后是固定的。在HarmonyOS的服务卡片开发中,formId作为卡片的唯一标识符,用于标识不同的服务卡片。这样设计确保了卡片的身份在整个生命周期中的一致性和可识别性。
Q:服务卡片进程里面读取应用进程的首选项数据,只读取不写入会有文件损坏的风险吗?
A:服务卡片进程里面读取应用进程的首选项数据,只读取不写入不会损坏文件。
Q:能否根据卡片的formName更新卡片信息?
A:对于当前应用程序相同moduleName、abilityName、formName的卡片,多次加桌后会每张卡片会有不同的卡片id。卡片提供方可以通过formProvider.reloadForms接口批量更新不同的卡片id但moduleName、abilityName、formName相同的卡片。
Q:应用卡片内部组件能使用装饰器吗?如@State、@Provide、@Prop等传递数据。
A:卡片内通过@State等修饰的数据是可以改变的。