适合谁看

  • 想把鸿蒙搜索直达或语义入口接进 Flutter 的开发者

  • 想看完整 Intents Kit 链路的人

  • 想处理"Flutter 还没 ready"这类入口时机问题的人

问题背景

页面直达能力真正难的地方不是"能不能跳页",而是:

  • 系统入口怎么声明

  • 参数怎么校验

  • Flutter 未初始化时怎么办

  • 原生入口和 Flutter 路由怎么解耦

项目中的真实场景

食界探味当前完整链路涉及 4 个文件:

文件

职责

配置层

insight_intent.json

声明系统入口

执行器

InsightIntentExecutorImpl.ets

参数校验

插件层

IntentNavigationPlugin.ets

桥接 Flutter

Flutter 层

intent_navigation_channel.dart

路由跳转

核心实现

一、配置层——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 个合法值

  • 每个值都有 displayNamekeywordsdisplayDescription 供系统搜索展示

系统会根据 keywordsdisplayName 在搜索结果中展示这些入口。用户搜索"搜索"时,系统会推荐"搜索美食"入口。

二、执行器——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 件事:

步骤

作用

类型校验

pageId 必须是 string

白名单校验

pageId 必须在合法列表中

详情页参数校验

dish_detail 必须带 dishId

桥接到 Flutter

调用插件的 navigateToPagesetPendingNavigation

三、插件层——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 层做了两件事:

  1. 监听实时推送onIntentNavigation 事件

  2. 消费 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')
  │
  ▼
用户进入搜索页面

关键代码位置

文件

作用

app/ohos/entry/src/main/resources/base/profile/insight_intent.json

入口配置

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

参数校验

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

桥接 Flutter

app/lib/core/platform/intent_navigation_channel.dart

路由跳转

常见坑

  • 入口配置里没有稳定参数定义 — 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 路由"的完整链路。食界探味当前的设计:

  1. 配置层声明入口insight_intent.json 定义 pageId 枚举和搜索关键词

  2. 执行器校验参数 — 白名单校验 + 详情页参数校验

  3. 插件桥接 Flutter — channel 可用时直接推送,不可用时暂存 pending

  4. Flutter 消费入口 — 初始化时消费 pending + 监听实时推送

原生侧负责系统入口,Flutter 侧负责业务路由。只要 pending navigation 问题处理好,这条链路就会稳定很多。

Logo

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

更多推荐