HarmonyOS元服务与卡片
元服务与卡片
文章目录
- 一、元服务
- 1.介绍
- 2.常见元服务项目步骤
- 二、卡片
- 1.介绍
- 2.卡片的创建
- 3.卡片的数据的变更
- 4.卡片的进程间通讯
- 4.1使用工具包
- 4.2使用步骤
- 5.卡片路由postCardAction:快速拉起后台
- 5.1格式
- 5.2快速拉起指定页面--router
- 5.3调用后台功能--call
- 5.3卡片内的数据获取(适合轻量级,限制在5s内)--message
- 6.卡片定时/定点/手动设置下次更新
- 6.1定时/定点
- 6.2设置下次更新
- 7.卡片图片的加载(本地和网络)
- 7.1方式一:(下载到本地后,使用文件描述进行表达回显)
- 7.2方式二:(使用Base64)见[工具包有多种方式](https://blog.csdn.net/Crowd_chips/article/details/141790509?sharetype=blogdetail&sharerId=141790509&sharerefer=PC&sharesource=Crowd_chips&spm=1011.2480.3001.8118)
一、元服务
1.介绍
元服务就是手机中自带的小组件类似。
是一种有独立入口、免安装、可为用户提供一个或多个服务的新型应用程序形态。它基于 HarmonyOS API 开发,支持运行在 “1+8+N” 设备上,让用户在合适的场景、合适的设备上便捷使用。
特点:
1.免下载安装:用户无需像传统应用那样经历下载、安装等繁琐过程,节省了设备存储空间和下载时间,能够快速使用服务。例如,用户在使用一些简单功能的应用服务时,无需等待下载安装,直接通过元服务即可快速获取相关功能。
2.即开即用、即用即走:使用非常便捷,用户在需要时可以快速打开使用,使用完毕后不需要专门去关闭或卸载,减少了操作步骤和使用负担。比如用户在查询某个信息时,通过元服务快速查询到结果后即可关闭,下次使用时再次快速打开。
3.多端部署:只需一次开发,就可以部署在各种 HarmonyOS 终端上,包括手机、平板、智能手表、智慧屏等,大大降低了开发者的开发成本和工作量,同时也为用户提供了跨设备的一致使用体验。
4.使用方式与入口:
用户可以通过负一屏、智慧搜索、小艺助手等入口唤起元服务。例如,在负一屏中搜索相关的元服务名称,或者通过语音指令让小艺助手打开相应的元服务。
元服务还可以以服务卡片的形式存在于用户的设备桌面,用户可以根据自己的需求添加、管理和使用服务卡片,方便快捷地获取服务信息和进行操作。
对开发者的优势:
5.开发相对简单,开发者可以快速加入鸿蒙生态。并且元服务代码 100% 可复用到原生应用开发,提高了开发效率和代码的复用性。
通过鸿蒙系统的服务分发能力,元服务可以获得更多的流量和曝光机会,有助于开发者的服务更好地推广和被用户使用。
总之,元服务是 HarmonyOS 生态中的一个重要组成部分,为用户提供了更加便捷、高效的服务体验,也为开发者提供了新的开发和推广机会。
2.常见元服务项目步骤
二、卡片
1.介绍
元服务与卡片开发之间的关联主要体现在:卡片作为元服务的展现形式之一,可以为用户提供与元服务交互的界面。通过卡片,用户可以方便地访问和控制后台的元服务,而开发者则可以利用元服务为卡片提供丰富的功能和数据。这种设计允许鸿蒙操作系统在不同设备上提供一致且高效的用户体验,同时也简化了跨设备服务的开发和维护。总的来说,元服务和卡片开发是鸿蒙操作系统中支持分布式能力和提高用户体验的两个关键技术。通过元服务,鸿蒙能够在多种设备之间提供无缝的服务体验;而卡片开发则使得这些服务能够以一种简洁而直观的方式呈现给用户,从而促进了不同设备和场景下的信息流通和功能使用。
2.卡片的创建
元服务和普通项目都一致,项目的文件夹可以变更
3.卡片的数据的变更
使用的工具包,可以见首选项工具包:PreferencesUtil,主要用于存放卡片ID
思路:根据ID
1.在EntryFormAbility中获取卡片ID:
2.在特定位置触发,用于加载卡片显示的数据
3. 卡片接收方式: @LocalStorageProp(“title”) // 使用页面级别Ui接收
1.在EntryFormAbility中获取卡片ID:(初始化数据)private preference: PreferencesUtil = PreferencesUtil.getInstance()onAddForm(want: Want) { // 请求不能超出5秒,否则自动断开// 正常业务使用的情况 -- 进保存卡片ID,存在问题:有变更则所有的都变更 ---优化增加卡片name保存到首选项let formData = {"title": "加载中..."} as Record<string, string>;// 业务处理和返回无关// 获取卡片IDlet id = want.parameters![formInfo.FormParam.IDENTITY_KEY] // 获取卡片name // 可以根据name过滤非同一类的卡片,做到指定更新let name = want.parameters![formInfo.FormParam.NAME_KEY] console.log('system===>卡片ID:' + id+'卡片name:'+name)// 首选项封装的工具,将ID,name保存至首选项this.preference.addFormId(this.context, JSON.stringify({"id":id,"name":name})).then((res) => {console.log('system===>添加')}).catch(() => {console.error('system===>错误')})return formBindingData.createFormBindingData(formData);}
2.在特定位置触发,用于加载卡片显示的数据。(变更数据)以根据卡片名称,指定更新为例:// 封装方法getForm(formName: string) { // 传递的卡片的名称// 根据首选项,获取所有的卡片IDthis.preference.getFormIds(getContext(this)).then((item) => { // 返回JSON数组// 所要变更的成的数据let formData = {"title": "卡片延时加载" + formName} as Record<string, string>;// 获取索要的卡片IDconst arr = item.filter((formDataS) => {const obj: Record<string,string|number> = JSON.parse(formDataS) as Record<string,string|number>console.log('system===>'+JSON.parse(formDataS)+':'+obj.name)return obj.name == formName})if (arr.length > 0) {arr.forEach((son: string) => {const obj: Record<string,string|number> = JSON.parse(son) as Record<string,string|number>// 执行卡片ID,将数据变更formProvider.updateForm(obj.id as string, formBindingData.createFormBindingData(formData)) // 执行固定函数 formBindingData.createFormBindingData(formData); 这个可以单独拿出来单独定义})}}).catch((err: BusinessError) => {console.error('system===>更新' + JSON.stringify(err))})}
3. 卡片接收数据方式: @LocalStorageProp("title") // 使用页面级别Ui接收@LocalStorageProp("title") // 使用页面级别Ui接收@State TITLE: string = '';build() {Row() {Column() {Text(this.TITLE).fontSize($r('app.float.font_size')).fontWeight(FontWeight.Medium).fontColor($r('app.color.item_title_font'))}}}
4.卡片的进程间通讯
使用的原因:
原因是在App存在期间,卡片与App不是同一个进程,App中不能直接访问卡片保存在首选 项中的数据,即App读取不到添加到桌面的元卡片ID,所以导致更新ArkTS卡片数据失败。
当App关闭后,第二次打开时,就可以正常访问到卡片保存在首选项中的卡片ID数据了,所以再次点击更新卡片数据就能成功了。
卡片的加载数据与APP应用不是同一个进程,可能回导致数据刷新不到,此时需要进程间的通讯(发布和订阅)–进程间通信(IPC)的订阅-发布模式实现。
发布: 谁触发
订阅: 根据获取的ID指定更新
4.1使用工具包
import commonEventManager from '@ohos.commonEventManager'// Publisher通讯事件类型
enum PublishEventType {APP_PUBLISH = "APP_PUBLISH",CARD_PUBLISH = "CARD_PUBLISH"
}class IPCManagerClass {static publishCount:number = 1// 发布者static publish(eventType:PublishEventType,data:string){// commonEventManager作用:可用于进程间通讯commonEventManager.publish(eventType,{data},(err)=>{if(err){// 失败只发3次if(this.publishCount<=3){this.publish(eventType,data)}else{this.publishCount = 1}}else{this.publishCount = 1}})}// 订阅者static subscribe(eventType:PublishEventType,subscriber,callback:(event:string)=>void){commonEventManager.createSubscriber({ events: [eventType] }, (err, data) => {if (err) {return console.log('common-->', `创建订阅者error ${JSON.stringify(err)}`)}console.log('common-->', `创建订阅者success`)subscriber = dataif (subscriber !== null) {//订阅事件commonEventManager.subscribe(subscriber, (err, data) => {if (err) {return console.error(`logData`, '订阅事件失败')}console.log('common-->',`接受订阅事件:${data.data}`)callback(data.data)})} else {console.error('common-->',`需要创建subscriber`);}})}
}export {IPCManagerClass,PublishEventType}
4.2使用步骤
发布:谁触发的就是发布者(卡片被触发)
订阅:端侧订阅
发布:
`src/main/ets/entryformability/EntryFormAbility.ets` 的 `onAddForm` 事件中增加如下代码:
PreferencesUtil.getInstance().addFormId(this.context, formid).then(res => {console.log('hmlog-->', '保存卡片id成功');// 增加一个发布卡片ID(formid)的消息IPCManagerClass.publish(PublishEventType.APP_PUBLISH,formid)}).catch(err => {console.log('hmlog-->', '保存卡片id失败');
});
订阅:
`src/main/ets/pages/Index.ets` 中的 ` aboutToAppear` 生命周期事件的重构代码如下:async aboutToAppear(): Promise<void> {console.log('hmlog-->','aboutToAppear....')IPCManagerClass.subscribe(PublishEventType.CARD_PUBLISH,undefined, async (formId)=>{console.log('hmlog-->',' aboutToAppear,IPCManagerClass,formId = '+formId);// this.cardIds.push(formId);// 将接收到的formId保存到当前进程的首选项中await PreferencesUtil.getInstance().addFormId(getContext(),formId);this.getCardIds();})}getCardIds(){PreferencesUtil.getInstance().getFormIds(getContext()).then(formIds=>{console.log('hmlog-->','cardids:'+JSON.stringify(formIds));this.cardIds.length = 0;this.cardIds.push(...formIds);}).catch((err:BusinessError)=>{console.error('hmlog-->','getCardIds Failed. Err:'+JSON.stringify(err));})}
5.卡片路由postCardAction:快速拉起后台
5.1格式
postCardAction(this, {action: 'router', // 三种方式: router:拉起后台 /call:拉起后台功能 /message 卡片内的数据获取,受限大abilityName: 'EntryAbility', // app的启动UiAbility// 参数params: {// 传参JSON格式message: '跳转B成功'}});// 接收参数的方式:在APP的EntryAbility文件中: let params = JSON.parse(want.parameters!.params as string) as Record<string, string>;
const message = params.message; // 结果: 跳转B成功。 看封装的json格式获取数据
5.2快速拉起指定页面–router
思路:
在EntryAbility 定义变量用于接收路径path 和 windowStage
当用于被隐藏第二次不会再次调用onWindowStageCreate和onCreate 则需要使用 使用 onNewWant 并在调用时,重新调用onWindowStageCreate
注意问题:router,属于不同进程间的通讯,会存在调用不到问题,需要使用到进程间通讯(发布-订阅模式) 见本章4.2有解决方法
格式:(其中可以根据参数,快速拉起某个应用UI) 涉及的生命周期:
onCreate()、onNewWant()、onWindowStageCreate()// 重新调用快速拉起后台应用
Button('功能A').margin('20%').onClick(() => {console.info('hmlog-->','Jump to EntryAbility funA');postCardAction(this, {'action': 'router','abilityName': 'EntryAbility', // 只能跳转到当前应用下的UIAbility'params': {'targetPage': 'funA' // 在EntryAbility中处理这个信息}});})
5.3调用后台功能–call
使用时需要加权限:ohos.permission.KEEP_BACKGROUND_RUNNING
// 监听call事件所需的方法
this.callee.on(‘funA’, FunACall);
当不适用时需要销毁,比如生命周期中的onDestroy()
this.callee.off(‘funA’)
格式:(其中可以根据参数,快速拉起某个后台功能) 涉及的生命周期:
onCreate()、onDestroy()// 重新调用快速拉起后台应用
权限:"requestPermissions": [{"name": "ohos.permission.KEEP_BACKGROUND_RUNNING","usedScene": {"abilities": ["EntryAbility"],"when": "inuse" // inuse(使用时生效) always(始终生效,可用于在后台使用这个,也会执行)}}],格式:
Button('拉至后台').width('40%').height('20%').onClick(() => {postCardAction(this, {'action': 'call', // 必填'bundleName': 'com.example.myapplication', // 可选'abilityName': 'EntryAbility', // 必填'params': {'method': 'fun', // 自定义调用的方法名,必填'message': 'testForCall' // 自定义要发送的message}});})****并在UIAblity的onCreate事件中监听Call事件 和关闭监听:
1.开启监听
在UIAblity的onCreate事件中,开启监听:
this.callee.on('fun', FunACall); // FunACall函数 并要求有return返回值,返回null就可以
例:
function FunACall(data) {// 获取call事件中传递的所有参数console.log('FunACall param:' + JSON.stringify(data.readString())); // data 不需要传参,调用data.readString()获取参数return null;
}2.关闭监听
在UIAblity的onDestroy事件中,进程退出时,解除监听:
this.callee.off('fun')
5.3卡片内的数据获取(适合轻量级,限制在5s内)–message
卡片内应用(受限制较多) 使用到的生命周期:onFormEvent()
postCardAction(this, {'action': 'message','params': {'msgTest': 'messageEvent'}
});
6.卡片定时/定点/手动设置下次更新
如何设置卡片的定时任务:(操作步骤)
1.(开启定时人物)修改‘src/main/resources/base/profile/form_config.json’中的 updateEnabled = true配置
2. 定时:需要将updateDuration =1 添加值 1代表30分钟
3.定点:将updateDuration =0 ,并设置定点scheduledUpdateTime = 18:30 (24小时) ,若updateDuration 不设置为0,则会优先定时。
4.在src/main/ets/entryformability/EntryFormAbility.ets 中修改 onUpdateForm()
6.1定时/定点
6.2设置下次更新
在触发定时、定点或主动刷新后,系统会调用FormExtensionAbility的onUpdateForm()生命周期回调。
使用函数:setFormNextRefreshTime(‘卡片ID’,定时时间(min),()=>{回调})
1.在初始数据时,设置下次更新时间
2.当到达设置时间,则会在onUpdateForm()周期中进行业务逻辑处理,同时也可以在onUpdateForm()中设置下次更新,实现定时更新的功能。
***最低设置时间是5分钟例:
初始数据:
onAddForm(want) { // 生命周期调用let formData = {'title': 'Title Update Success.'// 和卡片布局中对应};let myFormInfo = formBindingData.createFormBindingData(formData)let formid = want.parameters[formInfo.FormParam.IDENTITY_KEY];formProvider.updateForm(formid, myFormInfo).then((data) => {// 业务处理....// 设置更新try {// 设置过5分钟后更新卡片内容formProvider.setFormNextRefreshTime(formid, 5, (err, data) => {if (err) {return;} else {}});} catch (err) {}}).catch((error) => {})return formBindingData.createFormBindingData(formData);}触发更新调用:
onUpdateForm(formId) { // 生命周期更新函数let formData = {'title': 'Title Update Success.'+Math.random() // 和卡片布局中对应};let myFormInfo = formBindingData.createFormBindingData(formData)formProvider.updateForm(formId, myFormInfo).then((data) => {// 业务处理// 设置过5分钟后更新卡片内容(如果有需要时,可以继续定时刷新)formProvider.setFormNextRefreshTime(formId, 5, (err, data) => {if (err) {return;} else {}})})}
7.卡片图片的加载(本地和网络)
当请求网络资源则需要申请权限:“ohos.permission.INTERNET”
涉及的生命周期:onFormEvent()
7.1方式一:(下载到本地后,使用文件描述进行表达回显)
例:网络资源和本地不能直接显示。需要有特定的前缀import fs from '@ohos.file.fs';onFormEvent(formId: string, message: string) {// 初始化操作formProvider.updateForm(formId,formBindingData.createFormBindingData({"text":"更新中"+formId}))// 拉去文件let image ="https://mmbiz.qpic.cn/sz_mmbiz_jpg/ZjuK90OCRv1X4MVY9lKHV5NVEbiaibvCNHaiayvbDZJPHiaaYsPSfYHcKj74S45klAFHllicCwM2UVGNCFg8vUWb80A/640?wx_fmt=jpeg&from=appmsg"let tmpDir = this.context.getApplicationContext().tempDir+'fileImage_'+new Date().getTime()+'.png'// 沙箱路径:this.context.tempDir// 保存至本地(下载网络图片)request.downloadFile(this.context,{url:image,filePath:tmpDir}).then((task)=>{task.on("complete",()=>{// 获取下载文件路径let file:fileIo.File;file = fs.openSync(tmpDir)// 回显到卡片formProvider.updateForm(formId,formBindingData.createFormBindingData({"text":'更新成功',"imgName":'imgHttps','formImages':{ // 固定格式//file.fd: 文件描述符 // key:就是 imgName的值('imgHttps')imgHttps:file.fd },'loaded':true // 加载状态,自己定义参数}))})task.on("fail",()=>{// 更新数据formProvider.updateForm(formId,formBindingData.createFormBindingData({"text":"更新失败"+formId}))})}).catch((err:BusinessError)=>{// 更新数据formProvider.updateForm(formId,formBindingData.createFormBindingData({"text":"更新失败"+formId}))})}
卡片回显设置:@LocalStorageProp('imgName') imgName: string = 'name';Image('memory://' + this.imgName).width('50%').height('50%').margin('5%').alt($r('app.media.icon'))
7.2方式二:(使用Base64)见工具包有多种方式
思路:
1.使用到首选项,用于保存卡片ID,工具包:首选项工具包–卡片开发使用
2.可能使用到的工具(进程间通讯(IPC))
3.调用下载需要将文件下载到本地并转为Base64格式
例:let base64ImageStr:string = await this.toBase64(imgFile); // 封装的工具that.base64Img = base64ImageStr;
// 工具包:
// 其中当转为这个后,可能会与原文件有差异,需要修改配置base64ImgStr
async toBase64(image_uri: string) {if (image_uri) {let selectImage = fs.openSync(image_uri, fs.OpenMode.READ_WRITE)const imageSource = image.createImageSource(selectImage.fd);const imagePackerApi: image.ImagePacker = image.createImagePacker();let packOpts: image.PackingOption = { format: 'image/jpeg', quality: 100 };let readBuffer = await imagePackerApi.packing(imageSource, packOpts);let bufferArr = new Uint8Array(readBuffer)let help = new util.Base64Helperlet base = help.encodeToStringSync(bufferArr)let base64ImgStr = 'data:image/png;base64,' + base;return base64ImgStr;}return '';
}