服务卡片可以理解成桌面小组件,在桌面上不用打开app也可以看到关键app的数据
本文第一部分参考官方文档讲解了卡片的基本使用流程,第二部分讲解了实际使用中跨进程数据通信问题的解决方案
一、基本使用(动态卡片)
1.1创建卡片
通过ide创建一个dynamci widget
其中有2个文件需要重点关注, 一个是卡片的ui widigetCard,一个是 EntryFormAbility继承自FormExtensionAbility。
需要注意的是这俩是运行在单独的进程上,和主app不在同一个进程
1.2卡片页面
卡片页面基本可以当做一个普通的组件来写,需要注意的是接收formAbility的数据要按卡片的格式来
let localStorage = new LocalStorage()
@Entry(localStorage)
@Component
struct WidgetCard {
@LocalStorageProp('curDrink') curDrink: string = ''
@LocalStorageProp('totalDrink') totalDrink: string = ''
build() {
Row() {
Column() {
Text("今日饮水目标")
Text(this.curDrink + "/" + this.totalDrink)
}
.width('100%')
}.onClick(() => {
//因为要记录一次formId
postCardAction(this, {
action: 'message'
})
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility', // 只能跳转到当前应用下的UIAbility
});
})
.height('100%')
}
}
如上代码卡片需要的数据,要通过localStorage来接收,这里简单举了curDrink和totalDrink两个字段来示范
在提供数据时,也按照对应的k-v record组装数据
let record: Record<string, number> = {
"curNum": drinkManager.curDrink,
"totalNum": drinkManager.totalDrink
}
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(record);
formProvider.updateForm(formId, formInfo).then(() => {
logger.logD("updateForm success")
}).catch((error: BusinessError) => {
logger.logE("updateForm failed e=" + JSON.stringify(error))
})
1.3卡片事件
动态卡片事件的主要使用场景如下:
- router事件:可以使用router事件跳转到指定UIAbility,并通过router事件刷新卡片内容。
- call事件:可以使用call事件拉起指定UIAbility到后台,并通过call事件刷新卡片内容。
- message事件:可以使用message拉起FormExtensionAbility,并通过FormExtensionAbility刷新卡片内容。
简单点说 router事件用来拉起uiAbility,也就是主app的页面。有点类似从点击通知栏消息跳到app某个页面这样,app的uiAbility需要承接want事件
call事件用来后台拉起uiAbility然后执行对应主app对应的方法
message事件通过FormExtensionAbility来传递消息,比如想要存下当前卡片对应的formId时,通过message触发 formAbility的onFormEvent(formId: string, message: string),这时能够持久化对应的formId
1.4卡片数据交互
刷新策略如定时刷新、定点刷新这个可以参考官方文档配置。
这里需要注意的是卡片数据的获取,下面会展开详细介绍。
二、卡片数据问题
场景分析:
如果场景是想主app控制数据,在桌面上时通过卡片来展示app的数据时,当从formAbility获取app的数据(比如从一个dataManager里面取相关数据)来渲染卡片ui,会发现获取到的数据全都不符合预期,是初始化状态的。
原因:
不管是在运行时通过管理数据的manager,持久化的Preference文件以及数据库拿数据会发现formAbility拿到的数据都是‘异常’的。原因就是前面提到的uiAbility所在的主app进程和卡片formAbility所在的卡片进程是两个独立的进程。这点从debug attach的进程或者log日志里面能够很明显的看出来是两个进程。
不同进程肯定数据是独立的,从卡片进程直接拿数据比如通过manager拿到的肯定是初始状态的数据,因为在这个卡片进程这个manager没有赋值过。
如何解决:
先说结论:如果是联网app,可以通过云服务器来转数据,如主进程往服务器存数据,卡片进程从服务器取数据;如果是离线app或者需要离线时也能刷新卡片数据,通过umdf来实现数据共享
尝试的方式
2.1跨进程通信 IPC (不可行)
安卓的同事应该比较熟悉,跨进程通信使用aidl的方式。主进程当做server,其他进程当做client,通过binder来实现数据交互。
鸿蒙也有IPC的概念developer.huawei.com/consumer/cn…
但是文档里面写的当前使用场景: 仅限客户端是三方应用,服务端是系统应用。也就是没法实现上诉主app进程当做服务端。然后不信邪按文档操作了一番,报了个错误码,对应的就是只有系统应用才能做服务端。
还有个思路主app进程通过picker等方式往公共目录文件写,然后卡片进程再读公共目录文件。但是想到这个操作需要用户感知会影响用户体验就没有继续往下研究。
这条路就pass掉了。
2.2通过服务器中转(可行)
主app往接口上传数据,卡片进程通过网络请求下拉刷新,通过服务器中转也能完成数据共享。 缺点就是要依赖网络。
2.3umdf 通过标准化数据通路实现数据共享 (可行)
developer.huawei.com/consumer/cn…
标准化数据通路是为各种业务场景提供的跨应用的数据接入与读取通路,它可以暂存应用需要共享的符合标准化数据定义的统一数据对象,并提供给其他应用进行访问,同时按照一定的策略对暂存数据的访问权限和生命周期进行管理。
思路是主app通过数据库或者preference文件等持久化方式保存数据,运行时主app把数据更新到数据通路中,卡片进程在合适的时机从数据通路中读取,刷新到卡片上。
下面给一个例子主app使用时显示喝水的记录,退到到桌面时更新卡片的喝水记录。
/**
* 更新喝水的目标总量和当前量
* @param records
*/
static async updateDrinkRecord(records: Record<string, string>) {
if (!UDMFUtil.DATA_HUB_Key) {
UDMFUtil.insertData(records)
return
}
let textUpdate = new unifiedDataChannel.Text();
textUpdate.details = records
let unifiedDataUpdate = new unifiedDataChannel.UnifiedData(textUpdate);
// 指定要更新的统一数据对象的URI
let optionsUpdate: unifiedDataChannel.Options = {
// 此处的key值仅为示例,不可直接使用,其值与insertData接口回调函数中key保持一致
key: UDMFUtil.DATA_HUB_Key
};
try {
await unifiedDataChannel.updateData(optionsUpdate, unifiedDataUpdate);
} catch (e) {
let error: BusinessError = e as BusinessError;
logger.logE(`Update data throws an exception. code is ${error.code},message is ${error.message} `);
}
}
/**
* 查询当前的喝水目标量和进度
* @returns
*/
static async getDrinkRecord(): Promise<Record<string, string> | undefined> {
// 指定要查询数据的数据通路枚举类型
let dataRecord: Record<string, string> | undefined
let options: unifiedDataChannel.Options = {
// key:DrinkManager.DATA_HUB_Key,
intention: unifiedDataChannel.Intention.DATA_HUB
};
try {
let data: Array<UnifiedData> = await unifiedDataChannel.queryData(options)
for (let i = 0; i < data.length; i++) {
let records = data[i].getRecords();
for (let j = 0; j < records.length; j++) {
if (records[j].getType() === uniformTypeDescriptor.UniformDataType.TEXT) {
let text = records[j] as unifiedDataChannel.Text;
dataRecord = text.details
// logger.logD("queryData result=" + JSON.stringify(dataRecord))
break
}
}
}
} catch (e) {
let error: BusinessError = e as BusinessError;
logger.logE(`Query data throws an exception. code is ${error.code},message is ${error.message} `);
}
return dataRecord
}
/**
* 在app退到后台时,读取数据通路中的数据,通过formId更新对应卡片的数据
*/
onBackground(): void {
// Ability has back to background
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
let records: Record<string, string> = {
"curDrink": drinkManager.curDrink + "",
"totalDrink": drinkManager.totalDrink + ""
}
UDMFUtil.updateDrinkRecord(records).then(() => {
UDMFUtil.getFormId().then((formId: string | undefined) => {
UDMFUtil.getDrinkRecord().then((value: Record<string, string> | undefined) => {
if (value) {
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(value);
formProvider.updateForm(formId, formInfo).then(() => {
logger.logD("updateForm success")
}).catch((error: BusinessError) => {
logger.logE("updateForm failed e=" + JSON.stringify(error))
})
}
})
})
})
}
如上诉代码,在app使用时通过updateDrinkRecord更新数据通路,然后在ability backGround的时通过getDrinkRecord数据通路数据,使用formProvider.updateForm(formId, formInfo)更新卡片数据