HarmonyOS NEXT 服务卡片与跨进程数据库共享实践

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

整体架构

HarmonyOS 服务卡片跨进程架构:卡片进程与应用进程共享 MessageStore.db,FormLink 负责跳转链路

1
2
3
4
5
6
7
8
9
┌────────────────────────────────────────────┐
│ 卡片提供方进程(FormExtensionAbility) │
│ ├── 读取共享数据库,渲染卡片 UI │
│ └── 响应卡片点击 / 消息事件 │
├────────────────────────────────────────────┤
│ 应用主进程 │
│ ├── 消息入库(MessageStore.db) │
│ └── 推送模式:直接推送 / 代理推送 │
└────────────────────────────────────────────┘

数据流的关键决定:卡片进程和应用进程不直接通信,双方都读写同一个共享数据库。这样卡片刷新逻辑与应用业务彻底解耦,任何一边挂了另一边都不受影响。

FormExtensionAbility 生命周期

在 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();
}

// 卡片请求刷新:定时到点或调用 updateForm 时触发
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);
}
}

三个容易忽略的点:

  1. onUpdate 是高频回调,里面不要做重活,查库要快,数据量大时在应用侧预聚合一张”卡片专用表”;
  2. onVisibilityChange 是刷新数据的最佳时机,用户回到桌面卡片可见的那一刻刷一次,比定时轮询省电得多;
  3. 卡片尺寸变化(小/中/大卡)走 onSizeChange,不同尺寸绑定的数据字段可以不同,别把大卡的数据结构硬塞给小卡。

声明式卡片(FormLink)

卡片 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

# 强制刷新所有卡片(开发时验证 onUpdate)
hdc shell aa start -b <bundleName> -a <abilityName>

小结

服务卡片的开发体验比想象中成熟:FormExtensionAbility 负责生命周期和数据绑定,声明式 UI 负责渲染,跨进程 RdbStore 负责数据共享。真正的设计功夫在刷新时机(可见性驱动而非定时轮询)和数据边界(卡片只读、预聚合),这两点做好了,卡片的流畅度和省电效果都有保障。