鸿蒙原生 + Flutter 混合开发:页面生命周期怎么和原生关联

鸿蒙原生 + Flutter 混合开发:页面生命周期怎么和原生关联

这篇只讲一件事:Flutter 页面和鸿蒙原生页面在同一个 App 里,生命周期怎么接上。

接错了不会报错,只会出现三种现象:进页面不加载、切回来不刷新、退出后回调还在跑。

代码基线:ArkTS 原生 + Flutter 相册模块,Flutter 只占宿主的一个 Tab(嵌入式模式)。


一、先看全景:一次进页面,数据是怎么走的

【App 启动】一次性动作
UIAbility.configureFlutterEngine
   └─► Bridge.attach(engine)              ← 两条通道在这里挂好,只挂一次

UIAbility.onWindowStageCreate
   ├─► 创建 FlutterView
   ├─► 把 viewId 交给 Bridge
   └─► loadContent 回调里调用 flutterView.onWindowCreated()   ← 少这行会白屏

【切到相册 Tab】每次都发生
AlbumFlutterPage 可见(onVisibleAreaChange)
   └─► emitAlbumVisible()
         └─► 事件 'visible' ──► Flutter 侧 refreshContent()

【相机连上 / 断开】
@Monitor(netBean.isConnectCamera)
   └─► emitConnectionChanged()
         └─► 事件 'connection' ──► Flutter 侧刷新空状态与列表

【Flutter 页面自己的生命周期】
AlbumPage.initState ──► 通道 'initialize'
AlbumPage.dispose   ──► 通道 'dispose'

【App 退出】一次性动作
UIAbility.onDestroy
   └─► Bridge.detach()                    ← 解绑监听、停定时器、释放播放器

记三句话就够了:

  1. 通道挂在引擎上,不挂在页面上。
  2. 刷新靠原生推事件,不靠 Flutter 的生命周期回调。
  3. 切 Tab 不会销毁 Flutter,initState / dispose 都只跑一次。

二、两个基础概念,先垫一下

2.1 通道是什么

Flutter 页面和原生页面在同一个 App 里,但跑在两套互不相通的运行环境里(一边 Dart VM,一边 ArkTS 运行时)。
两边内存不共享,不能直接调对方的函数,也不能直接传对象。

通道(Platform Channel)就是两边约好的一条数据管道:

关于通道 说明
怎么找到对方 靠一个字符串名字,比如 com.example.hybrid/native_calculator,相当于频道号
要不要握手 不需要。名字对上就能用,第一次调用就自动接上
名字写错会怎样 各说各话,报 MissingPluginException
能传什么 只能是"值":键值对、数组、字符串、数字,不能传对象引用

2.2 三层是什么:三种寿命不同的容器

三层不是架构分层,而是寿命不同。判断标准只有一条:它什么时候生、什么时候死。

层 具体是什么 什么时候生 什么时候死 一生几次
引擎层 FlutterEngine(跑 Dart 的运行时)+ 宿主 UIAbility App 启动 App 退出 1 次
视图层 FlutterView(XComponent)+ FlutterPage 承载组件 宿主窗口创建 宿主窗口销毁 通常 1 次
页面层 宿主 TabContent + Flutter 的 AlbumPage Widget 切到相册 Tab 切走只是藏起来 反复显示隐藏,但不销毁

打个比方:引擎层是主机,视图层是显示器,页面层是屏幕上正在播的节目。
切 Tab 相当于切台——节目不会因为切台就"结束"。

2.3 所以东西该挂哪一层

要挂的东西 挂哪层 挂错的后果
通道、事件监听、定时器 引擎层(configureFlutterEngine) 挂页面层 → 页面重建时重复注册,事件丢一半
尺寸 / 键盘 / 全屏等视图配置 视图层(FlutterPage) 挂页面层 → 视图还没建好就配置,白屏
页面状态(选中项、编辑模式) 页面层(Flutter 的 Controller) 挂引擎层 → 所有页面共用一份状态,互相污染

这一节就是后面所有"为什么必须推事件"的答案:
切换发生在页面层,而页面层切走时并不销毁,所以 Flutter 那边收不到任何"我又回来了"的提醒。


三、鸿蒙和 Flutter 的生命周期对照

你想做的事 ArkUI 原生写法 Flutter 写法 本项目怎么接
首次显示时加载 aboutToAppear() initState() Flutter 调通道 initialize
再次可见时刷新 onPageShow()(整页)
onVisibleAreaChange()(嵌入)
⚠️ 没有对应回调 原生推 visible 事件
销毁时清理 aboutToDisappear() dispose() Flutter 调通道 dispose;切 Tab 不触发
外部状态变化 @Monitor ChangeNotifier 原生推事件,如 connection

表格第二行是这套方案的核心:
Flutter 里没有"页面又可见了"这个回调——因为视图挂在 TabContent 里一直活着,切 Tab 不重建、不 dispose。
这件事只能由原生主动推过去。


四、关键代码,按全景图的顺序看

4.1 通道挂在引擎上(只挂一次)

// EntryAbility.ets —— 唯一的挂载点
configureFlutterEngine(engine: FlutterEngine): void {
  super.configureFlutterEngine(engine);
  AlbumFlutterBridge.getInstance().attach(engine);   // ✅ 挂在引擎上
}

onDestroy(): void {
  AlbumFlutterBridge.getInstance().detach();         // ✅ 统一释放
  super.onDestroy();
}

❌ 不要写在页面的 aboutToAppear 里——页面会重建,通道会被重复注册。

