我最早在 Flutter 里做扫码功能,第一反应就是去 pub.dev 搜 qr_code_scanner,版本多、文档全、示例代码一跑就出画面,确实省心。直到我把同样一套代码迁移到 Flutter for OpenHarmony 上,才发现事情没那么简单:OpenHarmony 的相机接口、权限模型、原生插件机制跟 Android/iOS 完全是两套体系,qr_code_scanner 原版依赖的 Camera2 / AVFoundation 在鸿蒙设备上都不存在。
这篇文章不是教你怎么在安卓上扫码,而是记录我在 Flutter for OpenHarmony 工程里,把一个基于 qr_code_scanner 思路的二维码扫描能力从零跑通的全过程。包括 OpenHarmony 适配的底层差异、项目配置、桥接层设计、相机帧流转接、解码链路,以及我踩过的那些在 rk3568 / rk3588 开发板上特别典型的坑。准备在鸿蒙设备上用 Flutter 做扫码、工牌识别、设备绑定这类功能的同学,这篇文章可以直接当操作手册看。
1. 先搞清楚:Flutter for OpenHarmony 上的插件生态和安卓差在哪
1.1 Flutter for OpenHarmony 不是“换个包名”这么简单
很多人以为 OpenHarmony 兼容 Android APK,Flutter 应用打出来的 APK 直接丢上去就能跑,其实这个认识在消费级手机上有一定道理,但在 OpenHarmony 原生系统上并不成立。OpenHarmony 的应用开发接口是 ArkTS / ArkUI,底层渲染和系统服务都是自己的实现,Flutter 要跑在上面,必须通过 OpenHarmony 官方的 Flutter 适配分支来编译,最终产物是 HAP 包,不是 APK,也不是直接在 Android 运行时里兼容执行的。
Flutter for OpenHarmony 的适配工作在 OpenHarmony 官方仓库(flutter_flutter 的 ohos 分支)里已经做了不少,渲染能跑、基础 Widget 能显示、路由能跳、MethodChannel 也能用。但问题在于 Flutter 第三方插件的生态,绝大多数插件只实现了 Android 和 iOS 两端的原生代码,鸿蒙端没有对应实现。qr_code_scanner 就是非常典型的例子,它的 Android 端用 Camera2 API 管理相机和预览,iOS 端用 AVFoundation,OpenHarmony 这边既没有 Camera2 也没有 AVFoundation,所以原版插件在 OpenHarmony 工程里编译能过,但一调用相机就崩,或者干脆返回 NotImplemented。
1.2 qr_code_scanner 的插件分发机制决定了它需要“端口”
理解这个问题的关键,是搞清楚 Flutter 插件是怎么分发的。Flutter 插件本质上是一个 Dart 包加若干个原生工程目录,Android 的代码放在 android/,iOS 的代码放在 ios/,macOS、Windows、Linux 也各有自己的目录。Flutter 在构建时会读取插件的 pubspec,只把它对应平台的目录编译进产物。
OpenHarmony 不在 Flutter 官方支持的平台列表里,所以 pub.dev 上绝大多数插件都没有 ohos/ 目录。Flutter for OpenHarmony 的构建工具链为了兼容,会走一个类似“平台找不到就用 Android 实现”的兼容路径,但这里的实现是 Java/Kotlin 代码,OpenHarmony 原生模块是 ArkTS 的 .ets 文件加 native C++,两边的运行时互相不认。就算强行把 Android 源码编译进去,调用 CameraManager.openCamera 这类安卓 API 时也会直接抛异常,因为底层没有 Android Framework。
所以结论很直接:想在 Flutter for OpenHarmony 上用 qr_code_scanner,要么找别人已经移植好的 OpenHarmony 分支,要么自己照着它的接口设计写一套鸿蒙原生实现,通过 MethodChannel 桥接给 Dart 层调用。我这次采用的是后者,因为社区里针对 qr_code_scanner 的 OpenHarmony 移植版本还不太成熟,自己动手反而能把整个链路吃透。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建:把 Flutter 工程从安卓切换到 OpenHarmony
2.1 工具链清单,一个都不能少
先说我最后跑通的环境,方便你对照:
- OpenHarmony 4.1 Release(RK3568 开发板)
- DevEco Studio 5.0.0(含 SDK 和 hvigor 构建工具)
- flutter_flutter 的 ohos 分支,版本基于 Flutter 3.22
- Dart SDK 3.4.0
- Node.js 18+(hvigor 需要)
这里有个重点:不能用 pub.dev 下载的 Flutter 官方 SDK 直接编译 OpenHarmony 应用。你需要在 gitee 上拉取 flutter_flutter 仓库,切换到 ohos 分支,然后用这个 SDK 来执行 flutter 命令。路径配置也简单,把 flutter/bin 加到 PATH,然后跑 flutter doctor,如果提示 OpenHarmony 相关环境信息,说明 SDK 分支切换成功。
而且版本匹配很关键。我在一开始用了比较老的 ohos 分支,结果 DevEco Studio 的 hvigor 版本不兼容,构建 HAP 时直接报“hvigor version mismatch”。后来换成官方推荐的组合,才顺利过编译。建议你在搭环境前,先去看 flutter_flutter 仓库 README 里面写的版本兼容矩阵,别用最新版 DevEco Studio 去配老分支。
2.2 创建一个同时支持 Android 和 OpenHarmony 的工程
创建工程这一步,比普通 Flutter 工程多一点手工活。用 ohos 分支的 Flutter SDK 执行:
bash复制flutter create --platforms ohos --project-name qr_demo qr_demo
正常会在工程下生成 ohos/ 目录,里面就是 OpenHarmony 工程的入口。如果你只是想给已有 Flutter 工程加 OpenHarmony 支持,也可以手动在工程根目录创建 ohos/ 文件夹,再放一份标准的 OpenHarmony 工程结构,然后通过 flutter pub get 和 hvigor 联动构建。
我习惯保留 Android 平台,这样还能在模拟器上快速调试 UI,只在最后打 HAP 包时走 OpenHarmony 链路。需要注意,ohos/ 目录下的 module.json5 和 build-profile.json5 是核心配置,包名、签名、权限都在这里。
2.3 module.json5 里的相机权限,比想象的严格
二维码扫描肯定要相机权限,在 OpenHarmony 侧的 module.json5 里加上:
json5复制{
"module": {
"name": "entry",
"type": "entry",
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_permission_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
}
}
注意 reason 字段不能随便写,它对应 resources/base/element/string.json 里的字符串资源,原因会显示在系统权限弹窗上。还有 when 字段,建议用 inuse,这样权限只在使用期间生效,上架审核也更顺滑。
还有一个特别容易踩的坑:如果目标设备是 OpenHarmony 开发板,部分固件版本对相机权限的校验有 bug,明明在 module.json5 里声明了,运行时依然报 PERMISSION_DENIED。这种时候要检查系统设置里是否已经给了“相机”权限,或者先跑一下官方相机 demo 确认系统相机服务正常工作,别一上来就怀疑自己代码。
3. qr_code_scanner 的原理拆解,以及 OpenHarmony 侧怎么“等价实现”
3.1 一条扫码链路在 qr_code_scanner 里是怎么走的
要移植,先得把原版支离破碎的调用链捋清楚。qr_code_scanner 本质上做四件事:
- 权限申请:Dart 层调用
PermissionHandler去申请相机权限,系统弹窗,用户同意。 - 相机预览:原生层创建相机会话,把预览画面输出到
Texture或PlatformView。 - 帧流分析:原生层从相机输出流里拿到每一帧图像,通常转成 YUV 或 RGBA 格式。
- 二维码解码:把图像帧交给 ZXing 这类解码库,识别出内容后回调给 Dart。
用户看到的 UI 层其实很简单,就是一个 QRView Widget,加上 onQRViewCreated 回调。Dart 侧通过 QrReaderController 控制开始扫描、停止扫描、切换闪光灯、翻转摄像头。
在安卓上这四步各自由 Camera2 的 API 对应实现,打包成了 qr_code_scanner_android 等平台子包。到了 OpenHarmony,我就得把这四步挨个换成鸿蒙的 API,同时保留 Dart 层接口一致,这样上层 UI 代码不用动。
3.2 OpenHarmony 侧需要补齐的四个能力模块
OpenHarmony 相机开发用的是 @ohos.multimedia.camera 模块,核心思路跟安卓类似,但是 API 细节完全不同。以当前 OpenHarmony 4.1 的 ArkTS 接口为例,一个最简单的扫码相机链路包含:
- CameraManager:通过
camera.getCameraManager(context)获取,用来枚举相机和创建输入。 - CameraInput / PreviewOutput:负责打开设备摄像头,并输出预览流。
- ImageReceiver:这个非常关键,它负责接收相机帧数据,并可以设置分辨率、格式、帧率上限。
- Image:从 ImageReceiver 的
on('imageArrival')回调里拿到的图像封装,包含 buffer 和宽高信息。
二维码解码这块,OpenHarmony 没有现成的 ZXing 系统服务,我在工程里集成的是 ZXing 的 C++ 移植版,通过 Node-API 暴露给 ArkTS 调用。然后把 ImageReceiver 拿到的帧数据转成解码库需要的格式,通常有两种:
- NV21 格式直接喂给 ZXing 的
PlanarYUVLuminanceSource,省一次转换。 - 先把帧转成 RGBA 再做二值化,画面调试时比较直观,但性能开销更大。
我在 rk3568 上实测,NV21 直供解码是最稳的,1080p 帧率能跑到 20fps 左右,二维码在画面里停留不到半秒就能出结果。
3.3 桥接层设计:MethodChannel + EventChannel + PlatformView 怎么分工
Dart 和 OpenHarmony 原生之间要靠通道通信,我用三个通道各司其职:
- MethodChannel:负责“命令”,比如初始化相机、开始扫描、停止扫描、打开闪光灯。
- EventChannel:负责“事件流”,原生层把解码成功的二维码内容持续往 Dart 推。
- PlatformView:负责“画面”,让相机的预览画面直接嵌入到 Flutter 的 Widget 树里。
QRView 在 Dart 层就是一个 PlatformViewLink 或者 UiKitView 的封装。OpenHarmony 侧对应的是 XComponent,Flutter 的 ohos 分支已经支持把原生 XComponent 嵌入到 Flutter 视图里,预览数据就在这个组件里绘制。ArkTS 侧的核心结构大概长这样:
typescript复制@Component
export struct QrCameraView {
private xComponentController: XComponentController = new XComponentController();
private cameraService: CameraService = new CameraService();
build() {
XComponent({
id: 'qr_preview',
type: XComponentType.SURFACE,
controller: this.xComponentController
})
.onLoad(() => {
this.cameraService.init(this.xComponentController.getXComponentSurfaceId());
this.cameraService.startPreview();
})
}
}
Dart 侧通过 MethodChannel('qr_code_scanner_ohos/method') 调用 initCamera、startScan、stopScan,ArkTS 侧在收到 startScan 后才把 ImageReceiver 挂到相机链路上,之前只预览不解码,节省 CPU。
4. 实操过程:从依赖声明到真机跑通的关键步骤
4.1 先想清楚:到底用现成分支,还是自研通道
如果你不想自己写原生桥接,网上也有人把 qr_code_scanner 的接口封装到了 OpenHarmony 的 SDK 里,做成一个独立插件包。用现成包的好处是省事,坏处是版本滞后、可定制性差,特别是如果你需要识别高密度二维码、连续扫码、多码同时存在这样的场景,现成包往往调不动。
我这次是半自研:Dart 层直接用 qr_code_scanner 的公开接口规范(QRView、QRViewController),原生层自己实现。这样好处是如果以后官方适配了,我可以无缝切换,坏处是要保证接口兼容,写起来得对着源码一点一点对齐。
依赖声明上,我没有直接依赖 pub.dev 的 qr_code_scanner,而是把它的 Dart 层接口定义抽了一份到本地工程,因为原版的 qr_code_scanner 包里可能还带了 Android 的路由逻辑,在不支持平台的工程里会触发加载问题:
yaml复制dependencies:
flutter:
sdk: flutter
permission_handler: ^11.0.0
zxing_ohos:
path: ./plugins/zxing_ohos
zxing_ohos 是我自己封装的 OpenHarmony 原生扫码能力,对外提供 initCamera、startDecode、stopDecode、toggleFlash 等接口,内部走 @ohos.multimedia.camera 和 C++ 解码。
4.2 在工程里配置 hvigor 和签名,别在最后一步卡住
OpenHarmony 工程的构建走的是 hvigor,不是 gradle。在 ohos/ 目录下执行:
bash复制hvigorw assembleHap --mode module -p product=default --no-daemon
首次构建时间可能比较长,因为要下载 SDK 组件,还要编译 native C++ 代码。这里有一个新手必踩的坑:HAP 包必须签名才能安装到开发板上。用 DevEco Studio 打开 ohos/ 目录,在 File -> Project Structure -> Signing Configs 里勾选自动签名,它会自动生成调试证书。证书生成之后,build-profile.json5 里会多出 signingConfigs 配置,千万别手抖删掉,否则安装阶段就是各种 “install sign verify failed”。
4.3 Dart 侧扫码页面的完整写法,可以直接抄
Dart 层调用很简单,直接继承 qr_code_scanner 的接口风格,我贴一个实际能跑的核心页面:
dart复制class ScanPage extends StatefulWidget {
@override
_ScanPageState createState() => _ScanPageState();
}
class _ScanPageState extends State<ScanPage> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
QRViewController? controller;
@override
void initState() {
super.initState();
_requestCameraPermission();
}
Future<void> _requestCameraPermission() async {
final status = await PermissionHandler().requestPermission(Permission.camera);
if (status != PermissionStatus.granted) {
// 提示用户去设置
}
}
Widget build(BuildContext context) {
return Scaffold(
body: QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.green,
borderRadius: 12,
borderLength: 24,
borderWidth: 4,
cutOutSize: 240,
),
),
);
}
void _onQRViewCreated(QRViewController controller) {
this.controller = controller;
controller.scannedDataStream.listen((event) {
// event.code 是识别到的二维码内容
// 这里做一次防抖,避免同一帧重复回调
if (event.code != null && event.code!.isNotEmpty) {
controller.pauseCamera();
Navigator.pop(context, event.code);
}
});
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}
注意 controller.dispose() 一定要在 dispose 里调用,否则相机资源不会释放,下次进页面就是黑屏。这条我在早期版本漏掉过,导致页面销毁后相机还在工作,开发板被整得发热严重。
4.4 真机调试时,rk3568 / rk3588 开发板上的相机服务要提前测
你如果用的是 rk3568 或 rk3588 这类开发板,建议在跑 Flutter 工程之前,先用系统自带的相机应用或 DevEco Studio 里的相机 demo 测一下摄像头模组。我遇到过好几次,开发板的 MIPI-CSI 接口没插好,或者驱动的 sensor 型号不匹配,导致系统相机都打不开。这时候 Flutter 工程检测不到相机设备,getCameraManager 返回的列表是空的,扫码自然就是黑屏加报错。
真机验证的流程一般是:hdc shell 连上开发板之后,先执行 hdc shell media_service 或者直接拉起系统相机应用,确认相机画面能出,再回到 Flutter 工程做集成测试。千万别跳过这步,不然你会拿着 Flutter 代码排查半天,最后发现是硬件接线问题。
5. 常见问题与排查技巧实录
5.1 扫描识别率低,二维码离远一点就解不出来
这是我在 rk3568 上遇到最多的问题,现象是二维码贴得很近能识别,离远一点就失败。归根结底是图像分辨率设置不合理。ImageReceiver 的分辨率配的是 1920×1080,但预览画面里二维码只占很小一块,解码库拿到的像素点太稀疏,定位图形都识别不出来。
解决办法有两个方向,一个是把 ImageReceiver 的分辨率调低到 1280×720,减少帧数据量的同时,让二维码在画面里占据更大比例;另一个是加一个“放大预览”的交互,让扫码框可以缩放。实测下来,1280×720 这个档位在大多数场景下是性价比最高的,解码速度和识别率都能接受。如果二维码特别小,可以尝试 640×480,虽然画面模糊,但解码库对低分辨率大目标的识别率反而高。
5.2 摄像头预览是好的,但扫一次之后第二次进页面就黑屏
这类问题几乎都是资源没有释放干净。OpenHarmony 的相机 API 跟安卓不一样,CameraInput 和 PreviewOutput 都有独立的 release 流程,Flutter 页面销毁时,MethodChannel 调了 stopPreview,但底层 ImageReceiver 没有 release,导致相机设备还被占着。
我的处理方式是:在原生层维护一个相机状态机,init -> start -> stop -> release,每个状态都做幂等处理。Dart 层 dispose 时,依次调用 stopDecode 和 releaseCamera,并且原生层在 release 里真正释放所有资源。这样即使调用顺序乱,也不会因为重复释放崩溃。
5.3 构建时遇到 flutter-plugin-loader 解析失败的报错
这是 Flutter 工程的经典问题,哪怕不是 OpenHarmony 也经常见:
text复制Error resolving plugin [id: 'dev.flutter.flutter-plugin-loader', version: ...]
造成这个问题的原因一般是 Flutter SDK 和插件包的版本不匹配,或者本地 gradle 缓存里有旧版插件的残留。我的排查思路是:先 flutter clean,再删掉 ~/.gradle/caches 里跟 flutter 相关的目录,然后重新 flutter pub get。如果还没解决,去 android/settings.gradle 里看一眼 pluginManagement 仓库是否配置了 google() 和 mavenCentral(),部分镜像环境只配了阿里云镜像,缺少 google 仓库也会导致插件下载失败。
顺带说一句,OpenHarmony 构建本身不走 gradle,但工程里还保留着 android 目录的话,某些 IDE 的检查任务会自动跑 gradle,这个报错就会莫名其妙冒出来。不对 Android 目标做修改的话,可以直接忽略它。
5.4 画面卡顿、解码延迟高,怎么定位瓶颈
扫码的流畅度由三部分决定:相机帧率、YUV 转码速度、解码算法耗时。先在 ArkTS 侧打日志,分别记录 imageArrival 回调频率和解码函数耗时。如果 imageArrival 频率只有 10fps 以下,说明 ImageReceiver 的 capability 配置不够,把 FPS 上限调高;如果解一帧要好几百毫秒,问题在 C++ 解码侧,需要检查是不是每次都在创建新的解码器对象,理论上 ZXing 的 MultiFormatReader 只需要创建一次,复用实例。
还有一个隐藏性能杀手:日志打印。在帧回调里每帧都 console.info 一张图的信息,开发板立刻卡成幻灯片。上真机调试时,把这类高频日志级别调到 debug 以下,或者用宏包裹,只在 debug 版本打印。
5.5 PlatformView 和 Flutter 手势冲突,扫码框拖不动
OpenHarmony 的 XComponent 嵌入 Flutter 后,会出现在 Flutter 手势系统的“上层”,如果扫码框里有拖动调整框大小的交互,事件会被原生组件吃掉,Flutter 收不到。我的绕行方案是:不在 PlatformView 上做手势,而是把扫码框、遮罩层、提示文字全部做成 Flutter Widget 覆盖在 XComponent 上面,用 Flutter 自己的 GestureDetector 处理。这样手势逻辑全部留在 Dart 层,原生侧只负责渲染相机画面,边界清爽。
5.6 权限回调不回来,弹窗点了“允许”还是没反应
OpenHarmony 上权限回调有异步时序问题,特别是在 devEco 的 API 版本较老时,requestPermissionsFromUser 的 Promise 在某些情况下不会 resolve。我用的是兼容写法——在 Dart 层先检测权限,再调用原生通道;ArkTS 侧再做一次二次检查,两层都通过才真正启动相机。如果开发板权限弹窗压根没弹,检查 module.json5 里有没有 reason 和 usedScene,OpenHarmony 4.0 之后这两个字段缺一不可。
6. 写在最后:给新手的三个实操建议
折腾完这整套流程,给我最大的感受是:Flutter for OpenHarmony 的发展速度已经比想象中快不少,基础框架层面确实能用了,但插件生态的“最后一公里”依然要靠开发者自己补。如果你只是做一个内部工具类 App,建议先评估一下能不能用系统相机扫码能力直接顶上去,或者找现成的鸿蒙原生扫码组件,而不是一上来就在 Flutter 里做嵌入式相机开发,成本完全不是一个量级。
如果产品确实需要 Flutter 一站式跨端,那我的个人建议是:前期先用一个模拟数据源把 Dart 层 UI 和业务逻辑全部跑通,相机相关功能留到最后再接入真正的 OpenHarmony 原生实现。这样排查问题时,你会很清楚问题出在业务层还是相机层,不会两边互相甩锅。
最后分享一个小技巧:开发板上调扫码,摄像头模组最好固定在支架上,不要用手拿着,否则每帧图像都轻微抖动,解码稳定性会很差。等整套链路跑通,再考虑用自动对焦的摄像头模组,能省掉一大半“识别不了”的烦恼。这套方案现在稳定跑在我手头的 RK3568 板子上,后续如果官方适配了 qr_code_scanner 的 OpenHarmony 分支,我大概率会直接切过去,毕竟少维护一层原生代码,对团队来说就是实打实的收益。
