Flutter/OpenHarmony 三方库适配实战:flutter_subscreen_plugin 双屏通道

收银机、POS 终端和客显设备经常采用“主屏操作 + 副屏展示”的组合。原库
flutter_subscreen_plugin 在 Android 上通过第二 Flutter Engine 和
Presentation 承载副屏,同时用两条方法通道让主屏和副屏交换数据。

这次适配的版本是 1.3.2-ohos.1。适配目标不是把 Android 的
Presentation 机械翻译成 ArkTS,而是先建立稳定的 OHOS 插件入口和可验证
的通道契约:

  1. 保留 Dart 层的静态 API、方法名和参数结构。
  2. 在 OHOS HAR 中注册 FlutterSubscreenPlugin。
  3. 用 display.getAllDisplays() 做真实多显示器检测。
  4. 让主通道、副通道的 success、error、notImplemented 状态可以
    回到 Dart。
  5. 不在没有第二窗口能力时伪造副屏窗口。

一、项目信息与适配边界

项目内容
仓库名称flutter_subscreen_plugin
仓库地址https://atomgit.com/oh-flutter/flutter_subscreen_plugin
适配版本1.3.2-ohos.1
主通道screen_plugin_main_channel
副通道screen_plugin_sub_channel
OHOS 原生入口FlutterSubscreenPlugin
当前能力检测display.getAllDisplays()
当前真实副屏窗口未接入,待后续 OHOS 多屏窗口/FlutterView 验证

原库的核心流程可以抽象为:

Main Flutter Engine
        |
        | screen_plugin_main_channel
        v
Native plugin bridge
        |
        | screen_plugin_sub_channel
        v
Sub Flutter Engine

Android 的副屏不是普通 Dialog,而是绑定到外接 Display 的第二 Flutter
Engine。OHOS 上需要单独确认多屏窗口、FlutterView、窗口生命周期和触摸
事件模型,因此本次先完成可编译、可测试的通道兼容层。

二、先确认这个库为什么需要适配

原库的 lib/src/sub_screen_plugin.dart 只依赖 Flutter 的
MethodChannel,没有平台专属 Dart 代码:

static MethodChannel _mainChannel = MethodChannel(_mainChannelName)
  ..setMethodCallHandler(_onMainChannelMethodHandler);

static MethodChannel? _subChannel;

这说明业务层已经具备跨平台基础,但 isMultipleScreen、
doubleScreenShow 等方法必须由原生实现。根 pubspec.yaml 原本只有
Android 插件声明,缺少 OHOS 入口。因此适配的关键不是重写 Dart 类,而是
让 Flutter OHOS 工具链找到原生类,并让 ArkTS 使用和 Dart 相同的通道契约。

三、让 Flutter OHOS 找到插件

根 pubspec.yaml 保留 Android 配置,只增加 OHOS 声明:

version: 1.3.2-ohos.1
homepage: https://atomgit.com/oh-flutter/flutter_subscreen_plugin

flutter:
  plugin:
    platforms:
      android:
        package: com.rex.flutter_subscreen_plugin
        pluginClass: FlutterSubscreenPlugin
      ohos:
        pluginClass: FlutterSubscreenPlugin

OHOS 模块使用 HAR 类型:

{
  "module": {
    "name": "flutter_subscreen_plugin",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}

入口文件只导出插件类:

import FlutterSubscreenPlugin
  from './src/main/ets/components/plugin/FlutterSubscreenPlugin';

export default FlutterSubscreenPlugin;

这三个配置缺一不可:pubspec.yaml 负责让 Flutter 发现插件,
oh-package.json5 和 build-profile.json5 负责让 Hvigor 构建 HAR,
index.ets 负责让原生入口被导出。

四、ArkTS 插件的生命周期

插件同时实现 FlutterPlugin、MethodCallHandler 和 AbilityAware:

const MAIN_CHANNEL = 'screen_plugin_main_channel';
const SUB_CHANNEL = 'screen_plugin_sub_channel';

export default class FlutterSubscreenPlugin
  implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private mainChannel: MethodChannel | null = null;
  private subChannel: MethodChannel | null = null;

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.mainChannel = new MethodChannel(
      binding.getBinaryMessenger(),
      MAIN_CHANNEL,
    );
    this.subChannel = new MethodChannel(
      binding.getBinaryMessenger(),
      SUB_CHANNEL,
    );
    this.mainChannel.setMethodCallHandler(this);
    this.subChannel.setMethodCallHandler({
      onMethodCall: (call: MethodCall, result: MethodResult): void => {
        this.forwardToMain(call, result);
      },
    });
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    this.mainChannel?.setMethodCallHandler(null);
    this.subChannel?.setMethodCallHandler(null);
    this.mainChannel = null;
    this.subChannel = null;
  }
}

