HarmonyOS7 桌面卡片怎么从 0 到 1?Widget 小组件开发全流程
文章目录前言FormExtensionAbility卡片的幕后大脑配置卡片的元数据卡片 UIArkTS 写法和普通页面不太一样处理卡片的交互事件数据更新定时 事件驱动双保险实战日程卡片几点踩坑经验前言桌面卡片Widget是 HarmonyOS 里特别讨喜的一个功能。用户不用打开 App 就能在桌面上看到关键信息——天气、日程、股票行情一眼就够。对开发者来说这也是一个很好的 App 曝光入口。但卡片的开发跟普通页面有不少区别特别是数据更新机制和交互方式。今天咱们从零搭一个天气卡片 日程卡片把整个流程跑通。FormExtensionAbility卡片的幕后大脑每个卡片背后都有一个FormExtensionAbility它就是卡片的服务端。系统通过这个 Ability 来管理卡片的生命周期——创建、更新、销毁。先看看它的核心回调import{formBindingData,FormExtensionAbility}fromkit.FormKit;exportclassWeatherFormAbilityextendsFormExtensionAbility{// 卡片被添加到桌面时触发onAddForm(want:Want){constformIdwant.parameters?.[formId]asstring;console.info(卡片创建:${formId});returnthis.buildFormData(formId);}// 卡片需要更新时触发定时/手动onUpdateForm(formId:string){console.info(卡片更新:${formId});returnthis.buildFormData(formId);}// 卡片被删除时触发onRemoveForm(formId:string){console.info(卡片删除:${formId});}// 卡片从不可见变为可见时触发onCastToNormalForm(formId:string){returnthis.buildFormData(formId);}privatebuildFormData(formId:string):formBindingData.FormBindingData{constdata{temperature:26°C,weather:晴,city:杭州,updateTime:newDate().toLocaleTimeString()};returnformBindingData.createFormBindingData(data);}}几个关键的生命周期节点要搞清楚onAddForm用户把卡片拖到桌面上时调用返回初始数据onUpdateForm定时触发或手动触发的数据更新onRemoveForm用户删除卡片时调用做清理工作onCastToNormalForm临时卡片转为常态卡片时调用比如卡片从后台回前台配置卡片的元数据卡片的大小、更新频率这些信息要在form_config.json里配置{forms:[{name:WeatherForm,displayName:天气卡片,description:实时天气信息,src:./ets/entryformability/EntryFormAbility.ets,uiSyntax:arkts,window:{designWidth:720,autoDesignWidth:true},colorMode:auto,isDynamic:true,isDefault:true,updateEnabled:true,scheduledUpdateTime:08:00,updateDuration:1,defaultDimension:2*2,supportDimensions:[2*2,2*4,4*4]}]}几个参数要注意isDynamic: true启用动态卡片ArkTS 卡片支持交互updateEnabled: true开启定时更新scheduledUpdateTime定时更新时间系统会在接近这个时间时触发更新supportDimensions卡片支持的尺寸多配几个让用户有选择卡片 UIArkTS 写法和普通页面不太一样卡片 UI 用的是 ArkTS但跟普通页面有一些限制——不能用弹窗、不能用动画、组件类型也有限制。不过常用的 Text、Image、Column、Row、Button 都有。来写天气卡片的 UI// WeatherFormWidget.etsEntryComponentstruct WeatherFormWidget{StorageProp(temperature)temperature:string--°C;StorageProp(weather)weather:string加载中;StorageProp(city)city:string;StorageProp(updateTime)updateTime:string;build(){Column(){Row(){Image($r(app.media.weather_sunny)).width(40).height(40)Column(){Text(this.city).fontSize(14).fontColor(#666666)Text(this.temperature).fontSize(36).fontWeight(FontWeight.Bold).fontColor(#333333)}.margin({left:12})}.width(100%)Row(){Text(this.weather).fontSize(14).fontColor(#888888)Blank()Text(更新于${this.updateTime}).fontSize(12).fontColor(#AAAAAA)}.width(100%).margin({top:12})// 按钮点击刷新天气Button(刷新天气).fontSize(12).height(28).width(80).margin({top:8}).onClick((){// 通过 postCardAction 发送事件给 FormExtensionAbilitypostCardAction(this,{action:message,params:{action:refresh}});})}.width(100%).height(100%).padding(16).backgroundColor(#FFFFFF).borderRadius(16)}}注意StorageProp装饰器——它从formBindingData传过来的数据里取值。postCardAction则是卡片 UI 跟 FormExtensionAbility 通信的桥梁。处理卡片的交互事件卡片里点击按钮后事件会传到 FormExtensionAbility 的onFormEvent回调exportclassWeatherFormAbilityextendsFormExtensionAbility{// ... 其他回调onFormEvent(formId:string,message:string){constparamsJSON.parse(message)asRecordstring,string;if(params[action]refresh){// 模拟获取最新天气数据constnewDatathis.fetchWeatherData();constformDataformBindingData.createFormBindingData(newData);this.context.updateForm(formId,formData);}}privatefetchWeatherData():Recordstring,string{// 实际项目中这里应该调网络接口return{temperature:${Math.floor(Math.random()*1520)}°C,weather:[晴,多云,阴,小雨][Math.floor(Math.random()*4)],city:杭州,updateTime:newDate().toLocaleTimeString()};}}卡片支持的交互方式有限主要是通过postCardAction发送message事件。不能直接调异步接口——onFormEvent里不能写async/await所以网络请求需要换个方式处理。数据更新定时 事件驱动双保险卡片的数据更新有两种方式定时更新由系统控制在form_config.json里配好scheduledUpdateTime就行系统到点会调onUpdateForm。但别指望精确到分钟——系统会综合考虑电量和性能可能延迟触发。事件驱动适合需要实时更新的场景比如日程提醒快到了// 在 EntryAbility 或其他地方主动触发卡片更新import{formProvider}fromkit.FormKit;asyncfunctionrefreshAllForms(context:Context){try{// 获取所有活跃的卡片constformInfosawaitformProvider.getFormsInfo(context);for(constformofformInfos){if(form.nameWeatherForm){constdataformBindingData.createFormBindingData({temperature:28°C,weather:多云转晴,city:杭州,updateTime:newDate().toLocaleTimeString()});awaitformProvider.updateForm(form.formId,data);}}}catch(err){console.error(刷新卡片失败:${JSON.stringify(err)});}}实战日程卡片再写一个日程卡片支持显示今日待办和点击标记完成// ScheduleFormWidget.etsinterfaceScheduleItem{id:string;title:string;time:string;done:boolean;}EntryComponentstruct ScheduleFormWidget{StorageProp(schedules)schedulesJson:string[];getschedules():ScheduleItem[]{try{returnJSON.parse(this.schedulesJson)asScheduleItem[];}catch(e){return[];}}build(){Column(){Row(){Text(今日日程).fontSize(16).fontWeight(FontWeight.Bold)Blank()Text(${this.schedules.filter(s!s.done).length}项待办).fontSize(12).fontColor(#FF6B00)}.width(100%)ForEach(this.schedules.slice(0,3),(item:ScheduleItem){Row(){Text(item.done?✓:○).fontSize(16).fontColor(item.done?#4CAF50:#999999).margin({right:8})Column(){Text(item.title).fontSize(14).fontColor(item.done?#BBBBBB:#333333).decoration({type:item.done?TextDecorationType.LineThrough:TextDecorationType.None})Text(item.time).fontSize(11).fontColor(#AAAAAA)}.alignItems(HorizontalAlign.Start).layoutWeight(1)}.width(100%).margin({top:8}).onClick((){postCardAction(this,{action:message,params:{action:toggle,scheduleId:item.id}});})})}.width(100%).height(100%).padding(16).backgroundColor(#FFFFFF).borderRadius(16)}}对应的 FormExtensionAbility 处理标记完成的事件exportclassScheduleFormAbilityextendsFormExtensionAbility{privateschedules:ScheduleItem[][{id:1,title:产品评审会,time:09:30,done:false},{id:2,title:提交周报,time:14:00,done:false},{id:3,title:健身,time:18:30,done:true},];onAddForm(want:Want){returnthis.buildData();}onFormEvent(formId:string,message:string){constparamsJSON.parse(message)asRecordstring,string;if(params[action]toggle){constidparams[scheduleId];consttargetthis.schedules.find(ss.idid);if(target){target.done!target.done;this.context.updateForm(formId,this.buildData());}}}privatebuildData():formBindingData.FormBindingData{returnformBindingData.createFormBindingData({schedules:JSON.stringify(this.schedules)});}}几点踩坑经验卡片开发最容易踩的坑是数据传递方式。formBindingData只支持 JSON 可序列化的数据复杂对象、函数什么的传不过去。我一开始想把整个数据对象传过去结果卡片上显示的全是 undefined排查了好久。另一个坑是卡片 UI 的组件限制。不是所有 ArkUI 组件都能在卡片里用像Dialog、Sheet这些弹窗类组件不行Canvas也有限制。写之前先看看官方文档的支持列表别写到一半发现组件不能用。最后一个建议卡片的数据尽量做本地缓存。onAddForm触发时如果网络不好用户看到的就是空白卡片体验很差。先展示缓存数据后台再异步更新。