Flutter/OpenHarmony 三方库适配实战:pda_scanner 兼容层

PDA 扫码插件和普通相机扫码插件有一个关键区别:它通常不负责打开摄像头,
而是依赖 Android 设备厂商提供的扫码广播、EMDK 或系统服务。pda_scanner
原库正是这种架构,支持斑马、海康威视、远望谷和思必拓等设备。

本次适配版本为 1.0.4-ohos.1,目标分为两层:

  1. 保留原有 Dart API、Android 厂商实现和音频提示能力。
  2. 新增 OHOS HAR 兼容层,让初始化、设备信息、回调管理、桌面跳转和通道
    生命周期先稳定下来,为后续接入具体 OHOS PDA SDK 留下明确入口。
    在这里插入图片描述

一、项目信息与适配边界

项目内容
仓库名称flutter_plugin_pda_scanner
原始包名pda_scanner
适配版本1.0.4-ohos.1
方法通道org.jerome/pda_scanner
原生入口PdaScannerPlugin
OHOS 扫码能力当前返回未检测到统一厂商桥接

整体链路如下:

Android vendor scanner
        |
        v
org.jerome/pda_scanner
        |
        v
PdaUtils.on(tag, callback)

OHOS compatibility layer
        |
        +-- initScanner
        +-- getPDAModel
        +-- getPlatformVersion
        +-- navigateToSystemHome
        |
        v
future vendor OHOS scanner SDK
        |
        v
sendBarcodeToFlutter

这次最重要的判断是:isThisPDASupported() 在没有 OHOS 厂商扫码桥接时必须
返回 false。如果为了让页面显示“支持”而返回 true,调用方会继续等待一个
永远不会到来的条码事件,排查成本反而更高。

二、先保留 Dart API,不让业务代码分叉

原库的 Dart 类 PdaUtils 已经把扫码订阅、设备信息、提示音和日志封装成静态
接口。业务代码通常在 main() 中先初始化:

void main() async {
  await PdaUtils.init();
  runApp(const MyApp());
}

再用 tag 管理多个页面的条码回调:

PdaUtils.on('inventory', (String barcode) {
  debugPrint('inventory: $barcode');
});

PdaUtils.off('inventory');
PdaUtils.offAll();

适配时没有新增 PdaUtilsOhos 之类的类,也没有要求业务页面增加 OHOS 分支。
Dart 层只修改平台判断和提示音加载时机,方法名、参数结构和回调签名保持不变。

平台判断也从 Platform.isAndroid 改成 Flutter 框架统一提供的
defaultTargetPlatform:

if (defaultTargetPlatform != TargetPlatform.android &&
    defaultTargetPlatform != TargetPlatform.ohos) {
  throw Exception(['PDA插件只支持 Android 或 OpenHarmony 系统设备!']);
}

这样在 OHOS Flutter 工具链中可以识别 TargetPlatform.ohos,在主机单元测试中
也不会访问只在 OHOS 运行时存在的 Platform.isOhos。

三、让 Flutter OHOS 识别原生入口

根 pubspec.yaml 保留 Android 的包名和插件类,只增加 OHOS 平台声明:

version: 1.0.4-ohos.1

flutter:
  plugin:
    platforms:
      android:
        package: org.jerome.pda_scanner
        pluginClass: PdaScannerPlugin
      ohos:
        pluginClass: PdaScannerPlugin

OHOS 模块配置为普通 HAR:

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

入口文件只导出插件类:

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

export default PdaScannerPlugin;

Flutter 工具链需要同时看到 pubspec.yaml、index.ets 和 Hvigor 配置。少任何
一层,都可能出现 Dart 通道已经创建,但 OHOS 原生类没有被注册的情况。

四、ArkTS 插件的通道实现

OHOS 插件实现 FlutterPlugin、MethodCallHandler 和 AbilityAware。Engine
绑定时创建通道,Ability 绑定时保存当前 UIAbility Context,分离时清空引用:

const CHANNEL = 'org.jerome/pda_scanner';

