适合谁看

  • 想理解鸿蒙 Intent 导航 Flutter 侧特殊性的开发者

  • 正在做系统入口到页面路由衔接的人

  • 想把外部入口和应用路由分开的开发者

问题背景

很多人第一次做原生通信时,默认模型都是:

  • 页面点一个按钮

  • Flutter 调原生

  • 原生回一个结果

这个模型对语音识别、TTS 这类“页面主动发起”的能力很适用。
但鸿蒙 Intent 导航不是这样。

它最大的特殊性在于:

  • 导航意图可能先从系统外部到达

  • Flutter 页面甚至还没 ready

  • 页面层不是发起者,而更像承接者

这也是为什么 IntentNavigationChannel 的 Flutter 侧封装思路,和语音识别、TTS 这类 channel 完全不一样。

项目中的真实场景

当前这条 HarmonyOS 系统入口链路很典型:

  • app/ohos/entry/src/main/ets/entryability/InsightIntentExecutorImpl.ets

  • app/ohos/entry/src/main/ets/plugins/IntentNavigationPlugin.ets

  • app/lib/core/platform/intent_navigation_channel.dart

其中 Flutter 侧的 intent_navigation_channel.dart 做的不是“普通平台调用边界”,而是:

  • 接住系统整理好的导航 payload

  • 解析 pageId

  • 映射到 GoRouter 路由

  • 在 Flutter 侧继续完成页面跳转

所以这篇的核心不是“channel 怎么发请求”,而是“Flutter 怎么把鸿蒙系统入口翻译成应用内导航”。

核心实现

先说结论:

IntentNavigationChannel 最重要的职责不是向原生发命令,而是把鸿蒙系统入口结果收成 Flutter 应用能稳定消费的导航语义。

一、它和语音、TTS 的根本区别在哪里

speech_recognition_channel.darttext_to_speech_channel.dart,很容易发现它们更像:

  • 我主动发起一件事

  • 原生帮我完成

IntentNavigationChannel 这边更像:

  • 鸿蒙系统先把一条意图送进来

  • 原生先把它整理好

  • Flutter 再接住并转成本应用路由

也就是说,它的起点不是页面按钮,而是系统入口。
这一点决定了它的 Flutter 封装思路必须不同。

二、为什么这层必须负责 pageId 到路由的映射

intent_navigation_channel.dart 里,最关键的一段结构是:

  • _pageIdToRoute

这里把鸿蒙系统侧的意图标识,例如:

  • search

  • ai_assistant

  • wish_box

  • ingredients

  • explore

映射成应用内部真正的 Flutter 路由。

这一步为什么一定要留在 Flutter 侧做,而不是原生侧直接决定最终页面?

因为从职责上看:

  • 原生侧更接近“系统怎么把用户送进来”

  • Flutter 侧更接近“应用内部到底怎么走路由”

如果把这层路由决定也塞进原生侧,后面一旦 Flutter 路由结构变化,ArkTS 插件也要跟着一起变。
这会让鸿蒙入口层和页面层耦合得很紧。

三、为什么 init(router) 是这个 channel 的核心入口

这类 channel 和普通调用型 channel 最大的不同之一,是它不是靠页面某一次主动调用才开始工作。
在当前项目里,IntentNavigationChannel 的主入口是:

  • init(GoRouter router)

这说明它真正依赖的是:

  • Flutter 路由系统已经可用

  • 之后才能把鸿蒙系统入口翻译成页面导航

这和语音识别、TTS 那种“静态方法直接调一次”差别非常大。
它更像是在 Flutter 应用里挂一层“鸿蒙外部入口适配器”。

四、为什么要消费 pending navigation

这是这条链路最关键、也最容易被忽略的设计点。

在 Flutter 侧初始化时,IntentNavigationChannel 会主动调用:

  • _consumePending()

它背后的原因非常现实:

  • 鸿蒙系统入口可能先到了

  • 但 Flutter 路由和页面还没 ready

如果没有这一步,最容易发生的情况就是:

  • 系统把意图送进来了

  • 结果 Flutter 这边还没准备好承接

  • 这次导航就直接丢了

所以 pending navigation 不是“多此一举的缓存”,而是:

  • 鸿蒙系统入口时序和 Flutter 应用初始化时序之间的桥

五、为什么它必须注册 setMethodCallHandler

这也是它和普通调用型 channel 的又一个关键区别。

init(router) 里,Flutter 侧注册了:

  • _channel.setMethodCallHandler(...)

它要接的不是“某次调用的返回值”,而是:

  • onIntentNavigation

也就是说,HarmonyOS 原生层会主动把导航事件推给 Flutter。

所以这类 channel 的运行方式更接近:

  • Flutter 先把接收器挂好

  • 原生一旦有入口事件,就把它推回来

这也是为什么 IntentNavigationChannel 比普通调用型 channel 更像“鸿蒙入口事件适配层”。

六、为什么解析 payload 也必须留在边界层

在当前实现里,_parseArguments(Object? arguments) 负责:

  • 校验参数是不是 Map

  • 拿出 pageId

  • 拿出 dishId

  • 收成 _NavigationPayload

