鸿蒙 EntryAbility 的完整启动链:Flutter 引擎初始化、插件注册与首帧渲染的时序控制

适合谁看

  • 正在做 Flutter 鸿蒙项目,但对 EntryAbility 生命周期不清晰的开发者

  • 遇到"Flutter 页面还没出来,原生跳转参数就丢了"这类问题的人

  • 想理解鸿蒙 Stage 模型下 Flutter 应用启动全链路的开发者

问题背景

在一个纯 Flutter 项目里,应用启动流程相对简单:main() 执行 → Flutter 引擎初始化 → 首页渲染。但在 Flutter 鸿蒙项目里,启动链路多了一层鸿蒙 Ability 生命周期,而这层生命周期里的时序问题,往往是很多 bug 的根源。

典型问题包括:

  • 应用从卡片或搜索跳转进来时,pageId 参数丢失

  • Flutter 页面还没渲染完成,原生就尝试调用 MethodChannel 导致 MissingPluginException

  • 沉浸式窗口配置后,Flutter 页面的 MediaQuery.padding 出现异常偏移

这些问题的根源都在于:没有搞清楚 EntryAbility 各生命周期的调用顺序,以及 Flutter 引擎在其中处于什么位置

项目中的真实场景

食界探味的 EntryAbility 是整个鸿蒙壳工程的唯一 Ability,它承担了四项职责:

  1. 配置 Flutter 引擎 :注册 GeneratedPluginRegistrant 和 4 个自定义插件

  2. 接收系统入口参数 :从 Want 中提取 pageIddishId

  3. 处理二次启动onNewWant 处理应用已在前台时的新跳转

  4. 配置窗口样式:设置沉浸式全屏和隐藏系统栏

这四项职责的调用时序,直接决定了 Flutter 侧能不能正确接收到导航参数。

核心实现

启动时序全图

复制代码
用户点击图标/卡片/搜索结果
        ↓
  EntryAbility.onCreate(want)
        ↓ 提取 pageId, dishId
        ↓ 存入 IntentNavigationPlugin.pendingNavigation
        ↓
  EntryAbility.configureFlutterEngine(flutterEngine)
        ↓ GeneratedPluginRegistrant.registerWith(flutterEngine)
        ↓ flutterEngine.getPlugins()?.add(SpeechRecognitionPlugin)
        ↓ flutterEngine.getPlugins()?.add(TextToSpeechPlugin)
        ↓ flutterEngine.getPlugins()?.add(IntentNavigationPlugin)
        ↓ flutterEngine.getPlugins()?.add(AntiPeepProtectionPlugin)
        ↓
  EntryAbility.onWindowStageCreate(windowStage)
        ↓ mainWindow.setWindowLayoutFullScreen(true)
        ↓ mainWindow.setWindowSystemBarEnable([])
        ↓
  Flutter 引擎启动,main() 执行
        ↓
  GoRouter 初始化 → IntentNavigationChannel.init(router)
        ↓
  _consumePending() 消费 pendingNavigation
        ↓ Flutter 跳转到目标页面

为什么 onCreate 在 configureFlutterEngine 之前

这是 Stage 模型的标准行为。onCreate 是 Ability 的生命周期回调,在 Flutter 引擎创建之前触发。这意味着:

  • onCreate 里拿到的 want.parameters 是可靠的

  • 但此时 Flutter 引擎还没有创建,MethodChannel 不可用

  • 所以必须用 static pendingNavigation 做缓存,等 Flutter 侧 ready 后再消费

    // EntryAbility.ets - onCreate
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    super.onCreate(want, launchParam)
    const pageId = want.parameters?.['pageId'] as string;
    const dishId = want.parameters?.['dishId'] as string;
    if (pageId) {
    // 此时 Flutter 引擎尚未创建,MethodChannel 不可用
    // 只能先缓存参数
    IntentNavigationPlugin.setPendingNavigation(pageId, dishId);
    }
    }

onNewWant:应用已在前台时的二次跳转

当应用已经在前台,用户从小艺搜索或卡片再次跳转时,系统不会重新创建 Ability,而是调用 onNewWant。此时 Flutter 引擎已经 ready,MethodChannel 可用:

复制代码
// EntryAbility.ets - onNewWant
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) {
      // Flutter 引擎已 ready,直接通过 MethodChannel 推送
      plugin.navigateToPage(pageId, dishId);
    } else {
      // 理论上不应走到这里,但做防御性编程
      IntentNavigationPlugin.setPendingNavigation(pageId, dishId);
    }
  }
}

两条路径的区别:

场景 调用方法 Flutter 引擎状态 参数传递方式
首次启动(冷启动) onCreate 未创建 pendingNavigation 缓存
二次跳转(热启动) onNewWant 已 ready MethodChannel 直接推送

configureFlutterEngine:插件注册的顺序

复制代码
// EntryAbility.ets - configureFlutterEngine
configureFlutterEngine(flutterEngine: FlutterEngine) {
  super.configureFlutterEngine(flutterEngine)
  // 第一步:注册自动生成的插件(如有)
  GeneratedPluginRegistrant.registerWith(flutterEngine)
  // 第二步:手动注册自定义插件
  flutterEngine.getPlugins()?.add(new SpeechRecognitionPlugin())
  flutterEngine.getPlugins()?.add(new TextToSpeechPlugin())
  flutterEngine.getPlugins()?.add(new IntentNavigationPlugin())
  flutterEngine.getPlugins()?.add(new AntiPeepProtectionPlugin())
}

