鸿蒙 ArkTS 卡片与 Flutter 应用的数据同步:Form Kit 卡片点击如何跳转到 Flutter 详情页
适合谁看
-
正在做鸿蒙卡片开发,但不清楚卡片与 Flutter 应用如何联动的开发者
-
想在 Flutter 项目中接入 Form Kit 但不知道数据流怎么走的人
-
遇到"卡片点击后 Flutter 页面没有跳转到正确位置"问题的开发者
问题背景
鸿蒙桌面卡片运行在独立进程中,它不能直接访问 Flutter 引擎。当用户点击卡片时,系统通过 Want 参数启动 Ability,Ability 再通过 MethodChannel 将参数推送给 Flutter。
这个链路涉及三个独立的数据存储位置:
-
卡片 ArkTS 侧:
RecommendData.ets维护推荐数据 -
Ability 生命周期:
EntryAbility接收并缓存 Want 参数 -
Flutter 路由层:
IntentNavigationChannel执行最终跳转
数据在这三者之间如何流转,是本文要解决的核心问题。
项目中的真实场景
食界探味的桌面卡片体系包含 4 张卡片:
|
卡片 |
文件 |
类型 |
点击行为 |
|---|---|---|---|
|
每日推荐 |
|
动态卡片 |
跳转到菜品详情 |
|
搜索 |
|
静态卡片 |
跳转到搜索页 |
|
AI 助手 |
|
静态卡片 |
跳转到 AI 助手页 |
|
愿望盒 |
|
静态卡片 |
跳转到愿望盒页 |
其中"每日推荐"是唯一的动态卡片,它需要在 onAddForm 和 onUpdateForm 时从 RecommendData.ets 获取当天推荐菜品数据,绑定到卡片 UI 上。
核心实现
卡片数据绑定:RecommendData → DailyRecommendCard
DailyRecommendFormAbility.ets 的 onAddForm 方法负责首次创建卡片时的数据绑定:
// DailyRecommendFormAbility.ets
onAddForm(want: Want): formBindingData.FormBindingData {
const formId = want.parameters?.['ohos.extra.param.key.form_identity'] as string;
const formName = want.parameters?.['ohos.extra.param.key.form_name'] as string;
// 静态卡片直接返回空数据
if (STATIC_CARD_NAMES.has(formName)) {
return formBindingData.createFormBindingData({});
}
// 动态卡片:从 RecommendData 获取今日推荐
const item = getRecommendOfToday();
return formBindingData.createFormBindingData({
dishName: item.name,
dishRegion: item.region,
dishImage: resolveImageResName(item.imageResName),
dishId: item.id,
dishHighlight: item.highlight,
dishSummary: item.summary,
});
}
数据绑定的关键点:
-
formBindingData.createFormBindingData创建的数据会通过LocalStorage注入到卡片 UI 组件 -
卡片 UI 通过
@LocalStorageProp装饰器读取这些数据 -
dishImage经过resolveImageResName校验,确保资源名有效
卡片 UI 数据读取:LocalStorageProp
// DailyRecommendCard.ets
Entry(dailyRecommendStorage)
@Component
struct DailyRecommendCard {
@LocalStorageProp('dishName') dishName: string = '环球美食';
@LocalStorageProp('dishRegion') dishRegion: string = '世界';
@LocalStorageProp('dishImage') dishImage: string = FALLBACK_IMAGE_RES_NAME;
@LocalStorageProp('dishId') dishId: string = '';
@LocalStorageProp('dishHighlight') dishHighlight: string = '今天吃什么';
@LocalStorageProp('dishSummary') dishSummary: string = '打开食界探味,挑一道想去认识的新菜。';
@LocalStorageProp 是 ArkUI 的状态管理机制,它从 LocalStorage 中读取数据。当 DailyRecommendFormAbility.onAddForm 返回的 FormBindingData 被系统注入到 LocalStorage 后,卡片组件自动获得最新数据。
卡片点击跳转:postCardAction → Want → EntryAbility
当用户点击卡片时,ArkTS 侧通过 postCardAction 发起跳转:
// DailyRecommendCard.ets
private openDishDetail(): void {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
pageId: this.dishId ? 'dish_detail' : 'explore',
dishId: this.dishId,
}
});
}
postCardAction 的参数说明:
-
action: 'router':表示这是一次路由跳转 -
abilityName: 'EntryAbility':指定目标 Ability -
params:传递给 Ability 的 Want 参数
系统收到 postCardAction 后,会根据 abilityName 启动或唤醒 EntryAbility,并将 params 作为 Want.parameters 传递。
EntryAbility 接收参数
如果应用未启动,走 onCreate:
// EntryAbility.ets
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
super.onCreate(want, launchParam)
const pageId = want.parameters?.['pageId'] as string;
const dishId = want.parameters?.['dishId'] as string;
if (pageId) {
IntentNavigationPlugin.setPendingNavigation(pageId, dishId);
}
}
如果应用已在前台,走 onNewWant:
// EntryAbility.ets
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
super.onNewWant(want, launchParam)
const pageId = want.parameters?.['pageId'] as string;
const dishId = want.parameters?.['dishId'] as string;
if (pageId) {
const plugin = IntentNavigationPlugin.getInstance();
if (plugin) {
plugin.navigateToPage(pageId, dishId);
} else {
IntentNavigationPlugin.setPendingNavigation(pageId, dishId);
}
}
}
Flutter 侧路由执行
IntentNavigationChannel 收到 onIntentNavigation 事件后,根据 pageId 执行跳转:
// intent_navigation_channel.dart
static void _navigate(_NavigationPayload payload) {
if (payload.pageId == 'dish_detail') {
final dishId = payload.dishId;
if (dishId == null || dishId.isEmpty) {
AppLogger.warning('Missing dishId for dish_detail intent');
return;
}
const homeRoute = '/explore';
final detailRoute = '/dish/$dishId';
// 先回到首页,再 push 详情页,确保路由栈正确
_router?.go(homeRoute);
scheduleMicrotask(() {
_router?.push(detailRoute);
});
return;
}
final route = _pageIdToRoute[payload.pageId];
if (route == null) {
AppLogger.warning('Unknown intent pageId: ${payload.pageId}');
return;
}
if (_shellRoutes.contains(route)) {
_router?.go(route);
} else {
_router?.go('/explore');
scheduleMicrotask(() {
_router?.push(route);
});
}
}
关键设计:dish_detail 页面需要先 go('/explore') 再 push,因为详情页是 push 页面,不能直接 go 到一个 push 路由。用 scheduleMicrotask 确保两次路由操作不在同一帧执行。
关键代码位置
-
app/ohos/entry/src/main/ets/formability/DailyRecommendFormAbility.ets— 卡片数据绑定 -
app/ohos/entry/src/main/ets/formability/RecommendData.ets— 推荐数据源 -
app/ohos/entry/src/main/ets/widget/pages/DailyRecommendCard.ets— 卡片 UI 与点击跳转 -
app/ohos/entry/src/main/ets/entryability/EntryAbility.ets— Want 参数接收 -
app/ohos/entry/src/main/ets/plugins/IntentNavigationPlugin.ets— 参数缓存与 MethodChannel 推送 -
app/lib/core/platform/intent_navigation_channel.dart— Flutter 侧路由执行
鸿蒙侧实现
鸿蒙侧涉及三个组件的协作:
-
FormExtensionAbility(
DailyRecommendFormAbility.ets):在onAddForm/onUpdateForm中获取数据并返回FormBindingData -
数据层(
RecommendData.ets):维护推荐列表,提供getRecommendOfToday()方法 -
卡片 UI(
DailyRecommendCard.ets):通过@LocalStorageProp读取数据,通过postCardAction发起跳转
数据流方向:RecommendData → FormAbility → LocalStorage → Card UI → postCardAction → EntryAbility → MethodChannel → Flutter
Flutter 侧实现
Flutter 侧的职责是:
-
在
IntentNavigationChannel.init中注册 MethodChannel 监听 -
收到
onIntentNavigation事件后解析pageId+dishId -
根据映射表找到目标路由,通过 GoRouter 执行跳转
-
处理 shell 路由(底部 Tab)和 push 路由(详情页)的不同跳转策略
常见坑
-
坑 1:卡片数据和应用内数据不一致。
RecommendData.ets是 ArkTS 侧的静态数据,如果 Flutter 侧也维护了一份推荐列表,两边可能不同步。建议卡片数据来源和应用内数据来源统一。 -
坑 2:
dishId为空时跳转失败。如果当天推荐数据的id字段为空,postCardAction传过去的dishId就是空字符串,Flutter 侧会因为缺少dishId而放弃跳转。DailyRecommendCard.ets中用this.dishId ? 'dish_detail' : 'explore'做了兜底。 -
坑 3:
resolveImageResName校验失败返回 fallback。如果RecommendData.ets中的imageResName拼写错误或资源不存在,resolveImageResName会返回 fallback 图片,但不会报错,导致难以排查。 -
坑 4:应用冷启动时
onCreate和configureFlutterEngine的时序。onCreate在configureFlutterEngine之前执行,此时 MethodChannel 不可用,必须走 pending 缓存。
可复用模板
// Flutter 侧 - 卡片跳转接收模板
class CardNavigationReceiver {
static const _channel = MethodChannel('com.example.card_navigation');
static GoRouter? _router;
static void init(GoRouter router) {
_router = router;
_channel.setMethodCallHandler((call) async {
if (call.method == 'onCardClick') {
_handleCardClick(call.arguments);
}
});
_consumePending();
}
static Future<void> _consumePending() async {
try {
final result = await _channel.invokeMethod<Object?>('consumePending');
if (result is Map) {
_handleCardClick(result);
}
} on MissingPluginException {}
}
static void _handleCardClick(Object? args) {
if (args is! Map) return;
final pageId = args['pageId'] as String?;
final itemId = args['itemId'] as String?;
if (pageId == null) return;
final route = _resolveRoute(pageId, itemId);
if (route != null) {
_router?.go('/home');
scheduleMicrotask(() => _router?.push(route));
}
}
static String? _resolveRoute(String pageId, String? itemId) {
switch (pageId) {
case 'item_detail':
return itemId != null ? '/item/$itemId' : null;
case 'search':
return '/search';
default:
return null;
}
}
}
// 鸿蒙侧 - 卡片点击跳转模板
@Component
struct MyCard {
@LocalStorageProp('itemId') itemId: string = '';
@LocalStorageProp('itemName') itemName: string = '';
private handleClick(): void {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
pageId: this.itemId ? 'item_detail' : 'home',
itemId: this.itemId,
}
});
}
build() {
Column() {
Text(this.itemName)
}
.onClick(() => this.handleClick())
}
}
本篇总结
ArkTS 卡片与 Flutter 应用的数据同步,核心链路是:FormAbility 获取数据 → LocalStorage 绑定到卡片 UI → postCardAction 携带参数 → EntryAbility 接收 Want → MethodChannel 推送到 Flutter → GoRouter 执行跳转。理解这条链路的关键在于:卡片是 ArkTS 进程中的独立 UI,它不能直接调用 Flutter,必须通过 Ability 生命周期和 MethodChannel 做桥接。
更多推荐


所有评论(0)