Flutter + 鸿蒙 Intents Kit:页面直达能力的完整接入方案

适合谁看
-
想把鸿蒙搜索直达或语义入口接进 Flutter 的开发者
-
想看完整 Intents Kit 链路的人
-
想处理"Flutter 还没 ready"这类入口时机问题的人
问题背景
页面直达能力真正难的地方不是"能不能跳页",而是:
-
系统入口怎么声明
-
参数怎么校验
-
Flutter 未初始化时怎么办
-
原生入口和 Flutter 路由怎么解耦
项目中的真实场景
食界探味当前完整链路涉及 4 个文件:
|
层 |
文件 |
职责 |
|---|---|---|
|
配置层 |
|
声明系统入口 |
|
执行器 |
|
参数校验 |
|
插件层 |
|
桥接 Flutter |
|
Flutter 层 |
|
路由跳转 |
核心实现
一、配置层——insight_intent.json
{
"insightIntents": [
{
"intentName": "JumpFunctionPage",
"domain": "ToolsDomain",
"intentVersion": "1.0.1",
"srcEntry": "./ets/entryability/InsightIntentExecutorImpl.ets",
"uiAbility": {
"ability": "EntryAbility",
"executeMode": ["foreground"]
},
"inputParams": [
{
"properties": {
"pageId": {
"type": "string",
"enum": [
{
"value": "search",
"displayName": "搜索美食",
"keywords": ["搜索", "找菜", "查菜"],
"displayDescription": "搜索全球美食与食材"
},
{
"value": "wish_box",
"displayName": "心愿单",
"keywords": ["心愿", "想吃"],
"displayDescription": "查看我的美食心愿"
},
{
"value": "ingredients",
"displayName": "食材探索",
"keywords": ["食材", "原料"],
"displayDescription": "浏览全球食材"
},
{
"value": "explore",
"displayName": "探索美食",
"keywords": ["探索", "推荐", "发现"],
"displayDescription": "浏览今日推荐与全球美食"
},
{
"value": "dish_detail",
"displayName": "查看菜品详情",
"keywords": ["菜品", "详情", "做法"],
"displayDescription": "打开指定菜品的详情页"
}
]
}
}
}
]
}
]
}
这个配置告诉鸿蒙系统:
-
入口名叫
JumpFunctionPage -
属于
ToolsDomain领域 -
执行器在
InsightIntentExecutorImpl.ets -
参数
pageId有 5 个合法值 -
每个值都有
displayName、keywords、displayDescription供系统搜索展示
系统会根据 keywords 和 displayName 在搜索结果中展示这些入口。用户搜索"搜索"时,系统会推荐"搜索美食"入口。
二、执行器——InsightIntentExecutorImpl.ets
import { insightIntent, InsightIntentExecutor } from '@kit.AbilityKit';
const VALID_PAGE_IDS: string[] = [
'search', 'wish_box', 'ingredients', 'explore', 'dish_detail'
];
export default class InsightIntentExecutorImpl extends InsightIntentExecutor {
onExecuteInUIAbilityForegroundMode(
name: string,
param: Record<string, Object>,
pageLoader: window.WindowStage
): Promise<insightIntent.ExecuteResult> {
switch (name) {
case 'JumpFunctionPage':
return this.jumpFunctionPage(param);
default:
return Promise.resolve(makeResult(-1, 'unknown intent'));
}
}
private jumpFunctionPage(param: Record<string, Object>): Promise<insightIntent.ExecuteResult> {
return new Promise((resolve) => {
// 1. 类型校验
if (typeof param?.pageId !== 'string') {
resolve(makeResult(-1, 'pageId type error'));
return;
}
const pageId = param.pageId as string;
const dishId = typeof param?.dishId === 'string' ? param.dishId : undefined;
// 2. 白名单校验
if (!VALID_PAGE_IDS.includes(pageId)) {
resolve(makeResult(-1, `unknown pageId: ${pageId}`));
return;
}
// 3. 详情页参数校验
if (pageId === 'dish_detail' && (!dishId || dishId.length === 0)) {
resolve(makeResult(-1, 'dishId type error'));
return;
}
// 4. 桥接到 Flutter
const plugin = IntentNavigationPlugin.getInstance();
if (plugin !== null) {
plugin.navigateToPage(pageId, dishId); // Flutter 已 ready
} else {
IntentNavigationPlugin.setPendingNavigation(pageId, dishId); // Flutter 未 ready
}
resolve(makeResult(0, 'success'));
});
}
}
执行器做了 4 件事:
|
步骤 |
作用 |
|---|---|
|
类型校验 |
|
|
白名单校验 |
|
|
详情页参数校验 |
|
|
桥接到 Flutter |
调用插件的 |
三、插件层——IntentNavigationPlugin.ets
interface PendingNavigation {
pageId: string;
dishId?: string;
}
export default class IntentNavigationPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
private static instance: IntentNavigationPlugin | null = null;
private static pendingNavigation: PendingNavigation | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(
binding.getBinaryMessenger(),
'com.foodvoyage.intent_navigation'
);
this.channel.setMethodCallHandler(this);
IntentNavigationPlugin.instance = this;
}
navigateToPage(pageId: string, dishId?: string): void {
if (this.channel) {
// Flutter 已 ready,直接推送
const args = new Map<string, Object>();
args.set('pageId', pageId);
if (dishId) args.set('dishId', dishId);
this.channel.invokeMethod('onIntentNavigation', args);
} else {
// Flutter 未 ready,暂存为 pending
IntentNavigationPlugin.pendingNavigation = { pageId, dishId };
}
}
private handleConsumePendingNavigation(result: MethodResult): void {
const navigation = IntentNavigationPlugin.pendingNavigation;
IntentNavigationPlugin.pendingNavigation = null;
if (!navigation) {
result.success(null);
return;
}
const args = new Map<string, Object>();
args.set('pageId', navigation.pageId);
if (navigation.dishId) args.set('dishId', navigation.dishId);
result.success(args);
}
}
插件的核心设计是 pending 机制:
执行器调用 navigateToPage()
│
├─ channel 已可用 → 直接 invokeMethod 推给 Flutter
│
└─ channel 未可用 → 存入 static pendingNavigation
等 Flutter 初始化后主动消费
这解决了"原生入口早于 Flutter 初始化"的问题。鸿蒙的 InsightIntent 可能在应用启动时就触发,而 Flutter 引擎需要几百毫秒才能 ready。pending 机制确保了这个时间差不会导致入口丢失。
四、Flutter 层——intent_navigation_channel.dart
class IntentNavigationChannel {
static const _channel = MethodChannel('com.foodvoyage.intent_navigation');
static const _pageIdToRoute = <String, String>{
'search': '/search',
'ai_assistant': '/ai-assistant',
'wish_box': '/wish-box',
'ingredients': '/ingredients',
'explore': '/explore',
};
static GoRouter? _router;
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);
}
});
// 消费 pending navigation
_consumePending();
}
static Future<void> _consumePending() async {
try {
final payload = await _channel.invokeMethod<Object?>('consumePendingNavigation');
final navigation = _parseArguments(payload);
if (navigation != null) _navigate(navigation);
} on MissingPluginException {
// 非鸿蒙平台,忽略
} catch (e) {
AppLogger.warning('consumePendingNavigation failed: $e');
}
}
}
Flutter 层做了两件事:
-
监听实时推送 —
onIntentNavigation事件 -
消费 pending — 初始化时主动调用
consumePendingNavigation获取暂存的入口
五、完整的页面直达链路
用户在鸿蒙系统搜索"搜索美食"
│
▼
鸿蒙系统找到 JumpFunctionPage 入口
│
▼
InsightIntentExecutorImpl.onExecuteInUIAbilityForegroundMode()
│
├─ 参数校验(pageId 类型、白名单、dishId)
│
├─ IntentNavigationPlugin.navigateToPage('search')
│ │
│ ├─ channel 已可用 → invokeMethod('onIntentNavigation', {pageId: 'search'})
│ │ │
│ │ ▼
│ │ Flutter: IntentNavigationChannel._navigate()
│ │ → _pageIdToRoute['search'] = '/search'
│ │ → _router.go('/search')
│ │
│ └─ channel 未可用 → pendingNavigation = {pageId: 'search'}
│ │
│ ▼ Flutter 初始化后
│ │
│ Flutter: _consumePending()
│ → invokeMethod('consumePendingNavigation')
│ → 拿到 {pageId: 'search'}
│ → _navigate() → _router.go('/search')
│
▼
用户进入搜索页面
关键代码位置
|
文件 |
作用 |
|---|---|
|
|
入口配置 |
|
|
参数校验 |
|
|
桥接 Flutter |
|
|
路由跳转 |
常见坑
-
入口配置里没有稳定参数定义 — keywords 和 displayName 要覆盖用户可能搜索的词
-
原生侧不做白名单校验 — 非法 pageId 会导致 Flutter 路由出错
-
Flutter 初始化后没有消费 pending navigation — 入口被触发但页面没跳转
-
直接让原生层理解 Flutter 复杂路由 — 原生只管 pageId,路由细节留给 Flutter
-
dish_detail 没有校验 dishId — 详情页入口没有参数会报错
-
MissingPluginException 不处理 — 非鸿蒙平台会抛异常
可复用模板
入口配置模板(insight_intent.json)
{
"insightIntents": [{
"intentName": "JumpToPage",
"domain": "YourDomain",
"srcEntry": "./ets/entryability/IntentExecutorImpl.ets",
"uiAbility": { "ability": "EntryAbility", "executeMode": ["foreground"] },
"inputParams": [{
"properties": {
"pageId": {
"type": "string",
"enum": [
{ "value": "home", "displayName": "首页", "keywords": ["首页", "主页"] },
{ "value": "profile", "displayName": "我的", "keywords": ["个人", "设置"] }
]
}
}
}]
}]
}
执行器模板(TypeScript)
const VALID_PAGE_IDS = ['home', 'profile'];
class IntentExecutor extends InsightIntentExecutor {
onExecuteInUIAbilityForegroundMode(name, param, pageLoader) {
if (name === 'JumpToPage') {
const pageId = param.pageId as string;
if (!VALID_PAGE_IDS.includes(pageId)) {
return Promise.resolve({ code: -1, result: { message: 'invalid pageId' } });
}
const plugin = IntentPlugin.getInstance();
if (plugin) plugin.navigateToPage(pageId);
else IntentPlugin.setPendingNavigation(pageId);
return Promise.resolve({ code: 0, result: { message: 'success' } });
}
}
}
Flutter 路由映射模板
static const _pageIdToRoute = <String, String>{
'home': '/home',
'profile': '/profile',
};
static void _navigate(NavigationPayload payload) {
final route = _pageIdToRoute[payload.pageId];
if (route == null) return;
_router?.go(route);
}
本篇总结
页面直达能力是一条"配置 → 执行器 → 插件 → Flutter 路由"的完整链路。食界探味当前的设计:
-
配置层声明入口 —
insight_intent.json定义 pageId 枚举和搜索关键词 -
执行器校验参数 — 白名单校验 + 详情页参数校验
-
插件桥接 Flutter — channel 可用时直接推送,不可用时暂存 pending
-
Flutter 消费入口 — 初始化时消费 pending + 监听实时推送
原生侧负责系统入口,Flutter 侧负责业务路由。只要 pending navigation 问题处理好,这条链路就会稳定很多。
更多推荐



所有评论(0)