鸿蒙搜索直达到 Flutter 页面的全链路:insight_intent.json 配置到 ArkTS Executor 到 Flutter 路由
适合谁看
-
正在做鸿蒙 InsightIntent 接入但不清楚全链路的开发者
-
想让 Flutter 应用支持小艺搜索直达的开发者
-
遇到"搜索配置了但跳转不生效"问题的人
问题背景
鸿蒙的 InsightIntent(小艺搜索直达)允许用户在系统搜索框中输入关键词,直接跳转到应用内的特定页面。对于 Flutter 应用来说,这条链路比普通 Deep Link 更复杂,因为它涉及:
-
insight_intent.json的声明式配置 -
ArkTS 侧的
InsightIntentExecutor执行器 -
Flutter 引擎的 ready 时机判断
-
Flutter 侧 GoRouter 的路由栈管理
任何一层配置错误,都会导致搜索直达失败。
项目中的真实场景
食界探味支持以下搜索直达场景:
|
搜索关键词 |
pageId |
目标页面 |
说明 |
|---|---|---|---|
|
搜索、找菜 |
|
搜索页 |
非 shell 路由,需要 push |
|
AI 助手 |
|
AI 助手页 |
需要检查 enableAi 开关 |
|
愿望盒 |
|
愿望盒页 |
非 shell 路由 |
|
食材、食材列表 |
|
食材列表页 |
非 shell 路由 |
|
探索、发现 |
|
首页 |
shell 路由,用 go |
|
某道菜名 |
|
菜品详情页 |
需要 dishId 参数 |
这些场景覆盖了 shell 路由和非 shell 路由两种类型,以及需要额外参数(dishId)的场景。
核心实现
第一层:insight_intent.json 配置
insight_intent.json 声明了应用支持的搜索直达意图:
{
"applicable": true,
"insightIntents": [
{
"intentName": "JumpFunctionPage",
"intentId": 1001,
"parameters": [
{
"name": "pageId",
"type": "STRING",
"description": "目标页面标识"
},
{
"name": "dishId",
"type": "STRING",
"description": "菜品ID(仅dish_detail页面需要)",
"required": false
}
]
}
]
}
关键配置点:
-
intentName必须和InsightIntentExecutorImpl中的JUMP_FUNCTION_PAGE常量一致 -
parameters定义了搜索系统可以传递的参数 -
dishId标记为required: false,因为不是所有页面都需要
第二层:InsightIntentExecutorImpl 执行器
ArkTS 侧的执行器负责接收搜索系统传来的参数并转发给 Flutter:
// InsightIntentExecutorImpl.ets
export default class InsightIntentExecutorImpl extends InsightIntentExecutor {
private static readonly JUMP_FUNCTION_PAGE = 'JumpFunctionPage';
onExecuteInUIAbilityForegroundMode(
name: string,
param: Record<string, Object>,
pageLoader: window.WindowStage
): Promise<insightIntent.ExecuteResult> {
switch (name) {
case InsightIntentExecutorImpl.JUMP_FUNCTION_PAGE:
return this.jumpFunctionPage(param);
default:
break;
}
return Promise.resolve(makeResult(-1, 'unknown intent'));
}
private jumpFunctionPage(
param: Record<string, Object>
): Promise<insightIntent.ExecuteResult> {
return new Promise<insightIntent.ExecuteResult>((resolve) => {
// 1. 校验 pageId 类型
if (typeof param?.pageId !== 'string') {
resolve(makeResult(-1, 'pageId type error'));
return;
}
const pageId: string = param.pageId as string;
const dishId = typeof param?.dishId === 'string' ? param.dishId as string : undefined;
// 2. 校验 pageId 合法性
if (!VALID_PAGE_IDS.includes(pageId)) {
resolve(makeResult(-1, `unknown pageId: ${pageId}`));
return;
}
// 3. dish_detail 页面必须有 dishId
if (pageId === 'dish_detail' && (!dishId || dishId.length === 0)) {
resolve(makeResult(-1, 'dishId type error'));
return;
}
// 4. 尝试通过 Plugin 推送到 Flutter
const plugin: IntentNavigationPlugin | null = IntentNavigationPlugin.getInstance();
if (plugin !== null) {
plugin.navigateToPage(pageId, dishId);
} else {
// Flutter 引擎未 ready,走 pending 缓存
IntentNavigationPlugin.setPendingNavigation(pageId, dishId);
console.info(TAG, `Flutter not ready, pageId "${pageId}" stored as pending`);
}
resolve(makeResult(0, 'success'));
});
}
}
执行器的设计要点:
-
VALID_PAGE_IDS白名单防止非法 pageId 传入 Flutter -
dish_detail页面做额外校验,确保dishId存在 -
先尝试
IntentNavigationPlugin.getInstance(),如果为 null(Flutter 未 ready)则走 pending 缓存 -
返回
ExecuteResult给搜索系统,告知执行结果
第三层:IntentNavigationPlugin 的两种推送路径
// IntentNavigationPlugin.ets
navigateToPage(pageId: string, dishId?: string): void {
if (this.channel) {
// Flutter 已 ready,直接推送
console.info(TAG, `pushing pageId "${pageId}" to Flutter`);
const args = new Map<string, Object>();
args.set('pageId', pageId);
if (dishId) {
args.set('dishId', dishId);
}
this.channel.invokeMethod('onIntentNavigation', args);
} else {
// Flutter 未 ready,缓存
console.warn(TAG, `channel not ready, storing pageId "${pageId}" as pending`);
IntentNavigationPlugin.pendingNavigation = { pageId, dishId };
}
}
第四层:Flutter 侧路由执行
Flutter 侧在 IntentNavigationChannel 中处理两种来源的导航:
-
实时推送:
onIntentNavigation事件(来自onNewWant或InsightIntentExecutor) -
延迟消费:
consumePendingNavigation请求(来自onCreate后的_consumePending)
// intent_navigation_channel.dart
static const _pageIdToRoute = <String, String>{
'search': '/search',
'ai_assistant': '/ai-assistant',
'wish_box': '/wish-box',
'ingredients': '/ingredients',
'explore': '/explore',
};
static const _shellRoutes = <String>{
'/explore',
'/inspiration',
'/collection',
'/profile',
};
static void _navigate(_NavigationPayload payload) {
// AI 助手特殊处理:检查开关
if (payload.pageId == 'ai_assistant' && !AppConfig.enableAi) {
AppLogger.info('Intent navigation blocked: pageId="${payload.pageId}"');
_router?.go('/explore');
return;
}
// dish_detail 特殊处理:需要先 go 再 push
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';
_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)) {
// shell 路由直接 go
_router?.go(route);
} else {
// 非 shell 路由先 go 首页再 push
_router?.go('/explore');
scheduleMicrotask(() {
_router?.push(route);
});
}
}
路由策略的关键区别:
|
路由类型 |
跳转方式 |
原因 |
|---|---|---|
|
shell 路由( |
|
底部 Tab 切换,直接替换当前页面 |
|
非 shell 路由( |
|
需要在路由栈中有首页作为根 |
|
详情路由( |
|
push 页面不能直接 go |
关键代码位置
-
app/ohos/entry/src/main/resources/rawfile/insight_intent.json— 搜索直达声明 -
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 侧路由执行
鸿蒙侧实现
鸿蒙侧涉及三个文件的协作:
-
insight_intent.json:声明意图名称和参数,供搜索系统索引
-
InsightIntentExecutorImpl.ets:实现
InsightIntentExecutor,在onExecuteInUIAbilityForegroundMode中处理搜索命中 -
IntentNavigationPlugin.ets:作为 Executor 和 Flutter 之间的桥梁
Executor 的执行模式有两种:
-
onExecuteInUIAbilityForegroundMode:应用在前台时执行 -
onExecuteInUIAbilityBackgroundMode:应用在后台时执行(本项目未使用)
Flutter 侧实现
Flutter 侧的核心设计是路由映射表和跳转策略:
-
_pageIdToRoute:将pageId映射为 GoRouter 路由路径 -
_shellRoutes:标识哪些路由是底部 Tab 路由(用go) -
_navigate:根据路由类型选择go或go+push组合
常见坑
-
坑 1:insight_intent.json 的 intentName 和 Executor 里的常量不一致。搜索系统通过
intentName匹配 Executor,如果拼写不一致,搜索命中后找不到执行器。 -
坑 2:VALID_PAGE_IDS 白名单遗漏。新增页面后忘记在
InsightIntentExecutorImpl的VALID_PAGE_IDS中添加,导致搜索直达被拒绝。 -
坑 3:Flutter 侧
_pageIdToRoute和 Executor 的VALID_PAGE_IDS不同步。两边维护各自的 pageId 列表,新增页面时容易遗漏一边。 -
坑 4:
scheduleMicrotask时序问题。go和push必须分两帧执行,否则 GoRouter 可能合并操作。但如果scheduleMicrotask被其他微任务干扰,跳转可能失败。 -
坑 5:搜索直达在应用冷启动时不生效。如果
insight_intent.json配置正确但搜索不生效,检查InsightIntentExecutorImpl是否在module.json5中正确声明。
可复用模板
// Flutter 侧 - 搜索直达路由处理模板
class SearchIntentRouter {
static const _channel = MethodChannel('com.example.intent_navigation');
static GoRouter? _router;
static const _pageIdToRoute = <String, String>{
'home': '/home',
'detail': '/detail',
'settings': '/settings',
};
static const _shellRoutes = {'/home', '/profile'};
static void init(GoRouter router) {
_router = router;
_channel.setMethodCallHandler((call) async {
if (call.method == 'onIntentNavigation') {
_navigate(call.arguments);
}
});
_consumePending();
}
static Future<void> _consumePending() async {
try {
final result = await _channel.invokeMethod<Object?>('consumePendingNavigation');
if (result is Map) _navigate(result);
} on MissingPluginException {}
}
static void _navigate(Object? args) {
if (args is! Map) return;
final pageId = args['pageId'] as String?;
final extra = args['extra'] as String?;
if (pageId == null) return;
final route = _pageIdToRoute[pageId];
if (route == null) return;
if (_shellRoutes.contains(route)) {
_router?.go(route);
} else {
_router?.go('/home');
scheduleMicrotask(() => _router?.push(route));
}
}
}
// 鸿蒙侧 - InsightIntentExecutor 模板
const VALID_PAGE_IDS: string[] = ['home', 'detail', 'settings'];
export default class MyIntentExecutor extends InsightIntentExecutor {
onExecuteInUIAbilityForegroundMode(
name: string,
param: Record<string, Object>,
pageLoader: window.WindowStage
): Promise<insightIntent.ExecuteResult> {
if (name !== 'JumpPage') {
return Promise.resolve({ code: -1, result: { message: 'unknown' } });
}
const pageId = param?.pageId as string;
if (!pageId || !VALID_PAGE_IDS.includes(pageId)) {
return Promise.resolve({ code: -1, result: { message: 'invalid pageId' } });
}
const plugin = NavigationPlugin.getInstance();
if (plugin) {
plugin.navigateToPage(pageId);
} else {
NavigationPlugin.setPending(pageId);
}
return Promise.resolve({ code: 0, result: { message: 'success' } });
}
}
本篇总结
鸿蒙搜索直达 Flutter 页面的全链路是:insight_intent.json 声明意图 → InsightIntentExecutor 校验参数 → IntentNavigationPlugin 推送或缓存 → IntentNavigationChannel 解析并执行 GoRouter 跳转。每一层都有自己的校验和兜底逻辑,理解这条链路的关键在于搞清楚"参数从哪里来、经过哪些校验、最终如何变成 Flutter 的路由跳转"。
更多推荐


所有评论(0)