Flutter for OpenHarmony 实现 HarmonyOS NEXT 服务卡片开发教程
flutter for harmony 开发服务卡片全流程
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)