适合谁看
-
正在做鸿蒙 InsightIntent 接入但不清楚全链路的开发者
-
想让 Flutter 应用支持小艺搜索直达的开发者
-
遇到"搜索配置了但跳转不生效"问题的人
问题背景
鸿蒙的 InsightIntent(小艺搜索直达)允许用户在系统搜索框中输入关键词,直接跳转到应用内的特定页面。对于 Flutter 应用来说,这条链路比普通 Deep Link 更复杂,因为它涉及:
-
insight_intent.json的声明式配置 -
ArkTS 侧的
InsightIntentExecutor执行器 -
Flutter 引擎的 ready 时机判断
-
Flutter 侧 GoRouter 的路由栈管理
任何一层配置错误,都会导致搜索直达失败。
项目中的真实场景
食界探味支持以下搜索直达场景:
| 搜索关键词 | pageId | 目标页面 | 说明 |
|---|---|---|---|
| 搜索、找菜 | search |
搜索页 | 非 shell 路由,需要 push |
| AI 助手 | ai_assistant |
AI 助手页 | 需要检查 enableAi 开关 |
| 愿望盒 | wish_box |
愿望盒页 | 非 shell 路由 |
| 食材、食材列表 | ingredients |
食材列表页 | 非 shell 路由 |
| 探索、发现 | explore |
首页 | shell 路由,用 go |
| 某道菜名 | dish_detail |
菜品详情页 | 需要 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 =
{
'/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 路由(/explore 等) |
go(route) |
底部 Tab 切换,直接替换当前页面 |
非 shell 路由(/search 等) |
go('/explore') + push(route) |
需要在路由栈中有首页作为根 |
详情路由(/dish/:id) |
go('/explore') + push(route) |
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 的路由跳转"。