export default class PdaScannerPlugin
  implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private channel: MethodChannel | null = null;
  private uiAbilityContext: common.UIAbilityContext | null = null;
  private initialized = false;

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL);
    this.channel.setMethodCallHandler(this);
  }

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.uiAbilityContext = binding.getAbility().context;
  }

  onDetachedFromAbility(): void {
    this.uiAbilityContext = null;
  }
}

通道方法按原有 Dart 调用逐项映射:

Dart 方法OHOS 原生行为
initScanner标记兼容层已初始化并返回 true
isPDASupported返回 false,等待具体厂商桥接
getPDAModel读取 deviceInfo.productModel
getPlatformVersion返回 OHOS + deviceInfo.osFullName
navigateToSystemHome通过 Want action 返回系统桌面
errorSound保留方法兼容性,暂不模拟厂商蜂鸣

初始化方法返回 true 表示“通道和兼容层可用”,不表示“设备一定有扫码硬件”。
这两个语义必须分开:

case 'initScanner':
  this.initialized = true;
  this.sendLog('INFO', 'OpenHarmony compatibility layer initialized');
  result.success(true);
  return;
case 'isPDASupported':
  result.success(false);
  return;

五、设备信息为什么不能直接复用 Android 页面

原示例页面使用 device_info_plus 读取 Android Build 信息。OHOS 上继续调用
Android 信息接口只会得到“不支持”的结果,因此兼容层直接使用 OHOS 的
deviceInfo:

import { deviceInfo } from '@kit.BasicServicesKit';

case 'getPDAModel':
  result.success(deviceInfo.productModel || 'OHOS');
  return;
case 'getPlatformVersion':
  result.success(`OHOS ${deviceInfo.osFullName}`);
  return;

示例页面的 TargetPlatform 分支也补充了 OHOS:

TargetPlatform.ohos => <String, dynamic>{
  'platform': 'OHOS',
},

这样页面至少可以区分“Android Build 信息”和“OHOS 设备信息”,不会把 OHOS
设备错误地显示成 Fuchsia。

六、系统桌面跳转:使用 Want,而不是 Android Intent

Android 原库通过 Intent.ACTION_MAIN 和 CATEGORY_HOME 返回桌面。OHOS 使用
Want 表达同样的动作:

private navigateToSystemHome(): void {
  const context = this.uiAbilityContext;
  if (context === null) {
    return;
  }

  const want = new Want();
  want.action = 'ohos.want.action.home';
  context.startAbility(want).catch((error) => {
    this.sendLog('ERROR', `Unable to navigate home: ${String(error)}`);
  });
}

这里的 uiAbilityContext 不能省略。只保存 ApplicationContext 时,当前 Ability
可能已经分离,跳转行为会变得不稳定。页面返回键调用
PdaUtils.navigateToSystemHome() 后,Dart 层不需要知道底层是 Intent 还是
Want。

七、音频资源为什么要延迟加载

原库在 init() 中加载两个 WAV 文件:

var resource = await rootBundle.load(
  'packages/pda_scanner/assets/audio/scan_success.wav',
);
_scanSuccessAudioPlayer.setSource(
  BytesSource(resource.buffer.asUint8List()),
);

这对 Android PDA 可以接受,但在 OHOS 虚拟机、权限尚未完成或测试环境下,启动
阶段会产生额外的 audioplayers 原生初始化和定时器。适配后改为第一次真正
播放提示音时才加载:

static Future<void> _loadScanAudioPlayer() async {
  if (_audioInitialized) {
    return;
  }
  // load success and failure WAV resources
  _audioInitialized = true;
}

successSoundHumanVoice() 和 errorSoundHumanVoice() 会先等待资源加载,再调用
resume()。这样既保留原来的提示音行为,也让示例启动和自动化测试只依赖真正的
页面初始化。

原生 errorSoundDudu() 目前只返回成功,不伪造 OHOS 蜂鸣器能力;失败人声仍由
Dart audioplayers 播放。

八、厂商扫码桥接的预留设计

