最近在给自用的消息推送 App 做 HarmonyOS NEXT 服务卡片(Service Widget),把踩过的坑和最终架构整理出来。核心场景:在桌面上放一张卡片,实时展示最新消息,点击跳转 App 详情。
整体架构

1 2 3 4 5 6 7 8 9
| ┌────────────────────────────────────────────┐ │ 卡片提供方进程(FormExtensionAbility) │ │ ├── 读取共享数据库,渲染卡片 UI │ │ └── 响应卡片点击 / 消息事件 │ ├────────────────────────────────────────────┤ │ 应用主进程 │ │ ├── 消息入库(MessageStore.db) │ │ └── 推送模式:直接推送 / 代理推送 │ └────────────────────────────────────────────┘
|
数据流的关键决定:卡片进程和应用进程不直接通信,双方都读写同一个共享数据库。这样卡片刷新逻辑与应用业务彻底解耦,任何一边挂了另一边都不受影响。
在 module.json5 中注册 formExtAbility:
1 2 3 4 5 6 7 8 9 10 11 12
| { "abilities": [ { "name": "EntryFormAbility", "srcEntry": "./ets/entryformability/EntryFormAbility.ts", "type": "extension", "metadata": [ { "name": "ohos.extension.form", "resource": "$profile:form_config" } ] } ] }
|
核心回调就四个,理解它们的时间点很重要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export default class EntryFormAbility extends FormExtensionAbility { onCreate(want: Want, params: FormBindingData): FormBindingData { return this.bindCardData(); }
onUpdate(formId: string, params: FormBindingData): void { const data = this.bindCardData(); this.updateForm(formId, data); }
onVisibilityChange(newStatus: { [key: string]: number }): void { const formIds = Object.keys(newStatus); formIds.forEach((id) => this.refreshCard(id)); }
onDestroy(formId: string): void { clearInterval(this.timer); } }
|
三个容易忽略的点:
onUpdate 是高频回调,里面不要做重活,查库要快,数据量大时在应用侧预聚合一张”卡片专用表”;onVisibilityChange 是刷新数据的最佳时机,用户回到桌面卡片可见的那一刻刷一次,比定时轮询省电得多;- 卡片尺寸变化(小/中/大卡)走
onSizeChange,不同尺寸绑定的数据字段可以不同,别把大卡的数据结构硬塞给小卡。
卡片 UI 用 ArkTS 写,通过 FormLink 与卡片能力绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Entry @Component struct MessageCard { @LocalStorageProp('messages:latest') latest: string = '暂无消息'; @LocalStorageProp('messages:count') count: number = 0;
build() { Column() { Text(this.latest) .fontSize(14) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(`${this.count} 条未读`) .fontSize(11) .fontColor('#999') } .padding(12) .onClick(() => { postCardAction(this, { action: 'openDetail' }); }) } }
|
FormLink 在卡片页面里声明与卡片能力的关联,配合 @LocalStorageProp 实现数据驱动 UI。注意卡片里的交互能力很弱,不要试图在卡片里做复杂操作,它的职责就是”展示 + 跳转”。
跨进程共享数据库
这是整个方案的技术核心。HarmonyOS 的关系型数据库 RdbStore 支持配置为跨进程共享:
1 2 3 4 5 6 7 8 9 10 11 12
| const storeConfig: relationalStore.StoreConfig = { name: 'MessageStore.db', securityLevel: relationalStore.SecurityLevel.S1, encrypt: false, };
const rdbStore = await relationalStore.getRdbStore(context, storeConfig); const resultSet = await rdbStore.query( relationalStore.ResultSet.SUB_QUERY_FROM_BEGINNING, this.predicates.equalTo('unread', 1) );
|
要点:
- 库名、表结构、
StoreConfig 两边必须完全一致,改 schema 时两边同步发版,否则旧卡片读新表直接抛异常; - 卡片侧只读、应用侧可写,是更稳的分工。写操作集中在应用进程,卡片崩了不会写坏数据;
- 查询走
RdbPredicates,记得给常用查询字段建索引,卡片刷新是毫秒级要求; - 数据量控制:卡片只需要最新几条,
limit 一定要加,别把整表拉进内存。
两种推送模式
设置页提供模式切换,背后是两套完全不同的数据来源:
| 直接推送(无服务端) | 代理推送(自建服务器) |
|---|
| 数据来源 | 本机轮询/系统事件 | 自建后端长连接 |
| 依赖 | 无 | 服务器 + 账号 |
| 延迟 | 受系统策略限制 | 服务器实时下发 |
| 适用 | 单机使用 | 多设备同步 |
实现上模式是一个持久化开关,UI 上控制服务器配置组的显隐——只有代理模式下才显示服务器地址、密钥等输入项,避免用户配了一堆用不上的东西。
1 2 3 4 5 6
| if (this.pushMode === PushMode.PROXY) { this.serverGroupVisibility = Visibility.Visible; } else { this.serverGroupVisibility = Visibility.None; }
|
原生动画偏好
HarmonyOS 的 ArkUI 过渡动画足够好用,没必要引第三方动画库:
- 页面切换用系统
pageTransition,跟手且统一; - 列表项显隐用
transition + animation,几行声明式代码搞定; - 卡片内数字变化可以加一个
animation({ duration: 200 }) 的透明度过渡,细腻不抢戏。
真机调试备忘
1 2 3 4 5 6 7 8
| hdc install entry-default-signed.hap
hdc shell hilog | grep FormExtensionAbility
hdc shell aa start -b <bundleName> -a <abilityName>
|
小结
服务卡片的开发体验比想象中成熟:FormExtensionAbility 负责生命周期和数据绑定,声明式 UI 负责渲染,跨进程 RdbStore 负责数据共享。真正的设计功夫在刷新时机(可见性驱动而非定时轮询)和数据边界(卡片只读、预聚合),这两点做好了,卡片的流畅度和省电效果都有保障。