这些年做跨端开发,绕不开一个判断:Flutter 这套渲染管线和 OpenHarmony 的分布式硬件能力,到底能不能真正组合起来?我自己的答案是从一个自定义组件开始的。这篇文章会用 Flutter for OpenHarmony 做基础,讲清楚自定义组件从设计、桥接到上板的完整链路,适合正在评估鸿蒙生态、想把手头 Flutter 业务快速平移的同学参考。无论你是刚接触 Flutter 入门教程的新手,还是已经在 Android 上写过自定义组件的熟手,这都是一份可以直接拿去用的实操笔记。
写自定义组件最忌讳的就是一上来就堆代码。组件能不能稳定跑在 OpenHarmony 设备上,关键要看你的设计是否绕开了平台差异最大的几个雷区:生命周期、渲染通道、事件时序。这三个点我会逐个拆开讲,并且结合一个摄像头预览组件做完整演示。过程中还会聊到 Flutter AAR 的接入方式、PlatformView 的黑屏问题、Dart VM 初始化崩溃,以及 XTS 认证里组件相关的那几条红线。
1. 整体思路:为什么用 Flutter 适配 OpenHarmony,还要造自己的组件
1.1 先说结论:Flutter 的生态值得搬过来
Flutter 社区里现成的轮子太多了,下拉刷新、图表、地图、视频播放器,随便一个都是经过千万级应用验证的。而 OpenHarmony 的原生生态还处于爬坡期,三方库数量、成熟度、维护活跃度都很难和 Flutter 社区比。与其在 OpenHarmony 侧重新造一个 UI 组件库,不如直接把 Flutter 的渲染和交互层搬过来,用自定义组件的方式补上平台能力。
这里要注意,我说的是"搬过来",不是"跑起来就完事"。Flutter 的 dart:ui 层和 OpenHarmony 的 ArkUI 渲染层是完全两套体系。Flutter 自己画出来的控件,本质上是 Skia/Impeller 渲染出来的位图,它跟 OpenHarmony 原生控件之间没有自动的映射关系。想要让 Flutter 页面里出现一个真正的原生控件,比如摄像头预览 Surface,或者一个系统级的地图视图,就必须通过自定义组件把原生视图嵌进 Flutter 的视图树里。
这也是为什么网上很多 Flutter 教程讲到 PlatformView 就直接跳过了——因为这里涉及两套生命周期、两个线程模型、两套事件分发机制。拿 Flutter 和其他前端框架比,React Native 有原生组件映射层,小程序有 WebView 容器,而 Flutter 靠的就是 PlatformView 加自定义组件这套组合拳。理解这一点,你才算真正理解 Flutter 在 OpenHarmony 上做定制化开发的底层逻辑。
1.2 自定义组件要分三个层次来设计
很多人把自定义组件理解成"画一个好看的 UI",这是不对的。在 Flutter for OpenHarmony 的语境下,自定义组件至少分三个层次,选错层次会让你后期付出惨痛代价。
第一层是纯 Dart 绘制层。组件完全用 Flutter 的 Canvas、Widget 组合来实现,不碰任何原生能力。比如下拉刷新的指示器、自定义的日历控件、流程图的拖拽节点,这些都属于纯 UI 范畴,直接写 Dart 就行,不需要原生参与。
第二层是原生视图包裹层。组件内部嵌入一个 OpenHarmony 原生视图,Flutter 只负责外层的布局和触发逻辑。典型例子就是摄像头预览、视频播放器、地图。实现方案上用 PlatformView,原生侧创建一个 XComponent 或者 SurfaceProvider,Flutter 侧用一个 UiKitView 类似物来承载。这层的难点是生命周期同步和触摸事件穿透。
第三层是纹理共享层。原生侧把图像数据直接写到共享纹理里,Flutter 侧用 Texture 控件消费。适合实时性要求极高的场景,比如视频流处理、AR 特效。这层性能最好,但开发成本也最高,需要自己管缓冲区和同步锁。
我给你的建议是:能做第一层就别碰第二层,能做第二层就别碰第三层。只有摄像头、播放器这类非原生不可的东西,才值得你去碰 PlatformView。还有一个更朴素的判断标准:如果你的组件在 Android 上需要用 Texture 或者 SurfaceView 才能不卡,那搬到 OpenHarmony 上就别指望纯 Dart 能搞定。
2. 核心细节:组件骨架、渲染方案和通信机制
2.1 组件骨架怎么搭,生命周期怎么对齐
自定义组件的第一行代码不是 UI,而是生命周期。OpenHarmony 的应用模型基于 Ability + UIAbility,页面有 onCreate、onShow、onBackground、onDestroy 等状态;Flutter 组件有 initState、didChangeDependencies、build、dispose。两套生命周期必须对齐,否则会出现"页面关掉了,原生摄像头还在跑"这种典型事故。
我习惯的做法是在自定义组件里定义一个 PlatformLifecycleListener,把 OpenHarmony 侧的原生视图生命周期回调转成 Flutter 熟悉的生命周期事件。
class CameraPreview extends StatefulWidget { const CameraPreview({Key? key, this.onCreated, this.onDisposed}) : super(key: key); final ValueChanged<int>? onCreated; final VoidCallback? onDisposed; @override State<CameraPreview> createState() => _CameraPreviewState(); } class _CameraPreviewState extends State<CameraPreview> { int _nativeViewId = -1; @override void initState() { super.initState(); // 注册原生视图 _nativeViewId = PlatformBridge.instance.createNativeView('camera_preview'); widget.onCreated?.call(_nativeViewId); } @override Widget build(BuildContext context) { return PlatformNativeView( viewId: _nativeViewId, onViewCreated: (id) { // 视图挂载完成的回调 }, ); } @override void dispose() { PlatformBridge.instance.disposeNativeView(_nativeViewId); widget.onDisposed?.call(); super.dispose(); } }代码本身不复杂,但有几个细节必须提醒。第一,initState 里创建原生视图后,不要立刻调用原生方法,要等 onViewCreated 回调,因为此时原生视图可能还没完成 attach。第二,dispose 里销毁原生视图的顺序必须是先原生后 Flutter,反过来会出现悬空引用。第三,如果组件嵌在 PageView 里面,原生视图的 detach 和 reattach 逻辑一定要在原生侧实现,否则滑动页面回来就是黑屏。
还有一个经常被问到的点:OpenHarmony 的组件绑定原生事件,跟 Android 的 setOnClickListener 有什么区别?区别在于事件源不在 Flutter 侧,而在原生侧。原生视图内部产生了 onClick、onStatusChanged、onFrameAvailable,需要通过通道把事件主动推给 Dart。这里的核心是把监听器注册在原生侧,把回调暴露给 Flutter 侧,而不是反过来。
2.2 渲染路线:PlatformView、Texture 还是纯 Dart 绘制
把渲染路线单独拉出来说,是因为这是决定组件性能上限的关键。很多人在 Android 上写过自定义组件,习惯了用 Java/Kotlin 直接操作 View,但在 OpenHarmony 上,ArkUI 的组件树和 Flutter 的组件树是两棵独立的树。PlatformView 本质上是在 Flutter 的树里嵌入一个原生的"洞",这个洞的合成方式直接决定帧率和触摸响应。
OpenHarmony 上的 PlatformView 有几种实现路线,我按稳定性排序:
一是 Graphics 共享内存路线。原生侧用 OH_NativeWindow 申请 buffer,Flutter 侧通过 Texture 消费。好处是帧率稳定,坏处是触摸事件要自己做 hit test,而且坐标变换的坑很多。
二是 XComponent 包裹路线。原生侧用 XComponent 承载摄像头预览流,Flutter 侧通过 PlatformView 关联。好处是系统帮你处理了大部分合成逻辑,坏处是 XComponent 在部分设备上有层叠和裁剪问题。
三是纯 Flutter 绘制路线。通过在 Dart 侧用 CustomPainter 画控件,完全不碰原生。好处是性能和稳定性都能控制,坏处是拿不到真正的系统能力。
拿摄像头预览来说,最稳的还是 XComponent 或者 SurfaceProvider 路线。你可以在原生侧把 Surface 的 buffer 拿给相机硬件消费,实现零拷贝预览。这里要注意 OpenHarmony Camera 的能力:前置、后置、闪光灯、对焦这些功能都要通过 HDI 接口调用,而 HDI 是 OpenHarmony 的硬件驱动接口层,不同设备的实现差异非常大。写自定义组件时,一定要把设备能力枚举和能力查询放在前面,别默认所有设备都支持美颜、夜景这些扩展能力。
再聊一句 Impeller。Flutter 3.10 之后默认开启 Impeller 渲染引擎,它把 Skia 换成了自研的 GPU 渲染管线。好处是动画更丝滑,坏处是对 GPU 的要求更高。OpenHarmony 上部分低端设备跑 Impeller 会出现奇怪的闪烁和纹理撕裂,如果你的自定义组件恰好用了大量高斯模糊或离屏渲染,表现会更明显。遇到这种情况,可以先关掉 Impeller 对比验证:在 Flutter 启动参数里加--no-enable-impeller,如果问题消失,就说明是渲染引擎兼容性问题,不是你的组件代码有 bug。
2.3 通信链路:MethodChannel、EventChannel 与原生事件绑定
自定义组件绕不开通信。Flutter 和 OpenHarmony 原生侧之间的通信,核心是 MethodChannel(方法调用)和 EventChannel(事件流)。这两个东西写起来不难,难的是线程和时序。
MethodChannel 是双向的,Dart 可以调用原生,原生也可以调用 Dart,但底层都是异步消息。我在实际项目里见过大量 bug 都是因为"调用完 MethodChannel 立刻操作 UI",结果 UI 还没更新完原生回调就回来了。这里涉及一个基础问题:Dart 的 Future.then 回调到底进不进微任务队列?答案是进。Dart 的 Future 回调默认被调度到微任务队列,在 UI 线程的空闲阶段执行。也就是说,MethodChannel 调用返回后,后续逻辑依然在 Flutter 的 UI 线程上,这一点和 JavaScript 的 Promise 很接近。但注意,原生侧的回调到达 Flutter 引擎时,可能会被封装成一个平台消息,而平台消息的解析和分发有可能发生在 UI 线程之外。所以规范做法是:原生侧回调 Dart 时,强制在主线程执行,Dart 侧收到回调后再做 UI 更新,中间不要跨过引擎层做任何同步操作。
EventChannel 适合推送高频事件。比如摄像头预览帧率回调、传感器数据、播放器状态。它的实现要点是理解 StreamSubscription 的生命周期。如果组件销毁了而订阅没取消,原生侧会一直向 Flutter 侧推数据,轻则内存泄漏,重则 UI 卡顿。
class CameraEventChannel { static const EventChannel _channel = EventChannel('camera_preview/events'); static Stream<Map<Object?, Object?>> startListening() { return _channel.receiveBroadcastStream().cast<Map<Object?, Object?>>(); } } // 使用时,务必在 dispose 里 cancel StreamSubscription? _sub; void _bindEvents() { _sub = CameraEventChannel.startListening().listen((event) { if (event['type'] == 'onShutter') { setState(() => _captured = true); } }); } @override void dispose() { _sub?.cancel(); super.dispose(); }关于"自定义组件绑定原生事件"的完整链路,实际是四段:原生控件产生事件,原生侧通过 EventChannel 的 sink 投递事件,Dart 侧 Stream 接收,最后映射成 Flutter 组件的回调。很多人只做了前两段,漏了最后一段映射层,导致组件API设计成"你必须自己监听 Stream",这对使用者非常不友好。正确做法是内部封装好 Stream,对外只暴露onShutter、onError、onPreviewReady这类回调,让调用方像写普通 Flutter 组件一样用。
3. 实操过程:从零实现一个摄像头预览组件
3.1 工程接入:Flutter AAR 产物和 OpenHarmony 侧依赖
进入实操之前,先把工程结构说清楚。Flutter for OpenHarmony 的接入方式,和你在 Android Studio 里把 Flutter 模块打包成 AAR 再塞进 App 的思路很接近。Flutter 侧先打出一个flutter.aar这样的构建产物,OpenHarmony 工程再把它作为三方库依赖进去。整个流程大概是:Flutter 工程编写组件和 Dart 逻辑,打包出引擎产物;OpenHarmony 工程通过依赖管理和 Har 包接入;两边的 channel 通过组件名映射。
环境准备阶段,有几个坑最容易踩。OpenHarmony SDK 版本要和 Flutter 适配版本对应,不能随便拿最新版。装完 SDK 之后,命令行检查依赖要默认走镜像源,国内网络环境如果下载引擎产物失败,项目根本跑不起来。我遇到过不少"flutter 新建项目后跑不起来"的求助帖,最后排查下来八成是引擎产物没下全,或者环境变量没生效。
工程接好后,原生侧的第一步是声明组件的原生视图工厂。OpenHarmony 的 ArkUI 里,可以通过自定义 Component 的方式承载 Flutter 渲染出来的内容,也可以用 XComponent 承载原生 Surface。你要根据组件类型二选一。摄像头预览这种带硬件数据的,选 XComponent 最稳;普通地图这种 UI 密集的,选自定义 Component 更容易做触摸事件。
这里还要提一句 HDI。OpenHarmony 的摄像头能力是通过 HDI(Hardware Driver Interface)暴露给上层框架的。你写相机组件时,不要直接调用 HDI 接口去操作硬件,那样会绕开权限和生命周期管理。正确姿势是调用 OpenHarmony Camera 服务框架,由它再往下去驱动 HDI。这样做还能顺便把设备兼容性交给框架处理,否则你会在某台设备上遇到预览方向错误、闪光灯无效、对焦回调丢失这一连串问题。
3.2 摄像头预览组件的最小可用实现
下面我们一步一步实现 CameraPreview。我的目标是最小可用:能启动相机、能预览、能拍照,事件能回传 Dart。其他的比如美颜、滤镜、多摄像头切换,都放在扩展接口里,不阻塞主线。
第一步,Dart 侧定义组件参数。组件对外暴露分辨率、相机方向、闪光灯模式这几个核心参数:
enum CameraDirection { back, front } enum FlashMode { off, on, auto } class CameraPreviewSpec { final CameraDirection direction; final FlashMode flash; final Size resolution; final bool mirrorPreview; const CameraPreviewSpec({ this.direction = CameraDirection.back, this.flash = FlashMode.auto, this.resolution = const Size(1280, 720), this.mirrorPreview = false, }); }第二步,原生侧创建 XComponent 并在它上面启动相机会话。原生侧的核心逻辑是:创建相机 manager,获取相机列表,为指定方向创建会话,配置 preview Surface(也就是 XComponent 的 surface),然后启动会话。
这里最容易翻车的是 Surface 的 BufferQueue 格式。XComponent 申请的 buffer 格式如果不匹配,预览会是绿的或者直接黑屏。我的经验是:先查设备支持的 pixel format 列表,再选定一个,不要硬编码 RGBA_8888。部分设备 RGBA_8888 和 NV12 之间的转换有性能损耗,低端机上直接掉帧。
第三步,把 Surface 传给原生侧后,Flutter 侧的组件包起来:
Widget build(BuildContext context) { return AspectRatio( aspectRatio: widget.spec.resolution.width / widget.spec.resolution.height, child: PlatformNativeView( viewId: _nativeViewId, creationParams: { 'direction': widget.spec.direction.name, 'flashMode': widget.spec.flash.name, 'mirror': widget.spec.mirrorPreview, }, ), ); }第三步的微小之处在于creationParams的设计。参数必须做成序列化字典,让原生侧在视图创建时一次性读取,不要等视图创建完再通过 MethodChannel 传第二次。因为有一部分原生视图的初始化逻辑只允许在 attach 之前做,传晚了某些配置就不生效了。
3.3 拍照事件绑定:一次点击从原生回到 Dart
预览跑通之后,加一个拍照按钮。拍照的逻辑很简单:点击 Dart 按钮,通过 MethodChannel 发给原生,原生调用相机框架的 capture 方法,拍完把图片保存路径回传。这里把事件绑定和异步回调完整走一遍。
Dart 侧:
Future<String> takePicture(String savePath) async { final String? result = await _channel.invokeMethod<String>('takePicture', { 'savePath': savePath, }); return result ?? ''; }原生侧收到takePicture后,走异步拍照流程,拍照完成通过 Result 回传路径。注意这里有个典型错误:直接在相机回调线程里调用 Result.success。某些版本的回调线程是被系统绑定的,Result 必须在 UI 线程调用,否则 Dart 侧 Future 永远不会完成,或者永远在 pending。我的解决方法是统一 wrapped withmContext.getUITaskDispatcher(),确保回调最终在 UI 线程上执行。
真实项目里拍照往往还要触发振动、音效、快门回调。这些可以用 EventChannel 推给 Dart。比如onShutter快门声事件、onPictureTaken拍照完成事件、onError错误事件。这样组件使用方可以灵活组合:点击按钮立刻播放快门音效,图片保存成功后更新相册缩略图。
还需要处理一个边界场景:用户在相机初始化完成前点击了拍照。这种情况不要直接把错误抛出来,而是在组件内部做状态机。只有started状态才接受拍照命令,其他状态直接返回CameraNotReadyException。使用方可以 catch 到异常并提示用户"相机正在启动,请稍候"。
3.4 经验映射:Android 自定义组件的兼容与复用
如果你在 Android 上写过自定义组件,会发现 OpenHarmony 的组件开发逻辑有很多相似处:原生的 View 需要被包装成 Flutter 认识的视图;触摸事件需要坐标转换;生命周期要跟随页面。区别在于 Android 生态有成熟的 plugin 模板和 PlatformView 兼容层,而 OpenHarmony 还在追赶。
我的建议是:不要直接改造 Android 插件,而是抽象一层公共接口。比如摄像头组件定义startPreview()、takePicture()、setFlashMode(),Android 侧实现一份,OpenHarmony 侧实现一份,Dart 侧只依赖接口。这样无论平台怎么换,组件对外 API 不变,业务层代码不用动双份。
很多 Flutter 面试题里也会问"Flutter 如何跟原生通信",答案就是 MethodChannel、EventChannel、BasicMessageChannel。但面试题不会告诉你,真实项目的难点在于"一个组件同时维护两套平台的生命周期"。
我建议你做一个统一的PlatformComponentManager单例,注册表里存 viewId 到组件实例的映射。这样原生侧收到生命周期事件时,可以根据 viewId 找到对应组件,做精准的 pause、resume、dispose。这套代码写完,后面每新增一个自定义组件,都只用实现新的组件逻辑,公共的生命周期管理和事件分发直接复用。
4. 常见问题与排查技巧实录
4.1 冷启动就崩:Dart VM 初始化异常
先来聊一个很多人见过的错误日志:
E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled exception:这个 31173 是进程号,每次都不一样,所以搜索时不要带进程号。Unhandled exception 说明 Dart 侧抛了异常,但没有被 catch。时机上看,冷启动阶段崩,基本可以锁定在 main() 执行早期、runApp 之前或者第一个页面 initState 阶段。
排查步骤我按概率排:第一,检查依赖初始化的插件是否在 OpenHarmony 上可用。很多 Flutter 插件在 Android/iOS 上正常,但底层依赖了 Google 服务或者 iOS 私有 API,在 OpenHarmony 上一调用就崩。第二,检查是否在 initState 里直接调用了含 PlatformView 的组件,并且没等引擎完全就绪。第三,检查flutter/assets目录的文件是否完整。
对于这类问题,我的笨办法是加全局兜底:
void main() { runZonedGuarded(() { runApp(const MyApp()); }, (error, stackTrace) { // 上报、降级处理 }); }至少在崩溃时能看到完整堆栈,而不是只看到一行 Unhandled exception。还有一个冷门原因:OpenHarmony 上某些版本对异步初始化的要求更高,ensureInitialized()必须等待完成才能调用需要通道的方法。如果你跳过了这一步,就会收到MissingPluginException,在外层人会以为是原生没实现。
4.2 新建项目跑不起来:环境问题排查清单
"flutter 新建项目后跑不起来"是个高频搜索词,我着重复盘一下。出现这种问题,通常不是代码问题,而是工具链问题。
我的排查顺序是这样的:
| 症状 | 大概率原因 | 处理方式 |
|---|---|---|
| flutter doctor 报错 | SDK 路径或工具链缺失 | 重设环境变量,检查依赖是否完整 |
| 创建项目后 build 卡住 | 引擎产物未下载 | 配置镜像源后重新拉取 |
| 编译通过但安装失败 | 签名或设备连接异常 | 检查 hdc 设备连接 |
| 运行后白屏 | 入口 Activity/Ability 配置问题 | 检查应用配置里的页面路径 |
印象很深的一次,是我在 Windows 上创建项目后一直编译失败,报错信息指向 Gradle 下载失败。排查了半小时,最后发现是没有给 Gradle 配置镜像。这个问题在 OpenHarmony 上更隐蔽,因为它的构建链更长,任何一个环节的网络问题都会表现为"莫名其妙跑不起来"。所以我的习惯是准备好一份环境检查脚本,把 SDK、引擎、依赖、设备四部分一次性全部检查完,再做项目开发。
4.3 PlatformView 黑屏、卡顿和触摸失灵
自定义组件和项目跑起来后,第一类高频 bug 就是黑屏。黑屏的原因通常是 Flutter 侧视图树的层级和原生侧视图层级对不齐。在 Android 上有 HybridComposition 和 VirtualDisplay 之争,OpenHarmony 上同样存在"原生视图是嵌入还是悬浮"的问题。如果你发现组件在部分设备上正常、部分设备上黑屏,优先检查合成模式切换。
卡顿的典型原因则是主线程被原生代码占用。摄像头预览这种高频数据流,千万不要在 UI 线程做图像处理,否则帧率会掉到个位数。要处理就在原生侧开独立线程,做完再回调。触摸失灵的问题则大概率是坐标转换或者 hit test 阶段被 Flutter 手势竞技场拦截。我的处理方法是:如果原生视图内部有自己的滑动逻辑,就在 Flutter 侧给组件包一层IgnorePointer,让触摸事件直接穿透给原生;反之,如果 Flutter 侧需要响应点击,原生视图要做touchMode的配置,防止先被原生消费掉。
还有一个细节:下拉刷新和自定义组件联动时,手势竞争非常常见。我之前做列表里嵌摄像头预览,手指在预览区域上下滑,列表不滚动。后来在原生侧监听触摸事件,手动把未消费的事件回传给 Flutter,问题才解决。这类问题没有统一答案,必须在组件内部提供"手势透传策略"的开关。
4.4 事件时序:Future 回调到底进不进微任务队列
针对热搜词里的问题,我直接说结论。Dart 的 Future 回调默认进入微任务队列,在 UI 线程空闲时执行。也就是说,Future.then里写的代码一定是异步执行的,但依然在同一个事件循环里。这一点和浏览器里的 Promise 微任务调度很接近。
了解这个有什么用?当你调用原生通道时,MethodChannel 返回的 Future 在整个链路里经过了原生侧的线程切换:Dart 发出调用,原生线程执行,原生线程把结果封成平台消息发回 Flutter,Flutter 引擎在 UI 线程接到消息后完成 Future。所以你的 then 回调虽然跑在微任务队列,但触发它的是平台消息到达的时间点。
这个时序特性衍生出一个实用技巧:如果你在原生回调后需要立刻测量组件尺寸,单靠 Future 的回调时机可能不够,因为此时布局可能还没有完成。更稳的方案是WidgetsBinding.instance.endOfFrame.then(...),等本帧渲染结束再执行操作。很多组件黑屏、布局错位,排查到最后都是这种时序问题,不是逻辑写错,而是执行得太早。
4.5 XTS 认证:组件能不能过测,看这几项
最后聊聊兼容性认证。OpenHarmony 的 XTS 认证是设备/应用上架前的一道关,你的自定义组件如果直接用到了系统能力,一定要提前过测,否则会在认证阶段被打回。
组件相关最容易踩的认证红线有以下几条:第一,权限声明必须与实际调用一致,相机组件需要申请相机权限,但如果你还偷偷读取了设备信息,就会被判越权。第二,后台行为要合规,组件在应用切后台时必须停止硬件访问,不能持续占用摄像头。第三,异常处理要完善,HDI 调用失败时要有优雅降级,不能直接 crash。第四,兼容性矩阵要覆盖,至少要在高、中、低三档设备上分别测过,低端机的内存和 GPU 限制最容易翻车。
XTS 失败的日志有时很隐晦,不要只看结论,要同时抓取 hilog 和 Flutter 侧日志,两边对照定位。我有一个自己的检查清单:内存泄漏测试、后台生命周期测试、连续快速操作压力测试、低内存场景测试。这四项过了,XTS 基本稳了。
最后再分享一个实战经验。写自定义组件,不要把精力耗在"怎么画出好看的界面"上,要耗在"怎么让组件在不同设备上都稳定"上。我早期做过一个带拖拽节点的流程图组件,桌面预览完美,上真机后频繁崩,后来发现是低端机的 GPU 撑不住大量的阴影和模糊效果。改完特效,换用纯色和轻量绘制后,问题就消失了。这个类似 warm-flow 流程定义的场景给了我一个启发:组件设计之初就要想清楚它的运行环境下限,而不是只在开发机上自嗨。现在我的自定义组件库已经沉淀了十几个组件,每个组件都自带一套生命周期管理和事件映射层。这套东西几乎不需要改,换平台时只换原生实现就行,这正是 Flutter for OpenHarmony 最值得投入的地方。