Android 的 CodeEmitterManager 负责把不同厂商广播统一发送到 Dart。OHOS 兼容
层没有绑定任何具体厂商 SDK,因此不会监听不存在的系统广播。后续接入具体 PDA
厂商时,桥接位置非常明确:

private emitBarcode(barcode: string): void {
  if (this.channel === null) {
    return;
  }
  this.channel.invokeMethod('sendBarcodeToFlutter', barcode);
}

厂商 SDK 的回调只需要调用 emitBarcode,Dart 端已有的
sendBarcodeToFlutter 分支就会把条码分发给所有 tag:

case 'sendBarcodeToFlutter':
  final String? barcodeContent = call.arguments;
  if (barcodeContent != null) {
    _callback.forEach((tag, callback) {
      callback.call(barcodeContent);
    });
  }
  break;

这种结构让“OHOS 兼容层”和“OHOS 厂商扫码实现”可以分阶段交付:

  • 第一阶段验证 Flutter 插件注册、通道、设备信息和生命周期。
  • 第二阶段按具体设备接入 OHOS SDK。
  • 第三阶段用真实扫码枪验证回调时序、重复条码过滤和提示音。

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

示例工程原来依赖 getwidget 4.0.0,在当前 Flutter OHOS 工具链上会因为
AppBarTheme 和 CardTheme 类型变化编译失败。适配时升级为:

getwidget: ^7.0.2

示例 widget 测试也从模板计数器测试改成真实页面测试:

  1. 注册 org.jerome/pda_scanner 方法通道 mock。
  2. 调用 PdaUtils.init()。
  3. 渲染 MyApp。
  4. 验证 OHOS 测试设备型号和“未知,请测试。”能力状态。

测试中同时 mock xyz.luan/audioplayers,避免没有原生播放器时出现
MissingPluginException。示例界面本地渲染图如下:

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上

十、构建与验证结果

本次执行:

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 设备运行待虚拟机测试
真实 PDA 扫码事件待厂商桥接和真机验证

Hvigor 构建成功证明插件 HAR、示例 entry、Dart 产物和通道声明能够组合成
Hap;它不能证明某个具体 OHOS PDA 的扫码 SDK 已经接入。

十一、虚拟机和真机测试清单

建议按以下顺序执行:

  1. 安装未签名 Hap,确认应用能够启动。
  2. 调用 PdaUtils.init(),确认没有 MissingPluginException。
  3. 查看首页的设备型号和系统版本,确认来自 deviceInfo。
  4. 确认 isThisPDASupported() 在没有厂商桥接时返回 false。
  5. 点击返回桌面按钮,确认 Want action 能回到系统桌面。
  6. 点击成功、失败提示音按钮,确认首次播放时资源加载。
  7. 在具体 OHOS PDA 上接入厂商 SDK 后,确认条码通过
    sendBarcodeToFlutter 到达 PdaUtils.on()。
  8. 记录条码内容、重复条码间隔、回调线程和页面切换后的监听状态。

如果初始化失败,先检查 pubspec.yaml 的 ohos.pluginClass、Hvigor HAR 配置
和 index.ets;如果初始化成功但没有条码,不要继续修改 Dart 回调,应先确认
厂商 OHOS SDK 是否已经发出原生事件。

十二、总结

这次适配把 Android 专用 PDA 插件拆成了稳定的跨平台契约和可替换的原生能力:

  • Dart API 保持不变,业务页面不需要 OHOS 分支。
  • OHOS 插件实现通道、设备信息、生命周期和桌面跳转。
  • isThisPDASupported() 如实表达当前没有统一 OHOS 扫码硬件能力。
  • 条码事件通过原有 sendBarcodeToFlutter 契约预留厂商接入点。
  • 提示音改为按需加载,降低启动阶段和测试环境的副作用。
  • 示例依赖升级后,Dart 分析、widget 测试和 Hap 构建均通过。

后续真正完成某个 OHOS PDA 型号适配时,应把厂商 SDK、真实条码截图、重复
扫描间隔和异常日志补到 verification.md,并继续保持 Android 原有实现不受
影响。

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

https://atomgit.com/CPF-Flutter

Logo

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

更多推荐