4.2 viewId 从哪来(三步,顺序不能换)

// EntryAbility.onWindowStageCreate(节选)
const flutterView = delegate?.createView(this.context);                    // ① 建视图
AlbumFlutterBridge.getInstance().setFlutterViewId(flutterView.getId());    // ② 交 viewId

windowStage.loadContent('pages/Index', storage, (err) => {
  flutterView?.onWindowCreated();   // ③ 内容加载完才真正渲染,少这行白屏
});

4.3 组件层:FlutterPage 和 FlutterEntry 别选错

FlutterPage(嵌入式,本项目用) FlutterEntry(整页)
适用场景 Flutter 只占宿主一个 Tab 整个页面都是 Flutter
aboutToAppear / aboutToDisappear ✅ 有 ✅ 有
onPageShow / onPageHide ❌ 没有 ✅ 有
onBackPress(返回键) ❌ 没有 ✅ 有,内部会转发给 Flutter 路由
// AlbumFlutterPage.ets:嵌入式承载组件
build() {
  Column() {
    FlutterPage({ viewId: this.viewId })     // 把 Flutter 视图挂进来
      .width('100%').height('100%')
  }
  .onVisibleAreaChange([0.0, 1.0], (isExpanding: boolean, ratio: number) => {
    // 嵌入式没有 onPageShow,就用"可见面积变化"当显示信号
    if (isExpanding && ratio > 0) {
      AlbumFlutterBridge.getInstance().emitAlbumVisible();
    }
  })
}

顺带说明:SDK 里 FlutterPage.aboutToDisappear() 只做了"移除首帧监听",
既没销毁引擎,也没释放视图。这就是"切 Tab 不触发 Flutter dispose"的根源。

4.4 页面可见:一条事件桥过去

// 原生:推
emitAlbumVisible(): void {
  this.emitEvent('visible', { 'visible': true });
}
// Flutter:收
case 'visible':
  unawaited(refreshContent(triggerNativeLoad: true));   // 重新拉一次数据
  unawaited(_refreshTabsFromNative());                  // 顺带重算一级 Tab

4.5 状态变化也一样靠推

// 原生:盯住全局状态,一变就推
@Monitor('netBean.isConnectCamera')
onConnectionChange(): void {
  AlbumFlutterBridge.getInstance().emitConnectionChanged(this.netBean.isConnectCamera);
}
// Flutter:收到后更新界面
case 'connection':
  _cameraConnected = event.data['connected'] == true;
  if (!_cameraConnected) { _isEditMode = false; _selectedIds.clear(); }  // 断开就清干净
  _safeNotify();

4.6 Flutter 侧自己的生命周期(对应两次通道调用)

// AlbumPage
void initState() {
  super.initState();
  _controller.initialize();        // → 通道 'initialize'
}

void dispose() {
  _subscription?.cancel();         // ① 先断开事件订阅
  unawaited(_service.dispose());   // ② 再让原生释放通道(→ 通道 'dispose')
  super.dispose();
}

⚠️ 顺序别反:先断订阅再通知原生,否则原生释放时推的最后一两条事件会打到已作废的订阅上。


五、Flutter 子页面(预览页、下载页)和宿主的关系

子页面是 Flutter 用自己的 Navigator 推的,原生完全不知道。需要协调的只有三件事:

事情 谁负责 怎么协调
页面跳转 Flutter 的 Navigator 原生无感知
宿主底栏遮挡 宿主 MainPage Flutter 调 setHostTabBarVisible(true),原生转成全局事件
系统返回键 宿主 Index.onBackPress() ⚠️ 目前没有转发给 Flutter,子页靠页面内的返回按钮

两个写成坑的经验:

  1. 跨层传布尔量,字段名和取值必须统一语义。
    早期 Bridge 传的是"底栏是否可见",宿主字段是"是否隐藏",直接赋值就反了——表现是编辑面板被底栏整条盖住。
  2. 返回键归谁管要提前定。
    SDK 的 FlutterView 其实留好了通路:监听 EVENT_BACK_PRESS → navigationChannel.popRoute()。
    整页模式(FlutterEntry)会自动转发;嵌入式(FlutterPage)需要宿主自己转发。
    本工程的宿主页只做了"双击退出",没有转发,所以系统返回键目前退不出 Flutter 子页。

这一段是按 SDK 代码读出来的结论,还没在本工程真机验证过,接入时请以实测为准。


六、踩过的坑

坑 现象 正确做法
通道 attach 写在页面 aboutToAppear 通道重复注册、事件丢一半 挂在 configureFlutterEngine,加幂等保护
忘了 flutterView.onWindowCreated() Flutter 区域白屏 在 loadContent 回调里调用
以为切 Tab 会触发 Flutter dispose 定时器、订阅、播放器一直不释放 刷新用事件驱动,释放在 dispose / detach
把"页面可见"逻辑写进 onListen 一辈子只触发一次 onListen/onCancel 只管订阅,可见性走 visible 事件
跨层布尔量语义反了 "要求显示"变成"隐藏" 字段名与取值统一语义
系统返回键被宿主拦截 Flutter 子页返回不了 转发 EVENT_BACK_PRESS,或保留页面内返回按钮
在 Flutter 里用 print 打日志 真机日志一条都没有 走通道转发到原生日志(另见《Flutter 端日志打印》)

七、接入自检清单

posted @ 2026-09-15 17:21  带头大哥d小弟  阅读(3)  评论(0)    收藏  举报