适合谁看
-
正在做 Flutter 鸿蒙项目窗口配置但遇到布局异常的开发者
-
想理解鸿蒙沉浸式窗口对 Flutter
MediaQuery影响的开发者 -
遇到"返回键拦截后 Flutter 页面无响应"问题的人
问题背景
在纯 Flutter 项目中,窗口管理(状态栏、导航栏、返回键)主要通过 SystemUiOverlayStyle 和 WillPopScope 处理。但在 Flutter 鸿蒙项目中,这些能力由鸿蒙系统 API 控制,需要在 ArkTS 侧配置,再通过事件通道同步到 Flutter 侧。
典型问题:
-
配置沉浸式后,Flutter 页面的
SafeArea不生效 -
返回键被 ArkTS 拦截后,Flutter 侧收不到通知
-
状态栏颜色和 Flutter 主题不一致
项目中的真实场景
食界探味在 EntryAbility.onWindowStageCreate 中配置沉浸式全屏,在 Index.ets 中拦截返回键:
// EntryAbility.ets
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)}`)
})
}
// Index.ets
Entry(storage)
@Component
struct Index {
private context = getContext(this) as common.UIAbilityContext
@LocalStorageLink('viewId') viewId: string = "";
build() {
Column() {
FlutterPage({ viewId: this.viewId })
}
}
onBackPress(): boolean {
this.context.eventHub.emit(EVENT_BACK_PRESS)
return true
}
}
核心实现
沉浸式窗口配置
setWindowLayoutFullScreen(true) 的作用:
-
应用内容延伸到状态栏和导航栏区域
-
状态栏和导航栏变为透明覆盖层
-
MediaQuery.padding.top变为 0(因为系统栏不再占据布局空间)
setWindowSystemBarEnable([]) 的作用:
-
隐藏状态栏和导航栏
-
应用获得完整的全屏显示区域
对 Flutter 侧的影响:
// Flutter 侧获取窗口信息
final padding = MediaQuery.of(context).padding;
// 配置沉浸式后:
// padding.top == 0(状态栏被隐藏)
// padding.bottom == 0(导航栏被隐藏)
// SafeArea 在这种情况下不会添加额外间距
// 因为 SafeArea 依赖 MediaQuery.padding
viewId 的作用
Index.ets 中的 @LocalStorageLink('viewId') 是一个 ArkUI 状态变量,它通过 LocalStorage 和 FlutterPage 组件关联。viewId 的作用是:
-
唯一标识当前 Flutter 页面实例
-
当系统需要向 Flutter 页面传递事件时,通过
viewId定位目标 -
在多窗口场景下区分不同的 Flutter 页面
// Index.ets
@Entry(storage)
@Component
struct Index {
@LocalStorageLink('viewId') viewId: string = "";build() {
Column() {
FlutterPage({ viewId: this.viewId })
}
}
}
FlutterPage 是 @ohos/flutter_ohos 提供的组件,它负责承载 Flutter 引擎渲染的页面。viewId 作为属性传递给 FlutterPage,用于页面标识。
返回键拦截
Index.ets 的 onBackPress 方法拦截系统返回键:
onBackPress(): boolean {
this.context.eventHub.emit(EVENT_BACK_PRESS)
return true // 返回 true 表示拦截,不执行默认返回行为
}
拦截后通过 eventHub.emit 发送事件。但这个事件目前只在 ArkTS 侧传播,Flutter 侧需要通过 MethodChannel 或 EventChannel 监听。
Flutter 侧适配策略
Flutter 侧需要处理两个问题:
-
沉浸式布局适配 :在
MediaQuery.padding.top == 0时,手动添加状态栏高度的安全间距 -
返回键事件监听 :通过 MethodChannel 监听 ArkTS 侧的
EVENT_BACK_PRESS事件// Flutter 侧 - 沉浸式布局适配
class ImmersiveLayout extends StatelessWidget {
final Widget child;const ImmersiveLayout({required this.child});
@override
Widget build(BuildContext context) {
final padding = MediaQuery.of(context).padding;
final isImmersive = padding.top == 0;return Column( children: [ // 如果是沉浸式,手动添加状态栏高度间距 if (isImmersive) SizedBox( height: MediaQuery.of(context).viewPadding.top, ), Expanded(child: child), ], );}
}// Flutter 侧 - 返回键事件监听
class BackButtonHandler {
static const _channel = MethodChannel('com.foodvoyage.back_button');
static VoidCallback? _onBack;static void initialize() {
_channel.setMethodCallHandler((call) async {
if (call.method == 'onBackPress') {
_onBack?.call();
}
});
}static void setCallback(VoidCallback onBack) {
_onBack = onBack;
}
}
关键代码位置
-
app/ohos/entry/src/main/ets/entryability/EntryAbility.ets:44-53--- 沉浸式窗口配置 -
app/ohos/entry/src/main/ets/pages/Index.ets--- FlutterPage 承载与返回键拦截 -
app/lib/main.dart--- Flutter 侧窗口适配
鸿蒙侧实现
鸿蒙侧的窗口管理涉及三个层次:
-
Ability 层 (
EntryAbility.ets):在onWindowStageCreate中配置窗口属性 -
页面层 (
Index.ets):onBackPress拦截返回键,viewId标识页面 -
系统层 :
window.WindowStage和window.Window提供窗口操作 API
窗口属性配置的时序:
EntryAbility.onCreate
↓
EntryAbility.onWindowStageCreate
↓ 获取 mainWindow
↓ setWindowLayoutFullScreen(true)
↓ setWindowSystemBarEnable([])
↓
Index.build
↓ FlutterPage({ viewId: this.viewId })
↓
Flutter 引擎渲染
Flutter 侧实现
Flutter 侧的适配策略:
-
检测沉浸式状态 :通过
MediaQuery.padding.top == 0判断 -
手动添加安全间距 :使用
MediaQuery.viewPadding.top获取真实状态栏高度 -
监听返回键事件 :通过 MethodChannel 接收 ArkTS 侧的
EVENT_BACK_PRESS
常见坑
-
坑 1:
setWindowLayoutFullScreen(true)后SafeArea不生效 。SafeArea依赖MediaQuery.padding,沉浸式配置后padding.top变为 0,SafeArea不会添加间距。需要手动处理。 -
坑 2:
onBackPress返回true后 Flutter 页面无响应 。eventHub.emit只在 ArkTS 侧传播,Flutter 侧需要额外的 MethodChannel 监听。 -
坑 3:
setWindowSystemBarEnable([])在某些设备上不生效。部分鸿蒙设备的系统栏行为不同,需要做兼容性测试。 -
坑 4:
viewId在多窗口场景下的冲突 。如果应用支持分屏,多个Index实例的viewId可能冲突。需要确保viewId唯一。 -
坑 5:沉浸式配置后 Flutter 页面的点击区域偏移。如果 Flutter 页面的点击区域和系统栏重叠,可能触发系统栏的点击事件。需要在 Flutter 侧避免在顶部区域放置可点击元素。
可复用模板
// Flutter 侧 - 沉浸式窗口适配模板
class WindowAdaptiveLayout extends StatelessWidget {
final Widget child;
final bool includeTopPadding;
const WindowAdaptiveLayout({
required this.child,
this.includeTopPadding = true,
});
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final isImmersive = mediaQuery.padding.top == 0;
return Column(
children: [
if (isImmersive && includeTopPadding)
SizedBox(height: mediaQuery.viewPadding.top),
Expanded(child: child),
if (isImmersive)
SizedBox(height: mediaQuery.viewPadding.bottom),
],
);
}
}
// 鸿蒙侧 - 返回键拦截模板
Entry(storage)
@Component
struct MainPage {
private context = getContext(this) as common.UIAbilityContext
@LocalStorageLink('viewId') viewId: string = "";
onBackPress(): boolean {
this.context.eventHub.emit('BACK_PRESS')
return true
}
build() {
Column() {
FlutterPage({ viewId: this.viewId })
}
}
}
本篇总结
鸿蒙窗口管理在 Flutter 项目中的落地,核心是三个环节的协同:ArkTS 侧配置窗口属性(沉浸式、系统栏)→ 页面层拦截系统事件(返回键)→ Flutter 侧适配布局变化(安全间距、事件监听)。理解这些环节的关键在于搞清楚"谁控制窗口、谁拦截事件、谁适配布局"。
