Flutter for OpenHarmony 实现 HarmonyOS NEXT 服务卡片开发教程
Flutter for OpenHarmony 实现 HarmonyOS NEXT 服务卡片开发教程
概述
服务卡片(Form Widget)是 HarmonyOS 的特色功能,允许用户在桌面上直接查看应用信息并进行快捷操作。由于 Flutter 本身不直接支持服务卡片,我们需要通过 Platform Channel 与原生 ArkTS 代码进行通信,在原生层实现服务卡片功能。
技术架构
Flutter 应用层 (Dart)
↓ MethodChannel 通信
原生插件层 (ArkTS) - FormWidgetPlugin
↓ 调用服务
数据服务层 (ArkTS) - FormDataService
↓ 更新卡片
卡片扩展层 (ArkTS) - FormExtensionAbility
↓ 渲染
卡片 UI 层 (ArkTS) - WidgetCard.ets
核心思路:Flutter 通过 MethodChannel 将数据传递给原生层,原生层负责管理和更新服务卡片。
项目结构
your_flutter_project/
├── lib/
│ └── services/
│ └── form_widget_service.dart # Flutter 侧通信服务
└── ohos/entry/src/main/
├── ets/
│ ├── entryability/
│ │ ├── EntryAbility.ets # 应用入口,注册插件
│ │ └── FormWidgetPlugin.ets # 原生通信插件
│ ├── formability/
│ │ └── MyFormAbility.ets # 卡片生命周期管理
│ ├── widget/pages/
│ │ └── WidgetCard.ets # 卡片 UI 界面
│ └── services/
│ └── FormDataService.ets # 卡片数据服务
└── resources/base/profile/
└── form_config.json # 卡片配置文件
第一步:配置服务卡片
1.1 创建卡片配置文件
在 ohos/entry/src/main/resources/base/profile/ 目录下创建 form_config.json:
{
"forms": [{
"name": "MyWidget",
"description": "应用服务卡片",
"src": "./ets/widget/pages/WidgetCard.ets",
"uiSyntax": "arkts",
"isDefault": true,
"updateEnabled": true,
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": ["2*2", "2*4", "4*4"]
}]
}
配置说明:
name- 卡片唯一标识名称src- 卡片 UI 页面的文件路径updateEnabled- 开启定时更新功能updateDuration- 更新周期,单位为 30 分钟(值为 1 表示每 30 分钟更新一次)supportDimensions- 支持的卡片尺寸,22 为小卡片,44 为大卡片
1.2 注册卡片扩展能力
在 ohos/entry/src/main/module.json5 的 extensionAbilities 数组中添加:
{
"name": "MyFormAbility",
"srcEntry": "./ets/formability/MyFormAbility.ets",
"type": "form",
"exported": true,
"metadata": [{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}]
}
配置说明:
type: "form"- 声明这是一个服务卡片扩展metadata- 关联上一步创建的卡片配置文件exported: true- 允许系统访问此扩展能力
第二步:实现卡片生命周期管理
在 ohos/entry/src/main/ets/formability/ 目录下创建 MyFormAbility.ets:
import { FormExtensionAbility, formInfo, formBindingData } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { FormDataService } from '../services/FormDataService';
export default class MyFormAbility extends FormExtensionAbility {
// 用户添加卡片到桌面时触发
onAddForm(want: Want): formBindingData.FormBindingData {
const formId = want.parameters?.[formInfo.FormParam.IDENTITY_KEY] as string;
FormDataService.getInstance().registerForm(formId);
return formBindingData.createFormBindingData({ title: '加载中...', count: 0 });
}
// 系统定时更新卡片时触发
onUpdateForm(formId: string): void {
FormDataService.getInstance().updateForm(formId);
}
// 用户删除卡片时触发
onRemoveForm(formId: string): void {
FormDataService.getInstance().unregisterForm(formId);
}
// 用户点击卡片上的交互元素时触发
onFormEvent(formId: string, message: string): void {
if (message === 'refresh') {
FormDataService.getInstance().updateForm(formId);
}
}
}
代码说明:
onAddForm- 卡片创建回调,需要返回初始数据,同时将卡片 ID 注册到数据服务中以便后续更新onUpdateForm- 定时更新回调,根据配置的updateDuration周期性触发onRemoveForm- 卡片删除回调,清理已注册的卡片 ID,释放资源onFormEvent- 事件回调,处理卡片上按钮点击等交互事件
第三步:实现卡片数据服务
在 ohos/entry/src/main/ets/services/ 目录下创建 FormDataService.ets:
import { formProvider, formBindingData } from '@kit.FormKit';
import { preferences } from '@kit.ArkData';
export class FormDataService {
private static instance: FormDataService;
private formIds: Set<string> = new Set(); // 存储所有已注册的卡片 ID
private dataPreferences: preferences.Preferences | null = null; // 本地数据存储
static getInstance(): FormDataService {
if (!FormDataService.instance) {
FormDataService.instance = new FormDataService();
}
return FormDataService.instance;
}
// 初始化本地存储(在 EntryAbility 中调用)
async initPreferences(context: Context): Promise<void> {
this.dataPreferences = await preferences.getPreferences(context, 'formDataStore');
}
// 注册卡片 ID
registerForm(formId: string): void {
this.formIds.add(formId);
this.updateForm(formId); // 注册后立即更新数据
}
// 注销卡片 ID
unregisterForm(formId: string): void {
this.formIds.delete(formId);
}
// 更新指定卡片
async updateForm(formId: string): Promise<void> {
const formData = await this.getFormData();
const bindingData = formBindingData.createFormBindingData(formData);
await formProvider.updateForm(formId, bindingData);
}
// 更新所有已注册的卡片(供 Flutter 调用)
async updateAllForms(): Promise<void> {
for (const formId of this.formIds) {
await this.updateForm(formId);
}
}
// 保存数据并刷新卡片(供 Flutter 调用)
async saveData(key: string, value: string): Promise<void> {
await this.dataPreferences?.put(key, value);
await this.dataPreferences?.flush();
await this.updateAllForms();
}
// 获取卡片显示数据
private async getFormData(): Promise<Record<string, Object>> {
const title = await this.dataPreferences?.get('widget_title', '我的应用') as string;
const count = parseInt(await this.dataPreferences?.get('widget_count', '0') as string, 10);
return { title, count, hasData: count > 0 };
}
}
代码说明:
formIds- 使用 Set 存储所有活跃卡片的 ID,支持多个卡片实例dataPreferences- 使用 HarmonyOS 首选项 API 持久化存储数据updateForm- 核心方法,通过formProvider.updateForm将数据推送到指定卡片saveData- 供 Flutter 调用,保存数据后自动刷新所有卡片- 采用单例模式确保全局只有一个数据服务实例
第四步:创建卡片 UI 界面
在 ohos/entry/src/main/ets/widget/pages/ 目录下创建 WidgetCard.ets:
@Entry
@Component
struct WidgetCard {
// 使用 @LocalStorageProp 绑定卡片数据,变量名必须与 FormDataService 返回的数据键名一致
@LocalStorageProp('title') title: string = '我的应用';
@LocalStorageProp('count') count: number = 0;
@LocalStorageProp('hasData') hasData: boolean = false;
build() {
Column({ space: 12 }) {
// 标题区域
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
// 数据展示区域
if (this.hasData) {
Text(this.count.toString())
.fontSize(32)
.fontColor(Color.White)
} else {
Text('暂无数据')
.fontSize(14)
.fontColor('rgba(255,255,255,0.8)')
}
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#667eea')
.borderRadius(16)
.onClick(() => {
// 点击卡片跳转到应用
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { page: 'home' }
});
})
}
}
代码说明:
@LocalStorageProp- 卡片专用的数据绑定装饰器,自动接收formBindingData传递的数据- 变量名(如
title、count)必须与FormDataService.getFormData()返回对象的键名完全一致 postCardAction- 卡片专用的交互 API,支持router(跳转应用)和message(发送事件)两种 action- 卡片 UI 使用 ArkTS 声明式语法,与 Flutter 的 Widget 概念类似
第五步:实现 Flutter 与原生通信
5.1 创建原生通信插件
在 ohos/entry/src/main/ets/entryability/ 目录下创建 FormWidgetPlugin.ets:
import { FlutterPlugin, MethodCall, MethodChannel, MethodResult } from '@ohos/flutter_ohos';
import { FlutterPluginBinding } from '@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/FlutterPlugin';
import { FormDataService } from '../services/FormDataService';
export default class FormWidgetPlugin implements FlutterPlugin {
private channel?: MethodChannel;
// Flutter 引擎加载完成后调用
onAttachedToEngine(binding: FlutterPluginBinding): void {
// 创建方法通道,名称必须与 Flutter 侧一致
this.channel = new MethodChannel(binding.getBinaryMessenger(), 'com.example.app/form_widget');
this.channel.setMethodCallHandler({
onMethodCall: (call: MethodCall, result: MethodResult) => {
switch (call.method) {
case 'updateWidgetData':
this.handleUpdateWidgetData(call, result);
break;
case 'refreshAllWidgets':
FormDataService.getInstance().updateAllForms().then(() => result.success(true));
break;
default:
result.notImplemented();
}
}
});
}
// 处理 Flutter 传来的更新数据请求
private async handleUpdateWidgetData(call: MethodCall, result: MethodResult): Promise<void> {
const service = FormDataService.getInstance();
const title = call.argument('title') as string;
const count = call.argument('count') as number;
if (title) await service.saveData('widget_title', title);
if (count !== undefined) await service.saveData('widget_count', count.toString());
result.success(true);
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
this.channel?.setMethodCallHandler(null);
}
getUniqueClassName(): string { return 'FormWidgetPlugin'; }
}
代码说明:
MethodChannel- Flutter 与原生通信的核心类,通过唯一名称建立双向通道onMethodCall- 接收 Flutter 发来的方法调用,根据call.method分发处理call.argument()- 获取 Flutter 传递的参数result.success()- 异步返回结果给 Flutter- 通道名称
com.example.app/form_widget必须与 Flutter 侧完全一致
5.2 注册插件到应用入口
修改 ohos/entry/src/main/ets/entryability/EntryAbility.ets:
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
import FormWidgetPlugin from './FormWidgetPlugin';
import { FormDataService } from '../services/FormDataService';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends FlutterAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
super.onWindowStageCreate(windowStage);
// 初始化卡片数据服务的本地存储
FormDataService.getInstance().initPreferences(this.context);
}
configureFlutterEngine(flutterEngine: FlutterEngine): void {
super.configureFlutterEngine(flutterEngine);
GeneratedPluginRegistrant.registerWith(flutterEngine);
// 注册自定义的卡片通信插件
this.addPlugin(new FormWidgetPlugin());
}
}
代码说明:
onWindowStageCreate- 窗口创建时初始化数据服务,确保首选项存储可用configureFlutterEngine- Flutter 引擎配置回调,在此注册所有原生插件this.addPlugin()- 将自定义插件添加到 Flutter 引擎中
5.3 Flutter 侧实现通信服务
在 lib/services/ 目录下创建 form_widget_service.dart:
import 'package:flutter/services.dart';
class FormWidgetService {
// 通道名称必须与原生侧完全一致
static const MethodChannel _channel = MethodChannel('com.example.app/form_widget');
static final FormWidgetService instance = FormWidgetService._();
FormWidgetService._();
/// 更新卡片数据
Future<bool> updateWidgetData({String? title, int? count}) async {
try {
final result = await _channel.invokeMethod<bool>('updateWidgetData', {
if (title != null) 'title': title,
if (count != null) 'count': count,
});
return result ?? false;
} on PlatformException catch (e) {
print('更新卡片失败: ${e.message}');
return false;
}
}
/// 刷新所有卡片
Future<bool> refreshAllWidgets() async {
try {
return await _channel.invokeMethod<bool>('refreshAllWidgets') ?? false;
} on PlatformException {
return false;
}
}
}
代码说明:
MethodChannel- Flutter 侧的通道类,与原生侧建立通信invokeMethod- 调用原生方法,第一个参数是方法名,第二个参数是传递的数据- 使用单例模式确保全局只有一个服务实例
- 异常处理确保原生调用失败时不会导致应用崩溃
第六步:在 Flutter 应用中使用
在业务代码中调用服务更新卡片:
import 'services/form_widget_service.dart';
class MyHomePage extends StatefulWidget {
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _widgetService = FormWidgetService.instance;
int _count = 0;
// 添加数据后同步更新卡片
Future<void> _addItem() async {
setState(() => _count++);
// 调用服务更新桌面卡片
await _widgetService.updateWidgetData(
title: '我的待办',
count: _count,
);
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('数量: $_count')),
floatingActionButton: FloatingActionButton(
onPressed: _addItem,
child: Icon(Icons.add),
),
);
}
}
代码说明:
- 在数据变化时调用
updateWidgetData同步更新桌面卡片 - 卡片更新是异步操作,不会阻塞 UI
- 建议在关键业务操作(如添加、删除、修改数据)后调用更新
常见问题排查
问题 1:卡片无法添加到桌面
检查步骤:
- 确认
module.json5中已正确注册FormExtensionAbility - 确认
form_config.json文件路径和内容正确 - 确认
metadata中的resource指向正确的配置文件
问题 2:卡片数据不更新
检查步骤:
- 确认
@LocalStorageProp的变量名与数据服务返回的键名完全一致 - 确认
formProvider.updateForm被正确调用 - 检查
formId是否正确传递和存储
问题 3:Platform Channel 通信失败
检查步骤:
- 确认 Flutter 侧和原生侧的通道名称完全一致(区分大小写)
- 确认插件已在
EntryAbility中注册 - 确认方法名在两端一致
问题 4:卡片点击无响应
检查步骤:
- 确认
postCardAction中的abilityName是已注册的 Ability 名称 - 确认
action类型正确(router或message)
最佳实践
1. 数据更新防抖
避免频繁更新卡片,建议使用防抖机制:
Timer? _debounceTimer;
void scheduleWidgetUpdate() {
_debounceTimer?.cancel();
_debounceTimer = Timer(Duration(milliseconds: 500), () {
FormWidgetService.instance.refreshAllWidgets();
});
}
2. 错误处理
原生侧处理卡片已被删除的情况:
try {
await formProvider.updateForm(formId, bindingData);
} catch (err) {
if ((err as BusinessError).code === 16501001) {
this.unregisterForm(formId); // 卡片已删除,清理 ID
}
}
3. 性能优化建议
- 卡片 UI 中避免复杂计算,数据处理放在
FormDataService中 - 合理设置
updateDuration,避免过于频繁的定时更新 - 使用缓存减少重复的数据库查询
参考资料
- HarmonyOS Form Kit 官方文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/form-overview
- Flutter for OpenHarmony 官方仓库:https://gitee.com/openharmony-sig/flutter_samples
- ArkTS 开发指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-get-started
更多推荐

所有评论(0)