这看起来像简单的数据处理,但位置其实非常关键。

因为页面层不该直接去理解:

  • 鸿蒙原生传来的参数结构

  • dishId 有没有带

  • pageId 空不空

这些都更应该由边界层收口。
只有这样,页面层才只需要面对“我要跳到哪一个 Flutter 路由”这件事。

七、为什么这里还要负责一些应用内导航策略

_navigate(_NavigationPayload payload) 会发现,这里除了路由映射,还做了几件很应用内的事情:

  • ai_assistantAppConfig.enableAi 关闭时要兜底

  • dish_detail 需要先回到 /explorepush('/dish/$dishId')

  • shell route 和普通 route 的跳法不一样

这说明 Flutter 边界层在这里不只是“收消息”,而是在做一层很重要的转换:

  • 鸿蒙系统入口语义 →

  • 应用内导航策略

而这一层正是原生侧不适合决定、页面层又不应该散着处理的地方。

八、如果把这条链路从鸿蒙系统入口走到 Flutter 页面,顺序是怎样的

把当前代码对起来看,完整链路大致是这样:

HarmonyOS 系统入口
-> InsightIntentExecutorImpl.ets 校验 pageId / dishId
-> IntentNavigationPlugin.ets 存 pending 或主动推送事件
-> IntentNavigationChannel.init(router)
-> Flutter setMethodCallHandler 接住 onIntentNavigation
-> _parseArguments 收成 _NavigationPayload
-> _navigate 映射成 GoRouter 路由
-> Flutter 页面完成跳转

只要这条链路先建立清楚,后面你再去改 pageId 映射、路由策略或系统入口逻辑,都会更知道自己在改哪一层。

九、这种 Flutter 封装最适合什么样的鸿蒙能力

当前这种封装特别适合下面这类 HarmonyOS 系统入口能力:

  • 小艺搜索直达

  • 系统推荐入口

  • 桌面卡片触达后的页面跳转

  • 外部意图把用户送进应用某个业务页

它们的共同点是:

  • 入口先从系统来

  • Flutter 页面不是第一触点

  • 页面层只需要承接整理后的导航语义

所以这类能力如果没有专门的 Flutter 边界层,页面层很容易被入口协议污染。

十、什么时候说明这层边界已经该重构了

如果后面开始出现下面这些信号,就说明这层 Flutter 边界可能需要升级:

  • _pageIdToRoute 越来越大,已经像隐藏路由表

  • 页面层开始自己理解 pageIddishId

  • 原生层直接写死越来越多 Flutter 业务页

  • 不同入口类型共用一套 payload,但语义已经明显分叉

这时候需要重构的不是页面,而是入口边界层本身。
也就是说,Flutter 边界层应该继续演化,但依然要守住“系统入口协议别直接漏进页面层”这条线。

关键代码位置

  • app/lib/core/platform/intent_navigation_channel.dart

  • app/ohos/entry/src/main/ets/plugins/IntentNavigationPlugin.ets

  • app/ohos/entry/src/main/ets/entryability/InsightIntentExecutorImpl.ets

鸿蒙侧实现

从 HarmonyOS 原生侧看,Intent 相关代码负责的是:

  • 接住系统入口

  • 校验入口参数

  • 在 Flutter 没 ready 时先缓存待处理导航

  • 在 Flutter 可用时主动把入口事件推回去

也就是说,原生层解决的是“入口从鸿蒙系统到应用边界”的问题。

Flutter 侧实现

从 Flutter 侧看,IntentNavigationChannel 解决的是:

  • 入口结果怎么接住

  • payload 怎么收口

  • pageId 怎么映射成 GoRouter

  • 鸿蒙系统入口和应用路由怎么衔接

这也是为什么它的封装重点不是“调用原生”,而是“承接系统入口”。

常见坑

  • 把 Intent 当成普通页面按钮跳转

  • 没处理 Flutter 还没 ready 的时机问题

  • 让原生层直接决定最终 Flutter 业务页

  • 页面层直接解析原生 payload,导致入口协议细节泄漏

  • 把鸿蒙系统入口策略散落在多个页面里处理

可复用模板

static void init(GoRouter router) {
  _router = router;

  _channel.setMethodCallHandler((call) async {
    if (call.method == 'onIntentNavigation') {
      final payload = _parseArguments(call.arguments);
      if (payload != null) {
        _navigate(payload);
      }
    }
  });

  _consumePending();
}
Intent 类 channel 的边界职责
1. 接事件
2. 解析 payload
3. 映射应用路由
4. 处理初始化时序

本篇总结

IntentNavigationChannel 的 Flutter 侧封装思路,核心不在“多写几个方法”,而在“把鸿蒙系统入口结果稳稳接住,再翻译成应用内路由”。
当前这层设计之所以稳,是因为它没有把入口协议散到页面层,也没有把最终路由决策推给原生层,而是把这条转换链准确地收在了 Flutter 边界层。

Logo

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

更多推荐