有一个 Flutter 插件,原来在 Android 和 iOS 上都跑得好好的,现在要加到 HarmonyOS 端。一开始以为就是把 Dart 层的代码拷过去,然后原生侧重新写一遍桥接代码就行。真正做起来才发现,Dart 层确实改不了多少,但原生侧的工作量比预想的大得多——Platform Channel 怎么接、异步回调怎么传、数据类型怎么转、生命周期怎么管,每一步都有坑。

这篇复盘一次真实的插件迁移过程,讲讲哪些部分基本不用动,哪些地方才是真正花时间的。

一、Dart 层代码大部分可以直接复用

先说个好消息:Dart 层的业务逻辑、UI 调用、参数组装这些,基本不用改。插件对外暴露的 API 接口,Dart 端调 MethodChannel.invokeMethod("xxx", params),这个调用方式在 HarmonyOS 上是一样的。

我把插件的 Dart 文件整个拷过来,改了一下 MethodChannel 的 name,编译直接通过。业务页面里调插件的方法,一行代码都不用动。这就是 Platform Channel 设计的好处——上层 Dart 代码不关心底层是 Android 还是 HarmonyOS,只要 Channel 接口对上就行。

真正要改的是两块:一是 Dart 侧初始化时注册的 MethodChannel name 要和 HarmonyOS 原生侧一致;二是原来 Android/iOS 特有配置(比如 AndroidManifest 里的权限声明)要换成 HarmonyOS 的 module.json5。其他 Dart 代码,基本原样保留。

二、Platform Channel 在 HarmonyOS 上怎么接

Android 上用 MethodChannel,HarmonyOS 上对应是不同的接口。Dart 调过来的方法名和参数,HarmonyOS 原生侧要注册一个 handler 来接收。

整体流程是:Dart 调 invokeMethod → Flutter Engine 通过 Platform Channel 把消息传给 HarmonyOS 原生侧 → 原生侧的 MethodCallHandler 收到方法名和参数 → 执行系统能力 → 把结果回传给 Dart。

这个流程和 Android 的思路是一样的,只是具体 API 写法不同。关键是方法名和参数格式要两边对齐,Dart 传过来的是 Map<String, dynamic>,原生侧收到后要拆成具体类型。

下面这段代码放在 BatteryPlugin.ets 里,是 HarmonyOS 原生侧注册 MethodChannel 的示例。它解决的问题:接收 Dart 发来的 getBatteryLevel 调用,执行后把电池电量回传给 Dart。

import methodChannel from '@ohos.methodChannel';

class BatteryPlugin {
  private channel: methodChannel.MethodChannel | null = null;

  register(channelName: string): void {
    this.channel = methodChannel.MethodChannel.getChannel(channelName);
    this.channel.setMethodCallHandler((call, result) => {
      switch (call.method) {
        case 'getBatteryLevel':
          this.handleGetBatteryLevel(result);
          break;
        default:
          result.notImplemented();
          break;
      }
    });
    console.info('[BatteryPlugin] registered channel:', channelName);
  }

  private handleGetBatteryLevel(result: methodChannel.Result): void {
    try {
      const level = this.queryBatteryLevel();
      result.success({ level: level });
    } catch (e) {
      result.error('UNAVAILABLE', 'Battery service not ready', null);
    }
  }

  private queryBatteryLevel(): number {
    return 85;
  }

  unregister(): void {
    if (this.channel) {
      this.channel.setMethodCallHandler(null);
      this.channel = null;
    }
  }
}

这段代码要解决的问题:register 时绑定 Channel name 和 MethodCallHandler,Dart 调 getBatteryLevel 时原生侧执行查询并返回结果;出错时用 result.error 把错误码和错误信息传回 Dart,而不是直接 throw;unregister 时清空 handler,防止页面销毁后还收到调用。

实际使用时要注意:methodChannel 的具体导入路径和 API 名称需要对照当前 Flutter HarmonyOS embedder 的版本确认,不同版本可能有差异。result.success 的参数格式要和 Dart 侧期望的一致——Dart 那边 await invokeMethod 拿到的就是这个 Map。unregister 一定要在页面或插件销毁时调,否则页面反复进入会注册多个 handler,同一个调用被执行多次。

三、异步结果和回调怎么返回 Dart

系统能力很多是异步的——查位置、读传感器、初始化 SDK,这些都不是同步返回结果的。Android 上用 Callback 或 Future,HarmonyOS 原生侧也要用异步方式执行,完成后再调 result.success 把结果传回去。

这里有个容易踩的坑:原生侧的 handler 是在平台线程上执行的,如果你在 handler 里直接调 UI 操作或者需要跨线程通信的系统 API,要先切到正确的线程。不然回调可能不触发,或者触发时页面已经销毁了。

EventChannel 的迁移思路类似:Dart 侧 listen,原生侧有事件流时通过 EventSink 发出去。页面销毁时 Dart 侧 cancel 订阅,原生侧也要把对应的事件监听移除,不要留着继续发事件。

四、数据类型怎么转换

Dart 的 string、number、bool、Map、List,在 Platform Channel 传输过程中会被序列化成标准格式,原生侧收到时是对应的 ArkTS 类型。基本类型一一对应,但要注意几个细节:

Dart 的 int 可能是 32 位也可能是 64 位,原生侧收到后要确认精度。Dart 的 Map 键值都是 dynamic,原生侧取值时要做类型判断,不能假设一定是 string。嵌套 Map 和 List 要递归拆,不要直接把整个 Map 当参数传给系统 API。

我在项目里写了一个参数转换层,把 Channel 传过来的 Map 拆成强类型的业务参数,原生侧后续代码都用强类型对象,不在业务代码里直接 dynamic 取值。这样做的好处是类型错误在转换层就暴露了,不会藏到运行时。

五、插件初始化与销毁怎么处理

插件初始化时机很重要。太早初始化,系统服务还没 ready;太晚初始化,Dart 那边第一次调用就失败。我的做法是在应用启动时注册插件,注册是轻量操作,真正调系统能力时才初始化。

页面反复进入时最容易出问题:第一次进入页面注册了事件监听,退出时忘了注销,第二次进入又注册一次,结果同一个事件回调被执行多次。这个和之前讲的后台任务未释放是同一类问题——创建和销毁必须配对。

六、多端差异怎么隔离

Android、iOS、HarmonyOS 三端的原生能力实现不一样,但 Dart 侧的调用接口是统一的。怎么隔离这些差异?

做法是在 Dart 侧定义一个抽象接口,每个平台有自己的实现类。Dart 业务代码只依赖抽象接口,不关心底层是哪个平台。新增 HarmonyOS 端时,只写一个 HarmonyOS 实现类,业务代码不用改。

如果某端暂时没有某个能力实现,就在抽象接口的 HarmonyOS 实现里返回一个未实现错误,Dart 侧捕获后给用户一个提示。不要因为某端缺能力就让整个插件编译不过。

七、后续升级怎么避免三端代码越来越乱

插件后续要维护三个端,最怕的就是改一个功能,三个端各自加一堆 if-else,代码越来越乱。

核心原则是:Dart 侧的接口定义保持稳定,三端各自实现。新增功能时先在 Dart 抽象接口加方法,再在三个端分别实现。不要在 Dart 侧写平台判断逻辑(if platform == Android ...),平台差异应该封装在各端的实现类里,不要漏到业务代码层。

插件迁移这件事,Dart 代码复用率确实高,但真正的工作量在原生侧的桥接层。把 Channel 接口、数据转换、生命周期管理这几块写清楚了,后面维护才不会越来越乱。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