HarmonyOS 服务卡片开发实战:CheckMe 多卡片方案是怎么设计和实现的
HarmonyOS 服务卡片开发实战:CheckMe 多卡片方案是怎么设计和实现的
摘要
服务卡片是 CheckMe 项目里最有鸿蒙特色的一部分。本文不讲空泛概念,而是直接结合项目代码,拆解如何在 HarmonyOS 中实现多种类型、多种尺寸、可动态更新的桌面服务卡片,以及为什么我会把卡片当成项目主线之一来做。
四个主题适配说明
这篇文章的主方向是 创新体验 和 能力增强。创新体验来自服务卡片把 CPU、电池、网络等高频状态直接放到桌面,用户不用每次打开 App;能力增强来自 FormKit、FormExtensionAbility、formProvider.updateForm 等卡片能力的完整落地。它也能兼顾 高端精致,因为多尺寸卡片不是简单缩放,而是重新组织信息层级;安全隐私 则体现在卡片展示的是本地采集状态数据,没有把设备运行状态上传到远端处理。
一、为什么设备监控类应用特别适合做服务卡片
先说结论:设备监控类应用和服务卡片天然契合。
原因很简单,因为用户高频关注的内容通常只有几类:
- 当前 CPU 负载高不高
- 电池电量和温度怎么样
- WiFi 信号是否稳定
- 内存是不是快满了
- 网络上传下载速率有没有异常
这些信息最大的特点是:
- 需要经常看
- 单次查看很短
- 不值得每次都点进 App
这时候,服务卡片就不只是“好看”而已,它真正解决的是交互成本问题。
所以 CheckMe 的卡片设计目标非常明确:
- 用最小操作成本提供高频设备状态。
- 根据桌面空间提供不同信息密度。
- 让卡片成为 App 外的一层轻量能力,而不是宣传页。
二、项目里一共定义了哪些卡片
在 CheckMe 中,我一共做了 7 类卡片,主要包括:
- CPU 核心监控卡片
- CPU 频率可视化卡片
- WiFi 信号监控卡片
- 内存使用监控卡片
- 存储空间监控卡片
- 电池状态监控卡片
- 网络流量监控卡片
以 CPU 卡片为例:
{
"name": "CpuWidgetForm",
"description": "CPU 核心使用率监控小组件",
"uiSyntax": "arkts",
"isDefault": true,
"isDynamic": true,
"updateEnabled": true,
"defaultDimension": "2*2",
"supportDimensions": ["2*2", "2*4", "4*4"]
}
这里最重要的不是字段本身,而是设计意图:
isDynamic表示它是动态卡片updateEnabled表示它允许更新supportDimensions决定它能否做多尺寸布局
如果没有这一步,后面所有卡片更新和差异化展示都无从谈起。
三、为什么我没有只做一张总卡片
很多人做卡片时,会倾向于把所有信息塞进一张通用卡片。但在这个项目里,我刻意没有这么做,而是拆成多张卡片。原因有三个。
1. 用户关注点不同
有人最关心 CPU,有人最关心电量,也有人只想在桌面看网络速率。拆成多张卡片,更符合用户实际使用习惯。
2. 不同数据适合不同展示方式
比如 CPU 使用率适合趋势图,网络流量适合双向速率图,电池更适合状态卡。强行塞到一张卡片里,反而会显得拥挤。
3. 更适合做多尺寸适配
同一种卡片在 2*2、2*4、4*4 下可以做不同层级展示,而不是所有卡片都争抢一个空间。
四、卡片能力在工程里怎么接进来
做服务卡片不能只写一个页面,还要在模块声明里注册对应扩展能力。
示例注册方式如下:
{
"name": "WidgetFormAbility",
"type": "form",
"exported": true,
"metadata": [{ "name": "ohos.extension.form" }]
}
这一步的作用是把 form_config.json 里的卡片配置真正接到运行时环境里。
很多初学者做卡片容易忽略这一层,最后会出现“页面写好了,但系统不认它是卡片”的问题。
五、真正管理卡片生命周期的是 WidgetFormAbility
卡片的中控层是 WidgetFormAbility。它继承自 FormExtensionAbility,负责处理几个关键问题:
- 卡片添加时如何初始化
- 卡片更新时如何重新拉数据
- 卡片移除时如何清理
- 卡片的尺寸和类型如何保存
最关键的方法是 onAddForm():
onAddForm(want: Want): formBindingData.FormBindingData {
const formIdStr: string = (want.parameters?.['ohos.extra.param.key.form_identity'] as number).toString();
const formDimension: number = (want.parameters?.['ohos.extra.param.key.form_dimension'] as number) ?? 2;
const dimString: string = this.getDimensionString(formDimension);
const widgetKind: string = this.extractWidgetKind(want);
...
}
这段逻辑的意义在于,它能把系统传进来的原始信息转换成应用内部真正能用的元信息:
- 这是哪一张卡片
- 它是 CPU 卡片还是 WiFi 卡片
- 它当前是
2*2还是4*4
这一步一旦抽出来,后面不管推送还是恢复,都会轻松很多。
六、为什么还要做一个 WidgetFormIdRegistry
项目里还有一个非常重要但不太显眼的模块:
它会把桌面上现有的卡片信息持久化存到 Preferences 中。
这个设计看似只是存个表,实际上解决了非常多问题:
1. 多卡片共存
如果用户同时添加了 CPU 卡片、内存卡片和电池卡片,应用必须知道这些卡片都存在。
2. 多尺寸共存
同样是 CPU 卡片,用户可能同时放一个 2*2 和一个 4*4。如果不存尺寸信息,就无法正确推送。
3. 应用重启后的恢复能力
如果应用进程重新被拉起,依然要知道之前桌面上有哪些卡片,这样才有机会继续刷新。
所以在我看来,WidgetFormIdRegistry 不是配角,而是卡片工程化管理的基础设施。
七、卡片数据为什么要单独建模
项目里我没有直接把页面数据扔给卡片,而是单独写了:
比如 CPU 卡片结构:
export interface CpuWidgetData extends WidgetBaseData {
type: AppWidgetType.CPU;
usagePercent: number;
usageHistory: number[];
coreCount: number;
avgFreq: number;
maxFreq: number;
}
这么做的好处是:
- 卡片字段更稳定
- 页面结构变化不会影响卡片
- 卡片渲染层不用再理解底层复杂对象
这一点非常适合写进教学文章,因为它体现的是“工程结构意识”。
八、卡片数据如何统一推送
项目里专门封装了:
它做的事情很纯粹:
- 根据
widgetKind确定卡片类型 - 从
WidgetDataService中拿对应数据 - 序列化成 JSON
- 调用
formProvider.updateForm()推送
核心逻辑如下:
static pushForm(formId: string, widgetKind: string, dimString: string): void {
const widgetTypeEnum: AppWidgetType = WidgetFormPushHelper.kindToType(widgetKind);
const widgetData: WidgetData | null = WidgetFormPushHelper.getWidgetData(widgetTypeEnum);
const jsonStr: string = widgetData !== null ? JSON.stringify(widgetData) : '{}';
...
formProvider.updateForm(formId, bindingData)
}
统一封装之后,无论是前台刷新、手动刷新还是后台 WorkScheduler 刷新,最终都走同一条更新链路。
九、卡片页面本身怎么做得更有价值
以 CpuWidgetPage.ets 为例,这张卡片不只是显示一个 CPU 百分比,而是做了几件更有价值的事:
1. 通过 LocalStorageProp 接收推送数据
@LocalStorageProp('widgetData') @Watch('onWidgetDataChange') widgetData: string = DEFAULT_CPU_DATA;
2. 在页面显示时主动触发刷新
postCardAction(this, {
action: 'call',
abilityName: 'EntryAbility',
params: {
method: 'refreshWidgets',
formId: this.formId
}
});
3. 根据尺寸切换布局
if (this.widgetType === '4*4') {
this.buildLargeLayout()
} else if (this.widgetType === '2*4') {
this.buildMediumLayout()
} else {
this.buildSmallLayout()
}
4. 使用 Canvas 绘制趋势图
这一点让卡片从“状态贴纸”升级成了“轻量监控窗口”。
十、这套服务卡片方案最大的技术价值是什么
如果一定要提炼成一句话,我会说:
它不是只实现了“卡片能显示”,而是把卡片做成了一套多类型、多尺寸、可管理、可更新、可恢复的完整能力。
这正是很多 Demo 和实际项目之间的区别。
十一、实战踩坑总结
1. 只做一张总卡片会导致信息拥挤
解决:拆分多卡片,让每张卡片承担清晰职责。
2. 不保存卡片元信息,后期很难维护
解决:做 WidgetFormIdRegistry 持久化。
3. 只依赖页面渲染,不做统一推送,会导致刷新入口混乱
解决:封装 WidgetFormPushHelper。
4. 多尺寸卡片如果只是缩放,会显得非常廉价
解决:按尺寸做差异化布局,而不是机械放大缩小。
十二、结语
在 CheckMe 这个项目里,服务卡片不是附加分,而是主线设计之一。它把 HarmonyOS 的桌面能力和工具类应用的高频使用场景结合了起来,也让项目从“普通设备信息 App”变成了“真正有鸿蒙特色的项目”。
如果你在做项目,我非常建议认真打磨卡片部分。因为这部分最容易体现系统能力,也最容易做出差异化。
更多推荐



所有评论(0)