onAttachedToEngine 创建通道并注册处理器,onDetachedFromEngine 必须
清空处理器。如果只把通道置空而不解绑处理器,Engine 重建后可能出现旧
Handler 仍然接收消息的情况。

五、显示器检测不能伪造

Android 原库通过 MediaRouter 找到 presentationDisplay。OHOS 当前
兼容层使用系统 display API 的真实显示器列表:

private async hasExternalDisplay(): Promise<boolean> {
  const displays = await display.getAllDisplays();
  return displays.length > 1;
}

supportDoubleScreen 和 doubleScreenShow 共用这个检测逻辑:

case 'supportDoubleScreen':
  this.respondWithDisplay(result);
  return;

case 'doubleScreenShow':
  this.respondWithDisplay(result);
  return;

respondWithDisplay 通过回调把异步结果送回 Flutter:

private respondWithDisplay(result: MethodResult): void {
  this.hasExternalDisplay().then((hasDisplay: boolean) => {
    result.success(hasDisplay);
  }).catch((error: Error) => {
    result.error('subscreen_error', error.message, null);
  });
}

这里刻意没有写 result.success(true),也没有在 Dart 层根据平台判断直接
返回支持。单屏虚拟机上,isMultipleScreen 应该返回 false,这是能力
边界,不是插件故障。

六、双通道如何转发消息

业务方法名和参数来自原库,OHOS 不再解释一套新的协议。主通道收到未知方法
时转发给副通道:

private forwardToSub(call: MethodCall, result: MethodResult): void {
  const channel = this.subChannel;
  if (channel === null) {
    result.success(false);
    return;
  }
  channel.invokeMethod(call.method, call.args, this.bridgeResult(result));
}

副通道收到消息时反向转发给主通道:

private forwardToMain(call: MethodCall, result: MethodResult): void {
  const channel = this.mainChannel;
  if (channel === null) {
    result.success(false);
    return;
  }
  channel.invokeMethod(call.method, call.args, this.bridgeResult(result));
}

当前 Flutter OHOS SDK 的 MethodCall 字段是 args,MethodChannel
的 invokeMethod 是回调式异步 API。转发时不能写
await channel.invokeMethod(...),也不能假设它返回 Promise。结果桥接
需要显式保留三种状态:

private bridgeResult(result: MethodResult): MethodResult {
  return {
    success: (value: Any): void => {
      result.success(value);
    },
    error: (
      errorCode: string,
      errorMessage: string,
      errorDetails: Any,
    ): void => {
      result.error(errorCode, errorMessage, errorDetails);
    },
    notImplemented: (): void => {
      result.notImplemented();
    },
  };
}

这一点是本次适配中非常容易踩坑的地方:如果转发时总是
result.success(true),Flutter 会看不到原生端真实的异常;如果忘记
notImplemented,未实现方法也可能被误认为成功。

七、Dart API 保持原样

业务代码仍然可以这样发送消息:

await SubScreenPlugin.sendMsgToViceScreen(
  'text',
  params: {'num': '1024'},
);

副屏页面监听副通道:

SubScreenPlugin.viceStream.listen((MethodCall call) {
  debugPrint('${call.method}: ${call.arguments}');
});

副屏回传主屏时,调用原有接口:

await SubScreenPlugin.sendMsgToMainScreen(
  'text',
  params: {'status': 'ready'},
);

SubScreenPlugin.mainStream.listen((MethodCall call) {
  debugPrint('${call.method}: ${call.arguments}');
});

适配没有新增 SubScreenPluginOhos,也没有要求业务页面写 OHOS 分支。
这样 Android 和 OHOS 使用同一套 Dart 调用方式,后续真正接入 OHOS 第二
窗口时,业务层只需要继续监听已有的 Stream。

八、示例工程的兼容性修正

原示例使用 Flutter 2 时代的 Key key 构造参数和 window API。在当前
Flutter OHOS 工具链上会触发空安全编译错误。适配后的入口改为:

