ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Flutter × HarmonyOS 6.0:FAB组件与双端通信完整实战

Flutter × HarmonyOS 6.0:FAB组件与双端通信完整实战 1. 跨端这件事为什么我盯上了 HarmonyOS 6.0先说结论如果你手上有一个打磨过的 Flutter 组件库那么把它迁移到鸿蒙生态成本绝对比你重新用 ArkUI 写一套要低得多。我在做这个 Flutter Harmony Studio 示例时最直观的感受是——HarmonyOS 对 Flutter 的兼容已经不再是能跑通 Hello World的演示级别而是到了可以认真讨论业务组件落地的阶段。这个项目本身不算复杂核心就两个点在 Flutter 页面里实现一个浮动操作按钮FAB以及点击 FAB 后弹出创建选项的交互流程。听起来像是所有 Flutter 新手都做过的东西但放到 HarmonyOS 6.0 的语境下它牵扯到一个关键问题Flutter 的 UI 层怎么和鸿蒙原生的能力打交道FAB 的点击事件、路由跳转、甚至系统分享面板到底是用 Flutter 自己管还是要桥接到鸿蒙侧我把整个项目定位成跨端基础能力验证Flutter 负责界面与交互HarmonyOS 负责承载与系统能力对接。这也是现阶段最实际的混合开发模式。适合正在评估 Flutter 进鸿蒙的团队也适合想提前占坑鸿蒙生态的独立开发者。2. 从零搭一个 Flutter × HarmonyOS 工程2.1 环境准备别急着写代码先把链路跑通HarmonyOS 6.0 对应的 API 版本和开发工具有些特殊要求建议直接按官方文档来但有几个坑我提前踩过了先帮你们排掉。第一DevEco Studio 的版本号要和 HarmonyOS SDK 版本匹配。比如我这个项目用的是 DevEco Studio 5.x 配合 HarmonyOS 6.0 SDK如果版本不匹配Flutter 插件很可能直接编译不过报错信息还特别晦涩经常是Unable to load class这类跟实际原因差了十万八千里的提示。第二OpenHarmony 的 Flutter SDK 需要单独配置。市面上主流的开源方案是 OpenHarmony/flutter_flutter 这个分支它维护了针对鸿蒙的引擎代码。配置方法是在.bash_profile里把FLUTTER_STORAGE_BASE_URL指向国内镜像然后把flutter命令指向这个分支的 SDK 目录。第三务必先跑一遍官方 demo 再动自己的业务代码。我见过太多人一上来就导入自己的老项目结果报错一堆根本分不清是环境问题还是代码问题。官方 demo 能跑通说明你的环境链路没问题后续排查范围就能锁定在自己写的代码上。注意HarmonyOS 的 Flutter 分支版本和上游 Flutter 版本存在一个对应关系并不是最新的 Flutter 就一定支持鸿蒙。我目前用的是 Flutter 3.x 的分支不要盲目追新稳定压倒一切。2.2 工程结构Flutter 模块怎么嵌进鸿蒙壳子Flutter 在鸿蒙上的工程形态和 Android 端类似外层是一个 HarmonyOS 工程Entry 模块内层是一个 Flutter 模块。但有个细节需要注意HarmonyOS 6.0 里 Flutter 模块是以源码方式集成还是以 HAR/AAR 方式集成对工程结构影响很大。我推荐的做法是源码集成。原因很简单鸿蒙的 Flutter 引擎还在快速迭代源码方式方便随时拉取最新修复而且调试时可以直接跟到引擎层代码排查问题效率高很多。具体操作步骤如下在 DevEco Studio 创建 HarmonyOS 工程选 Empty Ability 模板在工程根目录下执行flutter create --templateapp创建 Flutter 模块修改 Flutter 模块的pubspec.yaml添加你需要的依赖在 HarmonyOS 的Entry模块里配置对 Flutter 模块的依赖。这里有两种方式一种是在build-profile.json5里直接关联源码目录另一种是把 Flutter 模块打包成 HARHarmonyOS Archive再放到ohpm依赖里。我实际用的是第一种开发调试体验更流畅。打包 HAR 适合做交付或团队间共享但每次改 Flutter 代码都要重新打包非常痛苦。工程创建完之后还要确认一个关键文件Entry模块的module.json5里是否声明了abilities和后台运行权限。Flutter 页面需要在一个单独的Ability里承载通常继承FlutterAbility这个类由 OpenHarmony 的 Flutter 适配层提供。3. 浮动操作按钮的完整实现链路3.1 FAB 不算难度难在它和鸿蒙的窗口关系先写代码。Flutter 侧定义一个 FloatingActionButton 组件这是最基础的操作Scaffold( floatingActionButton: FloatingActionButton( onPressed: _showCreateOptions, tooltip: 创建, child: const Icon(Icons.add), ), )但这只是浮于表面的实现。在 HarmonyOS 上真正需要关心的是窗口焦点和显示层级的绑定。鸿蒙的多窗口机制下FlutterAbility 是一个独立的窗口FAB 虽然由 Flutter 渲染但它的显示还是要遵循鸿蒙窗口的生命周期规则。我踩到过一个问题Flutter 页面在鸿蒙上切换到后台再切回来FAB 偶发性地丢焦点点击没有反应。排查了半天发现是鸿蒙的onSaveState回调把 Flutter 窗口的状态标记成了不活跃而 Flutter 引擎的触摸事件分发没有及时恢复。解决方案是监听鸿蒙侧的生命周期事件在onForeground时主动刷新一下 Flutter 引擎的帧回调。这在 Android 上可能不需要但在鸿蒙上最好显式处理。class _FlutterAbility extends FlutterAbility { Override public void onForeground(Intent intent) { super.onForeground(intent); // 通知 Flutter 侧刷新 UI getFlutterEngine().getMessenger().send(refresh, onForeground); } }3.2 给 FAB 加上创建选项的三种交互形态点击 FAB 之后有两种主流交互一种是弹出底部菜单另一种是展开多个子按钮Speed Dial 效果。项目里我两种都实现了因为不同场景需求不一样。底部菜单用 Flutter 的showModalBottomSheet实现void _showCreateOptions() { showModalBottomSheet( context: context, builder: (context) SafeArea( child: Wrap( children: [ ListTile( leading: const Icon(Icons.photo_outlined), title: const Text(从相册选择), onTap: () Navigator.pop(context, photo), ), ListTile( leading: const Icon(Icons.camera_alt_outlined), title: const Text(拍摄照片), onTap: () Navigator.pop(context, camera), ), ListTile( leading: const Icon(Icons.event_note_outlined), title: const Text(新建笔记), onTap: () Navigator.pop(context, note), ), ], ), ), ); }Speed Dial 效果则需要自己管理动画状态我用了 Flutter 自带的AnimatedContainer和Transform没有引入额外库AnimatedRotation( turns: _isExpanded ? 0.125 : 0, duration: const Duration(milliseconds: 200), child: Icon(_isExpanded ? Icons.close : Icons.add), )这里有一个体验细节子按钮弹出时应该从 FAB 的位置以扇形或直线轨迹展开并且要有明暗遮罩。我用了Stack加ModalBarrier实现Stack( children: [ if (_isExpanded) ModalBarrier( dismissible: true, color: Colors.black54, onDismiss: () setState(() _isExpanded false), ), // FAB 和子按钮 ], )为什么不用现成的flutter_speed_dial包因为在鸿蒙上第三方包的原生依赖可能存在兼容问题而且只为一个交互效果引包有点不值。自己实现代码量不大还能完全控制动画曲线和点击边界。4. Flutter 与 HarmonyOS 双端通信的三种姿势4.1 从 PlatformView 说起为什么 FAB 点击后要调鸿蒙原生FAB 点击后的实际业务动作比如保存图片到相册、调用系统分享、发送通知等都属于鸿蒙原生能力。Flutter 只是一个 UI 框架要调鸿蒙的这些能力就得走双端通信。目前在 Flutter × HarmonyOS 通路下主流有三种通信方式通信方式适用场景实时性实现成本MethodChannel单向调用Flutter 调鸿蒙/鸿蒙调 Flutter高低EventChannel事件流推送鸿蒙主动给 Flutter 发消息中中PlatformView嵌入原生视图Flutter 包鸿蒙页面低高项目里我重点使用了EventChannel来做系统事件的实时通知比如文件保存成功、图片选择完成等。为什么不用 MethodChannel 单向调用因为单向调用是一次性问答如果鸿蒙侧的服务是长时间运行的比如监听文件变化用 EventChannel 更合适。4.2 MethodChannel 在鸿蒙侧的写法先看 Flutter 侧怎么发消息static const platform MethodChannel(harmony_studio/fab); Futurevoid _pickImageFromGallery() async { try { final result await platform.invokeMethod(pickImage); // result 里返回图片路径 } on PlatformException catch (e) { // 处理异常 } }鸿蒙侧用MethodChannel接收消息。注意这里不能用 Java/Kotlin 的写法HarmonyOS 的 Flutter 适配层用的是 ArkTS 风格let methodChannel new MethodChannel(getFlutterEngine(), harmony_studio/fab); methodChannel.setMethodCallHandler((call) { let methodName: string call.method; if (methodName pickImage) { let picker new photoAccessHelper.PhotoViewPicker(); let result await picker.select(); // 返回给 Flutter 侧 return Promise.resolve(result); } });关键点ArkTS 侧的方法调用是Promise风格返回值也要用Promise.resolve包一层否则 Flutter 侧会一直等不到结果。4.3 EventChannel 让鸿蒙主动喊话 Flutter再来说 EventChannel。我在项目里用它来监听 FAB 创建操作之后的资源变化事件。Flutter 侧这样订阅const eventChannel EventChannel(harmony_studio/events); override void initState() { super.initState(); eventChannel.receiveBroadcastStream().listen( (event) _handleSystemEvent(event), onError: (error) debugPrint(Event error: $error), ); }鸿蒙侧开一个线程或者用定时器检测到系统状态变化就往 Flutter 推数据let eventChannel new EventChannel(getFlutterEngine(), harmony_studio/events); let eventSink eventChannel.createEventSink(); // 在某个时机 eventSink.success({ type: imageSaved, path: /data/xxx.jpg });EventChannel 使用中有个容易忽略的坑事件流不关闭的情况下Flutter 页面销毁会导致内存泄漏。所以在 Flutter 侧dispose时一定要调用cancel或者让鸿蒙侧销毁EventSink。我在项目里是让鸿蒙侧感知页面销毁override void dispose() { platform.invokeMethod(pageClosed); super.dispose(); }5. 创建选项背后的生命周期与状态管理细节5.1 FAB 点击后跳转新页面Navigator 状态会不会丢很多 Flutter 开发者关心在鸿蒙上跳转页面后之前的 FAB 状态还在不在按照 Flutter 的标准机制Navigator.push之后原页面的状态会被保留除非你显式dispose。但鸿蒙的窗口机制会接管页面栈如果 Flutter 页面嵌入在鸿蒙的 Ability 里而新页面是鸿蒙页面那么 Flutter 的 Navigator 栈和鸿蒙的 Ability 栈是两个独立的体系。实测结论是在纯 Flutter 页面之间跳转完全没问题但从 Flutter 跳到鸿蒙原生页面再返回Flutter 的 FAB 状态可能丢失原因是鸿蒙 Ability 重建了 FlutterEngine。解决办法是配置鸿蒙侧的launchMode为singleTask让 FlutterAbility 实例复用。在module.json5里加一行{ module: { abilities: [ { name: FlutterEntryAbility, launchType: singleton } ] } }singleton对应 Android 里的singleTask保证 Flutter 引擎不被反复创建。5.2 状态管理选型Provider 还是 getX还是别的因为项目里有 FAB 展开状态、选择结果、事件流数据变化我用了Provider做基础状态管理。选型逻辑很简单项目规模不大不需要 Bloc 那么重的架构逻辑也不算复杂getX 的优势发挥不出来。但有个场景值得注意跨页面共享 FAB 的创建选项结果。比如你从 FAB 点了新建笔记跳转到笔记页之后笔记页要能知道用户选了哪种类型。我用了一个全局的Provider对象来保存选择结果class CreateOptionsModel extends ChangeNotifier { String? _selectedOption; String? get selectedOption _selectedOption; void setSelected(String option) { _selectedOption option; notifyListeners(); } }其实还有一个隐藏的坑Provider 的notifyListeners()触发时机。如果 FAB 的动画还在执行中就重建组件树会出现闪烁。我的做法是先更新数据等动画结束后再刷新 UI具体是在动画回调里统一处理避免中途重建。6. 我踩过的坑HarmonyOS 上 FAB 的六大疑难杂症6.1 编译阶段的问题最常见的错误是You are applying Flutters main Gradle plugin imperatively using the apply script。这个报错不是鸿蒙专属但在 Flutter × HarmonyOS 工程里更容易遇到因为你可能同时参照了 Android 和鸿蒙两套文档。解决思路新版 Flutter 建议在settings.gradle里声明插件而不是用apply方式。找到 Flutter 模块的build.gradle把apply from: $flutterRoot/packages/flutter_tools/gradle/flutter.gradle改成plugins { id dev.flutter.flutter-plugin-loader version 1.0.0 }如果版本对不上就锁定 Flutter 分支的版本号不要混用。另一个常见问题是镜像源。执行flutter pub get时经常卡死或者下载引擎产物超时。在国内环境下务必配置flutter config --flutter-root /path/to/flutter_flutter export PUB_HOSTED_URLhttps://pub.flutter-io.cn export FLUTTER_STORAGE_BASE_URLhttps://storage.flutter-io.cn6.2 运行阶段的疑难杂症问题一FAB 点击无响应但其他按钮正常。排查方向先查 Flutter 侧的onPressed是不是被置空再查鸿蒙侧是否消费了触摸事件。我遇到的是鸿蒙侧Stack布局中有一个透明 View 挡住了 FAB 区域但界面完全看不出来。用 DevEco Studio 的 Inspector 查看层级把遮挡 View 的hitTestBehavior改成Transparent即可。问题二FAB 显示在页面之外。Flutter 在鸿蒙上的页面高度和宽度受到Scaffold约束但有些场景如键盘弹出时 adjust resize会造成 FAB 偏移。解决方法是给Scaffold设置resizeToAvoidBottomInset: false并且监听键盘高度动态调整 FAB 位置。问题三EventChannel 收不到事件。优先检查鸿蒙侧的EventChannel是否在FlutterAbility的onCreate里创建。如果写在页面的onPageShow里可能因为重复注册导致事件路由混乱。正确做法是在 Ability 生命周期里统一管理 Channel 的创建和销毁。问题四FAB 点击后用Navigator.push跳转新页面页面切换动画卡顿。这是因为鸿蒙设备上 Flutter 的 Impeller 渲染引擎还在优化阶段。可以临时切换回 Skia 引擎在AndroidManifest.xml或鸿蒙的配置里加上meta-data android:nameio.flutter.embedding.android.EnableImpeller android:valuefalse /等 Impeller 在鸿蒙上成熟后再切回来。实测下来Skia 在鸿蒙 6.0 上的渲染稳定性更高特别是页面切换和圆角阴影这类复杂绘制场景。问题五FAB 图标不显示是一个空白圆。多半是字体或者 Icon 渲染问题。在鸿蒙分支的 Flutter 上部分 Material Icons 的字体预置不完整。解决方式是手动加载字体资源Icon( Icons.add, fontFamily: MaterialIcons, )如果还不行就换成自定义的 PNG/SVG 图标资源别在图标字体上浪费时间。问题六打包安装后 FAB 正常但一锁屏再解锁就崩溃。这是鸿蒙的进程回收机制把 Flutter 引擎释放了但 Flutter 的 Dart 层还保留了对象引用。解决方式是重写FlutterActivity或FlutterAbility的onDetachedFromEngine回调在引擎被回收前主动释放 Flutter 资源onDetachedFromEngine() { // 释放 Dart 侧的全局订阅和 Channel }另一种更彻底的方案是禁用鸿蒙对 FlutterAbility 的进程回收提名在module.json5里加{ metadata: [ { name: not_show_in_recently, value: false } ] }个人建议不要禁用而是做好资源释放因为强制保活会影响系统整体流畅度用户也不会买账。6.3 性能优化与体验调优流动操作按钮本身性能消耗不大真正影响体验的是它后面跟随的创建选项动画。我在这个项目里做了三个层面的优化第一动画帧率优化。FAB 展开动画涉及多个子按钮的位移、透明度变化、旋转如果在一个setState里同步触发鸿蒙的低端机型会掉帧。我改成每个动画单独用AnimationController并在动画触发前检查设备刷新率final vsync Window.instance.platformDispatcher.implicitView; final fps vsync?.display.refreshRate ?? 60;第二子视图复用。创建选项列表的每个按钮都是相似的布局结构我用ListView.builder而不是直接children: [...]减少 widget 重建压力。第三延迟加载。FAB 点击到弹出菜单之间插入一个极短的延迟约 50ms让用户感受到反馈先行心理触感更真实。但这个延迟不能太长否则会显得卡顿。7. 这个项目还能怎么扩展写完这个示例之后我最大的体会是Flutter 在 HarmonyOS 上的重心已经从能不能跑变成了怎么高效地业务落地。FAB 只是一个小小的组件但它背后的 Channel 通信、生命周期对接、状态管理、原生能力调用是任何跨端应用都绕不开的基础能力。接下来值得尝试的扩展方向有三个第一个是动态主题和深色模式适配。鸿蒙 6.0 有自己的深色模式策略Flutter 侧可以通过MediaQuery监听系统主题变化但需要和鸿蒙原生的配置结合确保 FAB 的颜色和阴影在两种模式下都有正确的对比度。第二个是FAB 与系统手势的联动。鸿蒙 6.0 支持侧边返回手势和手势导航Flutter 页面在手势区域渲染 FAB 时可能触发冲突。可以尝试监听手势区域的触控事件动态调整 FAB 的显示位置或大小。第三个是多端能力复用。把 FAB 的操作事件做成平台无关的逻辑层在适合用 ArkUI 的页面和适合用 Flutter 的页面之间自由切换。但这需要一个相对完整的跨端抽象层适合团队规模上来之后再考虑。根据我个人的实操经验做跨端开发最重要的是守住一条底线UI 层可以妥协逻辑层和数据层必须有清晰的分层。FAB 在鸿蒙上的实现功能本身不复杂但它逼着你把通信、状态、生命周期这些基础工程想清楚。想清楚一次后面做任何跨端页面都会顺畅很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进