适合谁看

  • 正在做鸿蒙卡片开发,但不清楚卡片与 Flutter 应用如何联动的开发者

  • 想在 Flutter 项目中接入 Form Kit 但不知道数据流怎么走的人

  • 遇到"卡片点击后 Flutter 页面没有跳转到正确位置"问题的开发者

问题背景

鸿蒙桌面卡片运行在独立进程中,它不能直接访问 Flutter 引擎。当用户点击卡片时,系统通过 Want 参数启动 Ability,Ability 再通过 MethodChannel 将参数推送给 Flutter。

这个链路涉及三个独立的数据存储位置:

  1. 卡片 ArkTS 侧RecommendData.ets 维护推荐数据

  2. Ability 生命周期EntryAbility 接收并缓存 Want 参数

  3. Flutter 路由层IntentNavigationChannel 执行最终跳转

数据在这三者之间如何流转,是本文要解决的核心问题。

项目中的真实场景

食界探味的桌面卡片体系包含 4 张卡片:

卡片

文件

类型

点击行为

每日推荐

DailyRecommendCard.ets

动态卡片

跳转到菜品详情

搜索

SearchCard.ets

静态卡片

跳转到搜索页

AI 助手

AiAssistantCard.ets

静态卡片

跳转到 AI 助手页

愿望盒

WishBoxCard.ets

静态卡片

跳转到愿望盒页

其中"每日推荐"是唯一的动态卡片,它需要在 onAddFormonUpdateForm 时从 RecommendData.ets 获取当天推荐菜品数据,绑定到卡片 UI 上。

核心实现

卡片数据绑定:RecommendData → DailyRecommendCard

DailyRecommendFormAbility.etsonAddForm 方法负责首次创建卡片时的数据绑定:

// 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 侧路由执行

鸿蒙侧实现

鸿蒙侧涉及三个组件的协作:

  1. FormExtensionAbilityDailyRecommendFormAbility.ets):在 onAddForm/onUpdateForm 中获取数据并返回 FormBindingData

  2. 数据层RecommendData.ets):维护推荐列表,提供 getRecommendOfToday() 方法

  3. 卡片 UIDailyRecommendCard.ets):通过 @LocalStorageProp 读取数据,通过 postCardAction 发起跳转

数据流方向:RecommendData → FormAbility → LocalStorage → Card UI → postCardAction → EntryAbility → MethodChannel → Flutter

Flutter 侧实现

Flutter 侧的职责是:

  1. IntentNavigationChannel.init 中注册 MethodChannel 监听

  2. 收到 onIntentNavigation 事件后解析 pageId + dishId

  3. 根据映射表找到目标路由,通过 GoRouter 执行跳转

  4. 处理 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:应用冷启动时 onCreateconfigureFlutterEngine 的时序onCreateconfigureFlutterEngine 之前执行,此时 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 做桥接。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