void main() {
  final String routeName = PlatformDispatcher.instance.defaultRouteName;
  if (routeName == 'subMain') {
    runApp(const SubApp());
  } else {
    runApp(const MaterialApp(home: MainApp()));
  }
}

页面构造参数改为 super.key,示例测试也改成验证真实页面:

testWidgets('renders the main-screen controls', (WidgetTester tester) async {
  tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
    const MethodChannel('screen_plugin_main_channel'),
    (MethodCall call) async => false,
  );
  tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
    const MethodChannel('screen_plugin_sub_channel'),
    (MethodCall call) async => false,
  );

  await tester.pumpWidget(const MainApp());
  await tester.pump();

  expect(find.text('主屏'), findsOneWidget);
  expect(find.text('发送数据给副屏'), findsOneWidget);
});

测试不验证未显示的“未查询到可用副屏”弹窗,而是验证页面已经渲染出主屏
操作入口;这样测试不会把一个被隐藏的 SnackBar 当成必须出现的 UI。

OHOS 虚拟机主屏页面已正常渲染,截图如下:

在这里插入图片描述

截图来自 emulator 7.0.0.107、系统 OHOS OpenHarmony-7.0.0.105,页面显示
“主屏”“接收到的副屏数据为:null”以及发送、开启和关闭副屏按钮。普通单屏
模拟器没有真实副屏,因此文章不伪造第二 Flutter Engine 或副屏数据。

九、构建与测试

本次执行的验证命令:

flutter pub get
flutter analyze
cd example
flutter pub get
flutter analyze
flutter test
flutter build hap --debug --no-codesign

验证结果:

检查项结果
根包 flutter analyze通过,No issues found
示例 flutter analyze通过,No issues found
示例 flutter test通过,1 个测试
flutter build hap --debug --no-codesign通过
Hap 产物example/build/ohos/hap/entry-default-unsigned.hap
OHOS 虚拟机启动通过,主屏页面正常显示,进程稳定存活
插件注册通过,FlutterSubscreenPlugin 已注册
主副通道通过,两个方法通道均已设置处理函数
单屏能力检测通过,不伪造副屏窗口
真实副屏窗口和触摸路由未实现,待后续接入 OHOS 多屏能力

ArkTS 编译过程中出现过三个真实 API 错误:MethodCall.arguments 不存在、
invokeMethod 不能使用 .then()、async 方法必须返回
Promise<T>。这些错误分别改成 call.args、回调式 MethodResult 转发
和非 async 的同步转发函数后,Hap 构建通过。

十、后续接入真实 OHOS 副屏窗口

当前代码已经明确了原生层的扩展位置。接入真实窗口时,建议按以下顺序:

  1. 在目标 OHOS SDK 中确认多屏窗口和 FlutterView 的创建接口。
  2. 在 doubleScreenShow 中选择外接 display,创建副屏窗口。
  3. 将副屏页面路由固定为 subMain,并处理主副 Engine 的生命周期。
  4. 将副屏触摸、返回键和窗口关闭事件转发到对应 Flutter Engine。
  5. 用真实客显设备验证显示时序、拔插显示器、熄屏恢复和消息队列。

在这些能力接入前,doubleScreenShow 只返回显示器检测结果;文档、测试和
验证记录都保持这一边界,不把“通道可编译”描述成“双屏窗口已经可用”。

十一、总结

这次适配把一个 Android 双 Flutter Engine 插件拆成了稳定的跨平台契约和
可替换的 OHOS 原生能力:

  • Dart API、方法名和参数结构保持不变。
  • OHOS HAR 入口、Hvigor 配置和 ArkTS 生命周期完成注册。
  • 双通道消息可以转发,错误和未实现状态不会被吞掉。
  • 多显示器检测使用 display.getAllDisplays() 的真实结果。
  • 示例工程升级到当前 Flutter 空安全 API,并加入可重复的 widget 测试。
  • Hap 构建、模拟器主屏启动、插件注册和双通道注册均已验证;真实副屏窗口
    和触摸路由仍待多屏设备验证。

后续最值得关注的不是继续增加 Dart 分支,而是确定目标 OHOS 设备的多屏
窗口模型,并把第二 FlutterView 的生命周期接入已经稳定的双通道桥接。

欢迎加入 CPF-Flutter 鸿蒙社区,一起交流 Flutter 三方库适配、工程实践和
虚拟机/真机验证经验:

https://atomgit.com/CPF-Flutter

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