适合谁看

  • 遇到过"鸿蒙原生收到入口但 Flutter 没反应"的开发者

  • 正在做多入口系统接入的人

  • 想处理冷启动导航时机问题的人

问题背景

普通页面按钮跳转不会遇到这个问题,因为 Flutter 页面已经在运行。但鸿蒙系统入口、搜索直达、冷启动参数不一样。这时顺序可能变成:

鸿蒙系统入口触发
  │
  ├─ 场景 1:冷启动
  │   鸿蒙先收到入口 → Flutter 引擎还在启动 → 路由系统未初始化
  │
  └─ 场景 2:热启动(应用已在后台)
      鸿蒙先收到入口 → Flutter 可能还没恢复 → channel 可能未就绪

如果原生侧没有缓存机制,这些入口请求就会丢失。

项目中的真实场景

食界探味当前的缓存机制涉及 3 个文件:

文件

角色

IntentNavigationPlugin.ets

维护 pendingNavigation 缓存

EntryAbility.ets

冷启动和热启动时写入缓存

intent_navigation_channel.dart

Flutter 初始化后消费缓存

核心实现

一、IntentNavigationPlugin——缓存的核心

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;

  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,暂存为 pending
      console.warn(TAG, `channel not ready, storing pageId "${pageId}" as 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);
  }
}

关键设计:

  • pendingNavigationstatic 的,确保所有实例共享同一个缓存

  • navigateToPage() 先判断 channel 是否可用,再决定是推送还是缓存

  • handleConsumePendingNavigation() 消费后立即清空,防止重复处理

二、EntryAbility——冷启动和热启动的处理

export default class EntryAbility extends FlutterAbility {
  configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    GeneratedPluginRegistrant.registerWith(flutterEngine)
    flutterEngine.getPlugins()?.add(new IntentNavigationPlugin())
    // ... 其他插件
  }

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    super.onCreate(want, launchParam)
    // 冷启动:把启动参数写成 pending navigation
    const pageId = want.parameters?.['pageId'] as string;
    const dishId = want.parameters?.['dishId'] as string;
    if (pageId) {
      IntentNavigationPlugin.setPendingNavigation(pageId, dishId);
    }
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    super.onNewWant(want, launchParam)
    // 热启动:尝试直接推送,失败则缓存
    const pageId = want.parameters?.['pageId'] as string;
    const dishId = want.parameters?.['dishId'] as string;
    if (pageId) {
      const plugin = IntentNavigationPlugin.getInstance();
      if (plugin) {
        plugin.navigateToPage(pageId, dishId);
      } else {
        IntentNavigationPlugin.setPendingNavigation(pageId, dishId);
      }
    }
  }
}

两种场景的处理差异:

场景

方法

处理方式

冷启动

onCreate

直接写入 pendingNavigation(因为 plugin 还没创建)

热启动

onNewWant

先尝试 navigateToPage,失败则写入 pendingNavigation

为什么冷启动不能直接调 navigateToPage

冷启动时:
  EntryAbility.onCreate() 被调用
    → IntentNavigationPlugin 还没被 configureFlutterEngine() 注册
    → getInstance() 返回 null
    → 只能写入 pendingNavigation

热启动时:
  EntryAbility.onNewWant() 被调用
    → IntentNavigationPlugin 已经被注册
    → getInstance() 可能返回实例
    → 可以尝试 navigateToPage()

三、Flutter 侧——初始化后主动消费

// intent_navigation_channel.dart

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');
  }
}

关键设计:

  • init() 在监听实时推送的同时,主动调用 _consumePending()

  • consumePendingNavigation 会返回缓存的导航数据(如果有的话)

  • 消费后缓存立即清空,不会重复处理

四、完整的时序图

场景 1:冷启动(应用未运行)

用户在鸿蒙搜索点击"搜索美食"
  │
  ▼
鸿蒙系统启动应用 → EntryAbility.onCreate(want)
  │
  ├─ 提取 pageId='search'
  ├─ IntentNavigationPlugin.setPendingNavigation('search')
  │   → pendingNavigation = { pageId: 'search' }
  │
  ▼
configureFlutterEngine()
  ├─ 注册 IntentNavigationPlugin
  │
  ▼
Flutter 引擎启动
  │
  ▼
intent_navigation_channel.dart.init()
  ├─ 监听 onIntentNavigation(实时推送)
  ├─ _consumePending()
  │   → invokeMethod('consumePendingNavigation')
  │   → 拿到 { pageId: 'search' }
  │   → _navigate() → _router.go('/search')
  │
  ▼
用户进入搜索页面

场景 2:热启动(应用在后台)

应用在后台运行
  │
  ▼
用户在鸿蒙搜索点击"探索美食"
  │
  ▼
EntryAbility.onNewWant(want)
  ├─ 提取 pageId='explore'
  ├─ IntentNavigationPlugin.getInstance()
  │   │
  │   ├─ plugin 不为 null → navigateToPage('explore')
  │   │   → channel.invokeMethod('onIntentNavigation', {pageId: 'explore'})
  │   │   → Flutter 收到实时推送 → _navigate()
  │   │
  │   └─ plugin 为 null → setPendingNavigation('explore')
  │       → Flutter 初始化后消费
  │
  ▼
用户进入探索页面

场景 3:鸿蒙搜索直达(非启动入口)

应用已在前台运行
  │
  ▼
用户在鸿蒙搜索点击"搜索美食"
  │
  ▼
InsightIntentExecutorImpl.jumpFunctionPage()
  ├─ 校验 pageId='search'
  ├─ IntentNavigationPlugin.getInstance()
  │   → plugin 不为 null
  │   → navigateToPage('search')
  │   → channel.invokeMethod('onIntentNavigation', {pageId: 'search'})
  │
  ▼
Flutter 收到实时推送 → _navigate()
  │
  ▼
用户进入搜索页面

五、为什么 pendingNavigation 必须是 static

private static pendingNavigation: PendingNavigation | null = null;

因为 EntryAbilityIntentNavigationPlugin 是两个不同的类。EntryAbility.onCreate() 需要写入缓存,IntentNavigationPlugin.handleConsumePendingNavigation() 需要读取缓存。只有 static 才能让两个类共享同一个缓存。

六、为什么消费后要立即清空

private handleConsumePendingNavigation(result: MethodResult): void {
  const navigation = IntentNavigationPlugin.pendingNavigation;
  IntentNavigationPlugin.pendingNavigation = null;  // 立即清空
  // ...
}

如果不清空,同一个导航请求会被多次处理——用户会看到页面跳转了两次。

关键代码位置

文件

作用

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

缓存核心

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

冷启动/热启动处理

app/lib/core/platform/intent_navigation_channel.dart

Flutter 消费缓存

三种场景对比

场景

入口来源

Flutter 状态

处理方式

冷启动

onCreate(want)

未初始化

写入 pending

热启动

onNewWant(want)

可能未恢复

尝试推送,失败则 pending

搜索直达

InsightIntentExecutorImpl

已运行

直接推送

常见坑

  • 原生入口到了,但 Flutter handler 还没注册 — 必须有 pending 机制

  • 没有 pending 机制,冷启动请求直接丢失 — 用户点击搜索直达但页面没跳转

  • 只处理 onCreate,不处理 onNewWant — 热启动时入口丢失

  • Flutter 初始化后忘了主动补消费逻辑_consumePending() 必须在 init() 中调用

  • pendingNavigation 没有清空 — 同一导航被多次处理

  • pendingNavigation 不是 static — 两个类无法共享缓存

  • 没有处理 MissingPluginException — 非鸿蒙平台会抛异常

可复用模板

鸿蒙插件缓存模板

interface PendingNavigation {
  pageId: string;
  extra?: string;
}

export default class NavPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;
  private static instance: NavPlugin | null = null;
  private static pendingNavigation: PendingNavigation | null = null;

  navigateToPage(pageId: string, extra?: string): void {
    if (this.channel) {
      const args = new Map<string, Object>();
      args.set('pageId', pageId);
      if (extra) args.set('extra', extra);
      this.channel.invokeMethod('onNavigation', args);
    } else {
      NavPlugin.pendingNavigation = { pageId, extra };
    }
  }

  private handleConsume(result: MethodResult): void {
    const nav = NavPlugin.pendingNavigation;
    NavPlugin.pendingNavigation = null;
    if (!nav) { result.success(null); return; }
    const args = new Map<string, Object>();
    args.set('pageId', nav.pageId);
    if (nav.extra) args.set('extra', nav.extra);
    result.success(args);
  }
}

EntryAbility 处理模板

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  super.onCreate(want, launchParam)
  const pageId = want.parameters?.['pageId'] as string;
  if (pageId) {
    NavPlugin.setPendingNavigation(pageId);
  }
}

onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  super.onNewWant(want, launchParam)
  const pageId = want.parameters?.['pageId'] as string;
  if (pageId) {
    const plugin = NavPlugin.getInstance();
    if (plugin) {
      plugin.navigateToPage(pageId);
    } else {
      NavPlugin.setPendingNavigation(pageId);
    }
  }
}

Flutter 消费模板

static void init(GoRouter router) {
  _router = router;
  _channel.setMethodCallHandler((call) async {
    if (call.method == 'onNavigation') {
      final payload = _parseArguments(call.arguments);
      if (payload != null) _navigate(payload);
    }
  });
  _consumePending();  // 主动消费
}

static Future<void> _consumePending() async {
  try {
    final payload = await _channel.invokeMethod<Object?>('consumePending');
    final nav = _parseArguments(payload);
    if (nav != null) _navigate(nav);
  } on MissingPluginException {
    // 非鸿蒙平台,忽略
  }
}

本篇总结

鸿蒙冷启动入口和普通页面跳转最大的不同,就是时机不确定。食界探味的处理方案:

  1. IntentNavigationPlugin 维护 static 缓存pendingNavigation 让两个类共享同一个缓存

  2. EntryAbility 区分冷启动/热启动onCreate 写入 pending,onNewWant 尝试推送

  3. Flutter 初始化后主动消费_consumePending()init() 中调用

  4. 消费后立即清空 — 防止同一导航被多次处理

pendingNavigation 这种设计是鸿蒙系统入口项目里非常实用的一层兜底。入口不丢失,比入口"马上跳"更重要。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