关键点:

  • super.configureFlutterEngine 必须先调用,否则 Flutter 引擎基础配置不完整

  • GeneratedPluginRegistrant.registerWith 是自动生成的,注册通过 pubspec.yaml 声明的第三方插件

  • 自定义插件通过 getPlugins()?.add() 手动注册,顺序不影响功能但影响调试日志顺序

  • 每个插件的 onAttachedToEngine 会在此时被调用,此时 MethodChannel 已创建

onWindowStageCreate:沉浸式窗口配置

复制代码
// EntryAbility.ets - onWindowStageCreate
onWindowStageCreate(windowStage: window.WindowStage): void {
  super.onWindowStageCreate(windowStage)

  windowStage.getMainWindow().then((mainWindow: window.Window) => {
    mainWindow.setWindowLayoutFullScreen(true)
    mainWindow.setWindowSystemBarEnable([])
  }).catch((err: Error) => {
    console.error(`Failed to enable immersive window: ${JSON.stringify(err)}`)
  })
}

这里配置了沉浸式全屏并隐藏系统栏。对 Flutter 侧的影响:

  • MediaQuery.padding.top 会变为 0(因为系统栏被隐藏)

  • Flutter 页面需要自己处理状态栏区域的安全间距

  • 如果 Flutter 页面用了 ScaffoldbodySafeArea 会自动处理

关键代码位置

  • app/ohos/entry/src/main/ets/entryability/EntryAbility.ets --- Ability 生命周期与插件注册

  • app/ohos/entry/src/main/ets/plugins/IntentNavigationPlugin.ets --- pending navigation 缓存与消费

  • app/lib/core/platform/intent_navigation_channel.dart --- Flutter 侧初始化与 pending 消费

鸿蒙侧实现

鸿蒙侧的核心工作在 EntryAbility.ets 中完成:

  1. onCreate:从 Want 提取 pageId/dishId,存入 IntentNavigationPlugin.pendingNavigation

  2. configureFlutterEngine:注册 GeneratedPluginRegistrant + 4 个自定义插件

  3. onNewWant:判断插件实例是否已创建,选择直接推送或缓存

  4. onWindowStageCreate:配置沉浸式窗口

Flutter 侧实现

Flutter 侧在 IntentNavigationChannel.init(router) 中完成:

  1. 设置 MethodChannel 的 setMethodCallHandler,监听 onIntentNavigation 事件

  2. 调用 _consumePending() 向原生侧请求已缓存的 pending navigation

  3. 解析 pageId + dishId,通过 GoRouter 执行跳转

关键代码:

复制代码
// 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');
  }
}

常见坑

  • 坑 1:onCreate 里直接调用 MethodChannel。此时 Flutter 引擎尚未创建,MethodChannel 不可用,会抛出异常。必须用 pending 缓存机制。

  • 坑 2:onNewWant 里不检查 plugin 实例 。如果插件还没注册完成就调用 channel.invokeMethod,同样会失败。需要先 getInstance() 判断。

  • 坑 3:沉浸式窗口配置后 Flutter 布局错位setWindowLayoutFullScreen(true) 会改变 MediaQuery.padding,Flutter 侧需要确认 SafeArea 或自定义 padding 是否正确。

  • 坑 4:configureFlutterEngine 里忘记 super 调用 。没有 super.configureFlutterEngine,Flutter 引擎的基础配置不完整,后续插件注册可能静默失败。

可复用模板

复制代码
// Flutter 侧 - pending navigation 消费模板
class PlatformNavigationHelper {
  static const _channel = MethodChannel('com.example.navigation');
  static GoRouter? _router;

  static void init(GoRouter router) {
    _router = router;
    _channel.setMethodCallHandler((call) async {
      if (call.method == 'onNavigation') {
        _handleNavigation(call.arguments);
      }
    });
    _consumePending();
  }

  static Future<void> _consumePending() async {
    try {
      final result = await _channel.invokeMethod<Object?>('consumePending');
      if (result is Map && result['pageId'] != null) {
        _handleNavigation(result);
      }
    } on MissingPluginException {
      // 非鸿蒙平台
    }
  }

  static void _handleNavigation(Object? args) {
    if (args is! Map) return;
    final pageId = args['pageId'] as String?;
    if (pageId == null) return;
    // 根据 pageId 执行路由跳转
  }
}

// 鸿蒙侧 - pending navigation 缓存模板
export default class NavigationPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;
  private static instance: NavigationPlugin | null = null;
  private static pending: { pageId: string; params?: string } | null = null;

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), 'com.example.navigation');
    this.channel.setMethodCallHandler(this);
    NavigationPlugin.instance = this;
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method === 'consumePending') {
      const p = NavigationPlugin.pending;
      NavigationPlugin.pending = null;
      result.success(p ?? null);
    }
  }

  static setPending(pageId: string, params?: string): void {
    NavigationPlugin.pending = { pageId, params };
  }

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

本篇总结

EntryAbility 的启动链是 Flutter 鸿蒙项目中最关键的时序控制点。理解 onCreateconfigureFlutterEngineonNewWantonWindowStageCreate 的调用顺序,以及 Flutter 引擎在其中的 ready 时机,是解决"参数丢失"、"Channel 不可用"、"布局错位"等问题的前提。核心原则只有一条:Flutter 引擎 ready 之前,所有需要和 Flutter 通信的数据都必须走 pending 缓存