适合谁看
-
正在做 Flutter 鸿蒙项目,但对
EntryAbility生命周期不清晰的开发者 -
遇到"Flutter 页面还没出来,原生跳转参数就丢了"这类问题的人
-
想理解鸿蒙 Stage 模型下 Flutter 应用启动全链路的开发者
问题背景
在一个纯 Flutter 项目里,应用启动流程相对简单:main() 执行 → Flutter 引擎初始化 → 首页渲染。但在 Flutter 鸿蒙项目里,启动链路多了一层鸿蒙 Ability 生命周期,而这层生命周期里的时序问题,往往是很多 bug 的根源。
典型问题包括:
-
应用从卡片或搜索跳转进来时,
pageId参数丢失 -
Flutter 页面还没渲染完成,原生就尝试调用 MethodChannel 导致
MissingPluginException -
沉浸式窗口配置后,Flutter 页面的
MediaQuery.padding出现异常偏移
这些问题的根源都在于:没有搞清楚 EntryAbility 各生命周期的调用顺序,以及 Flutter 引擎在其中处于什么位置。
项目中的真实场景
食界探味的 EntryAbility 是整个鸿蒙壳工程的唯一 Ability,它承担了四项职责:
-
配置 Flutter 引擎 :注册
GeneratedPluginRegistrant和 4 个自定义插件 -
接收系统入口参数 :从
Want中提取pageId和dishId -
处理二次启动 :
onNewWant处理应用已在前台时的新跳转 -
配置窗口样式:设置沉浸式全屏和隐藏系统栏
这四项职责的调用时序,直接决定了 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 页面用了
Scaffold的body,SafeArea会自动处理
关键代码位置
-
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 中完成:
-
onCreate:从Want提取pageId/dishId,存入IntentNavigationPlugin.pendingNavigation -
configureFlutterEngine:注册GeneratedPluginRegistrant+ 4 个自定义插件 -
onNewWant:判断插件实例是否已创建,选择直接推送或缓存 -
onWindowStageCreate:配置沉浸式窗口
Flutter 侧实现
Flutter 侧在 IntentNavigationChannel.init(router) 中完成:
-
设置 MethodChannel 的
setMethodCallHandler,监听onIntentNavigation事件 -
调用
_consumePending()向原生侧请求已缓存的 pending navigation -
解析
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 鸿蒙项目中最关键的时序控制点。理解 onCreate → configureFlutterEngine → onNewWant → onWindowStageCreate 的调用顺序,以及 Flutter 引擎在其中的 ready 时机,是解决"参数丢失"、"Channel 不可用"、"布局错位"等问题的前提。核心原则只有一条:Flutter 引擎 ready 之前,所有需要和 Flutter 通信的数据都必须走 pending 缓存。
