HarmonyOS 服务卡片开发实战:CheckMe 多卡片方案是怎么设计和实现的

摘要

服务卡片是 CheckMe 项目里最有鸿蒙特色的一部分。本文不讲空泛概念,而是直接结合项目代码,拆解如何在 HarmonyOS 中实现多种类型、多种尺寸、可动态更新的桌面服务卡片,以及为什么我会把卡片当成项目主线之一来做。

四个主题适配说明

这篇文章的主方向是 创新体验能力增强。创新体验来自服务卡片把 CPU、电池、网络等高频状态直接放到桌面,用户不用每次打开 App;能力增强来自 FormKit、FormExtensionAbility、formProvider.updateForm 等卡片能力的完整落地。它也能兼顾 高端精致,因为多尺寸卡片不是简单缩放,而是重新组织信息层级;安全隐私 则体现在卡片展示的是本地采集状态数据,没有把设备运行状态上传到远端处理。

一、为什么设备监控类应用特别适合做服务卡片

先说结论:设备监控类应用和服务卡片天然契合。

原因很简单,因为用户高频关注的内容通常只有几类:

  • 当前 CPU 负载高不高
  • 电池电量和温度怎么样
  • WiFi 信号是否稳定
  • 内存是不是快满了
  • 网络上传下载速率有没有异常

这些信息最大的特点是:

  • 需要经常看
  • 单次查看很短
  • 不值得每次都点进 App

这时候,服务卡片就不只是“好看”而已,它真正解决的是交互成本问题。

所以 CheckMe 的卡片设计目标非常明确:

  1. 用最小操作成本提供高频设备状态。
  2. 根据桌面空间提供不同信息密度。
  3. 让卡片成为 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*22*44*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;
}

这么做的好处是:

  • 卡片字段更稳定
  • 页面结构变化不会影响卡片
  • 卡片渲染层不用再理解底层复杂对象

这一点非常适合写进教学文章,因为它体现的是“工程结构意识”。

八、卡片数据如何统一推送

项目里专门封装了:

它做的事情很纯粹:

  1. 根据 widgetKind 确定卡片类型
  2. WidgetDataService 中拿对应数据
  3. 序列化成 JSON
  4. 调用 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”变成了“真正有鸿蒙特色的项目”。

如果你在做项目,我非常建议认真打磨卡片部分。因为这部分最容易体现系统能力,也最容易做出差异化。
在这里插入图片描述

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