做 Flutter 开发这几年,我一直觉得跨端方案的尽头就是“能跑的地方全都跑”。所以当 OpenHarmony 开始支持 Flutter 时,我第一时间就开始折腾,这中间踩过的坑加起来比过去一年还多。这篇文章把我在 OpenHarmony 设备上接入“扫一扫”功能的完整过程记录下来,从环境搭建、功能接入到各种报错修复,希望能让后面接手的人少走几步弯路。
先交代一下背景。我这边的项目是在已有的 Flutter 业务代码上做鸿蒙适配,核心需求是在 OpenHarmony 设备上调用相机实现二维码扫描。听起来不复杂,但实际做下来发现,这个需求牵扯到 Flutter 引擎编译、鸿蒙原生插件、相机权限、UI 线程调度好几个层面,每个环节都可能埋雷。整篇文章按照“环境准备 — 依赖接入 — 核心实现 — 踩坑复盘 — 性能调优”的顺序展开,适合手里已经有一些 Flutter 基础、正要往 OpenHarmony 上迁移的人参考。
1. 项目整体思路与方案选型
1.1 为什么选择 Flutter 做 OpenHarmony 端的扫一扫
先说说方案选型。OpenHarmony 的官方应用开发推荐的是 ArkTS 和 ArkUI,但团队里大部分人只会 Dart,如果为了扫一扫功能单独维护一套 ArkTS 原生页面,成本太高。而且后续的业务迭代还要继续跨端,不可能每加一个功能就在鸿蒙侧写一套原生实现。所以最终决定沿用 Flutter 作为 UI 层和业务层框架,OpenHarmony 只负责提供平台能力和相机硬件接口。
这个思路的关键在于:把 Flutter 当成 UI 引擎,把 OpenHarmony 当成底层操作系统来适配。Flutter 本身具备完整的三端能力抽象,比如 MethodChannel、EventChannel 之类的平台通道设计,天然适合来做这种桥接。扫一扫的核心是相机采集和图像识别,这两块完全可以丢给原生层,Flutter 层只负责调用和展示结果。
还有一点,OpenHarmony 社区的 Flutter 适配项目(flutter_flutter)已经持续维护了一段时间,对 API 的支持度比想象中好。至少在我做的这个版本上,基础渲染、事件分发、平台通道都能跑通,只是细节上还需要额外适配。
1.2 扫一扫功能的技术栈拆解
扫一扫这个功能,从技术上看可以拆成四个模块:
- 相机采集模块:负责打开相机、设置预览分辨率、回调帧数据。
- 图像处理模块:对采集到的图像做灰度化、裁剪、旋转等预处理。
- 码识别模块:识别二维码或条形码内容,返回字符串和码类型。
- 业务回调模块:把识别结果通过通道回传给 Flutter 层,并触发下一步业务。
在 OpenHarmony 上,相机采集一般使用 @ohos.multimedia.camera 接口,码识别可以借助 OpenHarmony 的多媒体能力,也可以直接用开源库如 ZXing 做二次开发。考虑到 OpenHarmony 的 native 接口和 Android 不同,我采用的是“系统相机采集 + ZXing 识别”的组合。系统的 CameraManager 负责拿帧,ZXing 负责对字节流做解码,这样代码的可移植性好,后续如果要在其他设备上用,也能直接搬。
架构上的分层大致是这样:
| 层级 | 职责 | 技术选型 |
|---|---|---|
| UI 层 | 扫码界面、结果展示、交互逻辑 | Flutter / Dart |
| 桥接层 | Flutter 与 OpenHarmony 通信 | MethodChannel / EventChannel |
| 能力层 | 相机控制、权限申请、生命周期绑定 | ArkTS / OpenHarmony SDK |
| 算法层 | 二维码识别、图像预处理 | ZXing(C++ 或 Java 移植) |
1.3 选 TypeScript 桥还是自己写平台通道
OpenHarmony 的 Flutter 适配版里,官方推荐用 TypeScript 编写插件代码,然后通过 flutter 命令打包生成原生依赖。不过实际用下来,TS 插件的调试链路长,编译期报错信息也不友好。我最后选择了直接在当前工程的 entry/src/main 目录下写 ArkTS 代码,通过 MethodChannel 暴露原生方法,这样能直接利用 OpenHarmony SDK 的能力,也能在 DevEco Studio 里做原生调试,链路最短。
对比一下两种方案:
| 对比维度 | TS 插件方案 | 直接写 ArkTS |
|---|---|---|
| 集成速度 | 中间多一层转换 | 直接写,一步到位 |
| 调试体验 | 需要编译成原生代码才能断点 | 可调试 ArkTS 原生代码 |
| 代码复用 | 可在多个 Flutter 工程间复用 | 绑死在当前项目里 |
| 依赖复杂度 | 需要维护 pub 包和原生包两套配置 | 只需改当前工程 |
如果你只是“一次性接入”,比如只做一个扫码页,直接写 ArkTS 就够了。如果你要做成 SDK 给团队里多个项目用,再考虑拆成 TS 插件。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与工程配置
2.1 工具链版本组合
OpenHarmony 的 Flutter 开发环境和标准 Flutter 开发有一个很大区别:不能直接用 flutter.dev 下载的官方 Flutter SDK,要用 OpenHarmony 社区维护的分支。我一开始没注意这点,用官方 Flutter 版本跑 flutter doctor,结果设备列表里根本看不到鸿蒙设备。
最终确定的版本组合是:
- Flutter SDK:OpenHarmony 社区版 v3.7 分支(对应 OpenHarmony 5.0)
- DevEco Studio:5.0.0 Release
- OpenHarmony SDK:API 12
- Node.js:18.x 以上(用于部分工具链)
- 命令行工具:
ohpm(OpenHarmony 包管理器)
这里要特别提醒一句:社区分支的版本号命名和官方不同,不要在 flutter --version 的结果上纠结。只要 flutter doctor 能正常识别出 OpenHarmony 设备,基本就说明环境没问题。
2.2 使用 FVM 管理多版本 Flutter
因为手头同时维护着 Android 和鸿蒙两个项目,官方 Flutter 和 OpenHarmony 分支需要切换,我一直用 FVM 来管理。FVM 的好处是可以按目录锁定 Flutter 版本,不同项目切目录就自动切版本,不用手动改 PATH。
安装和使用流程:
bash复制# 安装 fvm
dart pub global activate fvm
# 添加 OpenHarmony 分支
fvm add 3.7.0-ohos
# 在项目目录下指定版本
fvm use 3.7.0-ohos
# 查看当前版本
fvm flutter --version
注意,FVM 在首次添加 OpenHarmony 分支时,需要你本地已经有该分支的源码或者能够从合法渠道获取。建议直接去 OpenHarmony 官方代码仓库拉取,拉到本地后通过 fvm add <路径> 的方式注册,最稳妥。
2.3 解决 Gradle 与 Visual Studio 工具链报错
在环境配置阶段,我先后遇到了两个热门问题,网上问的人也很多,这里一并说清楚。
第一个报错是:
text复制You are applying Flutter's main Gradle plugin imperatively using the apply script method
这个是因为新版 Flutter 的 Gradle 插件机制发生了变化,不再推荐用旧的 apply 方式注入。OpenHarmony 分支对这块做了修正,但如果你本地 Gradle 缓存里有旧配置,还是会触发。解决办法是更新项目里的 settings.gradle 和 build.gradle,用插件 DSL 的方式来声明:
groovy复制// settings.gradle
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
第二个报错是:
text复制unable to find suitable visual studio toolc
这个听起来像是 Visual Studio 的问题,但实际多发生在 Windows 环境下编译 OpenHarmony 原生模块时,找不到合适的 C++ 工具链。有两种处理方式:
- 在系统里安装 Visual Studio Build Tools,并勾选“使用 C++ 的桌面开发”工作负载。
- 如果你不需要在本地编译原生 C++ 代码,可以在 DevEco Studio 里关闭 native 编译开关,或者使用鸿蒙提供的预编译产物。
我自己的处理方式是装好了 Build Tools,然后把 cl.exe 的路径加入了系统环境变量,重新打开 DevEco Studio 后问题就消失了。
2.4 配置国内镜像与依赖管理
OpenHarmony 的 Flutter 分支在下载依赖时默认走官方源,网络不稳定时容易出现超时。我在 .bashrc 或系统环境变量里配置了国内镜像仓库,主要是阿里云镜像和鸿蒙官方镜像:
bash复制export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
同时,鸿蒙侧的依赖使用 ohpm 管理。在工程根目录下找到 oh-package.json5,需要添加的依赖都在这里声明。比如相机和权限相关的:
json5复制{
"dependencies": {
"@ohos/camera": "5.0.0-rc1",
"@ohos.permission": "5.0.0-rc1",
"@ohos.zxing": "1.0.0"
}
}
ohpm install 执行后会生成 oh_modules 目录,后续在 ArkTS 代码里直接 import 即可。
3. 扫一扫功能接入实战
3.1 权限声明与动态申请
在 OpenHarmony 上做相机扫码,权限声明是绕不开的第一步。和 Android 不同,OpenHarmony 的权限配置在 module.json5 里,而不是 AndroidManifest.xml。
打开 entry/src/main/module.json5,在 requestPermissions 数组里加上相机权限:
json5复制{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "用于扫一扫识别二维码",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
}
}
光有声明还不行,运行时必须动态申请。OpenHarmony 的权限申请是异步的,需要传入 UIAbilityContext。我在 ArkTS 侧封装了一个方法:
typescript复制import abilityAccessCtrl from '@ohos.abilityAccessCtrl'
import bundleManager from '@ohos.bundle.bundleManager'
import common from '@ohos.app.ability.common'
async function requestCameraPermission(context: common.UIAbilityContext): Promise<boolean> {
let atManager = abilityAccessCtrl.createAtManager()
let bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
let permissionStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA'])
if (permissionStatus.authResults[0] === 0) {
return true
}
return false
}
这块有两个注意事项:
- 必须在 Ability 是 foreground 状态时申请,否则会静默失败,没有任何弹窗。
reason字段必须写清楚用途,应用市场上架审核会看这个字段,随便写会被打回。
3.2 相机会话的创建与预览
OpenHarmony 的相机接口设计比较接近 Android Camera2,需要先获取相机管理器,然后创建输入源、输出源和会话。
核心代码如下:
typescript复制import camera from '@ohos.multimedia.camera'
import image from '@ohos.multimedia.image'
async function initCamera(surfaceId: string, context: common.UIAbilityContext) {
let cameraManager = camera.getCameraManager(context)
let cameras = cameraManager.getSupportedCameras()
let cameraInfo = cameras.find(item => item.position === camera.CameraPosition.CAMERA_POSITION_BACK)
let capability = cameraManager.getSupportedOutputCapability(cameraInfo, camera.SceneMode.NORMAL_PHOTO)
let previewOutput = cameraManager.createPreviewOutput(capability.previewProfiles[0], surfaceId)
let photoOutput = cameraManager.createPhotoOutput(capability.photoProfiles[0])
let cameraInput = cameraManager.createCameraInput(cameraInfo)
await cameraInput.open()
let session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession
session.beginConfig()
session.addInput(cameraInput)
session.addOutput(previewOutput)
session.addOutput(photoOutput)
await session.commitConfig()
await session.start()
}
surfaceId 是渲染载体。在 Flutter 层,我用的方案是用 Texture 控件承载相机预览,通过 Texture 的 textureId 和原生侧共享 Surface。Flutter 的 Texture 组件在 OpenHarmony 上的实现已经比较成熟,可以直接用。
Flutter 侧创建 Texture 的代码:
dart复制class ScanPage extends StatefulWidget {
@override
_ScanPageState createState() => _ScanPageState();
}
class _ScanPageState extends State<ScanPage> {
TextureController? _textureController;
@override
void initState() {
super.initState();
_initTexture();
}
Future<void> _initTexture() async {
final textureId = await _channel.invokeMethod('createScanTexture');
setState(() {
_textureController = TextureController(textureId: textureId);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: _textureController != null
? Texture(textureId: _textureController!.textureId)
: Center(child: CircularProgressIndicator()),
);
}
}
createScanTexture 这个 MethodChannel 方法会在原生侧创建一个 SurfaceTexture,并把 surfaceId 传入相机初始化流程。
3.3 帧回调与图像数据转换
扫一扫要识别码,必须拿到相机的帧数据。我在创建会话时,额外加了一个 ImageReceiver 输出:
typescript复制let imageReceiver = image.createImageReceiver(1280, 720, image.ImageFormat.JPEG, 8)
let receiverOutput = cameraManager.createImageReceiverOutput(imageReceiver)
session.addOutput(receiverOutput)
然后监听 imageReceiver 的 'imageArrival' 事件:
typescript复制imageReceiver.on('imageArrival', () => {
imageReceiver.readNextImage((err, imageObj) => {
let buffer = imageObj.getComponent(image.ComponentType.JPEG)
let pixelBytes = buffer.byteBuffer
// 把 byteBuffer 转换成可识别的 YUV 或 RGB 格式
decodeQrCode(pixelBytes, imageObj.size.width, imageObj.size.height)
imageObj.release()
})
})
这里有一个很关键的转换点:相机默认输出的是 YUV 格式,而 ZXing 的扫码库通常吃 RGB 数据。如果直接拿 YUV 去做识别,经常出现“能扫码但识别率极低”的情况。我在实际项目里是先把 YUV 转成 NV21,再喂给 ZXing,识别率明显提升。
YUV 转 NV21 的算法不复杂,核心就是把 Y 分量拷贝出来,再把 UV 分量交错排列。不过这块在 ArkTS 里写循环性能不够好,我是通过 SO 库的 JNI 接口来做的,将来如果要压性能,可以继续优化。
3.4 Flutter 与 ArkTS 的通道通信
平台通道是整个功能衔接的“血管”。我在 Dart 侧定义了三个通道:
dart复制static const MethodChannel _scanChannel = MethodChannel('com.example.scan/method');
static const EventChannel _scanEventChannel = EventChannel('com.example.scan/event');
MethodChannel 用来做“单向调用”,比如初始化相机、开始识别、停止识别。EventChannel 用来做“数据上报”,比如识别成功后把结果回调到 Flutter 层。
Dart 侧监听识别结果:
dart复制_scanEventChannel.receiveBroadcastStream().listen((event) {
if (event is Map) {
final result = ScanResult(
code: event['code'],
type: event['type'],
);
setState(() => _lastResult = result);
}
});
ArkTS 侧发送结果:
typescript复制let eventSink: common.EventSink
this.scanEventChannel = new common.EventChannel('com.example.scan/event')
this.scanEventChannel.onReceive((event) => {
eventSink = event
})
// 识别成功后
eventSink.success({
'code': resultString,
'type': 'QR_CODE'
})
这个流程看起来简单,但我在联调时遇到一个很隐蔽的问题:EventChannel 在 Flutter 侧如果先订阅,原生侧后注册,会导致事件丢失。最稳妥的做法是在进入扫码页之前,让原生侧先注册好 EventChannel 的 onReceive,再通知 Flutter 侧开始订阅,顺序不能反。
4. 踩坑记录:从报错到修复的全过程
4.1 OpenHarmony 画面渲染异常与 Texture 不兼容
我在第一次跑通相机预览时,遇到画面严重撕裂、颜色偏绿、偶尔黑屏。这个问题非常典型,尤其在 OpenHarmony 的模拟器上更容易触发,但真机偶尔也有。
排查过程比较曲折。一开始我以为是相机参数设置不合理,试了各种分辨率组合都没用。后来单独测试 Texture 控件渲染静态图片,发现正常,于是把问题缩小到 Texture 与相机的 Surface 绑定环节。
最后定位到原因:OpenHarmony 的 Texture 控件对 Buffer 格式支持不完整。默认创建出来的 Surface 是 RGBA 格式,但相机输出的预览帧是 YUV,两者没对齐就出现了花屏。
解决办法是在原生侧创建 Surface 时,显式指定格式为 YUV:
typescript复制let surface = await surface.createSurfaceSync({
width: 1280,
height: 720,
pixelFormat: surface.PixelFormat.YUV_420_888
})
同时,Flutter 侧的 Texture 在创建时也要同步指定纹理格式。如果用的是旧版 flutter_flutter 分支,可能没有暴露这个参数,那就需要手动改动引擎代码,工作量会大不少。建议一上来就拉最新的 OpenHarmony 分支,省很多事。
4.2 权限申请成功后相机仍黑屏
权限申请成功、相机初始化也没有报错,但预览画面全黑。这个问题我查了很久,最后定位到是生命周期绑定问题。
我在代码里是在 onPageShow 的时候初始化相机,但 Flutter 页面生命周期和原生的 UIAbility 生命周期并不是完全同步的。如果相机在手势侧还没有完全可见时就启动预览,底层 Surface 的 isAvailable 状态还没就绪,黑屏就很容易出现。
解决办法是在原生侧监听 Surface 的 surfaceAvailable 回调,等事件触发后再调用 session.start()。而不是在创建完相机会话后立刻启动。
4.3 OpenHarmony x86 模拟器上的扫码报错
如果你用的是模拟器,尤其是 OpenHarmony x86 架构的模拟器,扫码时可能会遇到 libzxing.so 加载失败的问题。因为很多预编译的 ZXing 库只提供了 ARM 架构的版本,x86 模拟器上没法直接用。
我的解决办法是:在 build-profile.json5 中只保留 arm64-v8a 作为目标 ABI。这样应用将只在支持 ARM 的模拟器或真机上运行,x86 模拟器不再启动安装。如果你的场景必须要支持 x86 模拟器,就得自己去编译一个 x86 版本的开源库,这个工作量看库的复杂程度,通常一两天能搞定。
但我不推荐,实际扫码这样功能最好都在真机上测,画面颜色、对焦速度、弱光环境这些,模拟器模拟不了。
4.4 识别结果乱码与编码问题
二维码内容包含中文时,识别返回的字符串偶尔会变成乱码。这个问题其实是 ZXing 的经典坑:它默认用 ISO-8859-1 解码。如果二维码编码时用的是 UTF-8,ZXing 需要靠 CharacterSetECI 来判断编码,但有些二维码生成工具不写这个标识。
解决办法是在创建扫码配置时强制指定解码字符集:
typescript复制let hints = new Map<EncodeHintType, Object>()
hints.set(DecodeHintType.CHARACTER_SET, 'UTF-8')
hints.set(DecodeHintType.TRY_HARDER, true)
同时,在解析结果时,如果发现返回的是乱码,可以手动做一次二次解码。我在实践中发现,拿 ByteArray 先按 UTF-8 解码一次,成功率非常高。
4.5 Flutter 的 Gradle 插件模式问题
这个坑出现在用 DevEco Studio 打开 Flutter 工程时,构建阶段报出:
text复制You are applying Flutter's main Gradle plugin imperatively using the apply script method, which is removed. Use the plugins block in settings.gradle
原因是 OpenHarmony 的 Flutter 分支要求使用新的插件加载方式。如果你是从旧项目升级过来的,或者直接复制了网上的老配置,就会触发这个报错。
修复方式已经在前文提到,就是改 settings.gradle 用 plugins DSL。除此之外,还要同步检查根目录的 build.gradle,如果里面还有旧式的 apply from: "$flutterRoot/packages/flutter_tools/gradle/app_plugin_loader.gradle",也要一并删掉。
4.6 OpenHarmony 画面渲染异常的另一层原因:线程模式
还有一次渲染异常,根因是相机预览回调跑在非 UI 线程,但我在回调里直接更新了 Surface 相关状态。OpenHarmony 的 UI 框架规定 ArkTS 侧的 UI 操作必须在主线程执行,否则会触发渲染器断言,表现出来就是画面闪烁。
修复方式是在回调里切换线程:
typescript复制import taskpool from '@ohos.taskpool'
@Concurrent
function frameUpdate(data: ArrayBuffer) {
// 处理帧数据,做格式转换和识别
}
imageReceiver.on('imageArrival', () => {
let buffer = getBuffer()
taskpool.execute(frameUpdate, buffer)
})
当然也可以使用 Emitter 或直接通过 EventHub 把事件切回主线程。我的经验是,帧数据处理这种 CPU 密集操作,丢到 taskpool 里最合适,既不阻塞 UI,也能发挥多核优势。
5. 性能调优与识别率提升
5.1 相机参数对识别率的影响
扫一扫的识别率,很大程度上在相机初始化阶段就注定了。参数设置不合适的表现是:小屏的二维码扫码很慢,或者要凑得很近才能扫出来。下面是几组关键参数的经验值:
| 参数 | 推荐值 | 作用 |
|---|---|---|
| 预览分辨率 | 1280x720 | 平衡清晰度和性能 |
| 对焦模式 | CONTINUOUS_AUTO | 让码在移动时也能快速对焦 |
| 曝光补偿 | 0 | 保持默认,避免过曝 |
| 扫码帧间隔 | 300ms | 间隔太短导致 CPU 占用过高 |
关于预览分辨率多说一句:不要盲目上 4K,扫码识别不是拍照,不需要那么高的清晰度。720P 足够,而且帧回调的压力小很多,CPU 占用至少省一半。
5.2 图像裁剪与 ROI 区域优化
大多数扫码场景,码会出现在屏幕的中间区域。如果每次都将整帧图像交给 ZXing 去识别,不仅慢,还容易在画面边缘出现误识别。我在原生侧加了 ROI 区域限定,只把相机画面的中心 60% 区域传给识别库:
typescript复制let offsetX = (size.width - roiWidth) / 2
let offsetY = (size.height - roiHeight) / 2
let roiBuffer = cropImage(buffer, offsetX, offsetY, roiWidth, roiHeight)
这个优化做完,识别速度提升非常明显,从平均 200ms 左右降到了 80ms 以内。
5.3 弱光环境的补光策略
到了弱光环境,识别率下滑是必然的。我在项目里增加了一个简单的策略:连续 5 帧识别失败且平均亮度低于阈值时,自动打开闪光灯。实测在暗光场景下,扫码成功率提升将近 30%。
判断亮度的方法,可以直接取帧数据的 Y 通道均值。YUV 中 Y 就是亮度分量,计算量也不大,每帧采样部分像素就行,不用全量计算。
5.4 Flutter 侧状态管理与内存优化
扫码页通常不会关闭相机,所以 Flutter 侧需要注意避免不必要的 setState 导致重绘。我用了 ValueNotifier 来管理识别进度和结果状态,只在结果真正变化时才通知刷新。
另外,相机是重量级资源,离开页面时一定要在 dispose 中释放:
dart复制@override
void dispose() {
_channel.invokeMethod('releaseScan');
super.dispose();
}
原生侧 releaseScan 中做 session.stop()、cameraInput.close()、imageReceiver.release() 的完整释放,不然就会出现“第二次进入页面黑屏”的诡异 bug。这个 bug 我在项目里踩过一次,准确说是因为只 stop 了 session 没有 close input,导致相机设备一直被占用。
6. 打包与多设备适配经验
6.1 生成 HAP 并签名
OpenHarmony 的应用打包产物是 .hap 文件,不同于 Android 的 APK。在 DevEco Studio 中,默认通过 Build > Build Hap(s)/APP(s) 就可以生成。但如果你是用命令行方式集成 Flutter,建议直接使用 hvigorw 命令:
bash复制./hvigorw assembleHap --mode module -p product=default
签名配置在 build-profile.json5 里,Debug 和 Release 需要不同的签名文件。这里提醒一点:如果没配置签名,HAP 只能在本地设备用调试模式安装,要分发给其他设备,必须配置正式签名。
6.2 不同屏幕尺寸下的扫码框适配
扫码页的取景框在不同设备上看起来差异很大。我在 Flutter 侧用 LayoutBuilder 获取可用区域,然后按比例设置取景框大小:
dart复制LayoutBuilder(
builder: (context, constraints) {
final scanAreaWidth = constraints.maxWidth * 0.7;
return Container(
width: scanAreaWidth,
height: scanAreaWidth,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(12),
),
);
},
)
同时把 ROI 区域同步设置成同样的比例,保证用户看到的取景框和实际识别区域是一致,不然会出现“框内扫不出、框外反而识别成功”的困惑体验。
6.3 真机与模拟器的表面差异
最后还是要强调一下:OpenHarmony 的模拟器,尤其是 x86 架构的,在图形渲染、相机模拟、传感器模拟上都有一定失真。我在模拟器上测扫码,出现的问题是画面偏暗、对焦缓慢,一度怀疑是自己相机参数写错了。后来换到真机,表现完全正常。
如果条件允许,尽量从一开始就用真机做开发调试。模拟器只用来验证业务流程和 UI 布局,性能和识别率这类问题,不要指望模拟器能给结论。
另外,不同厂家的 OpenHarmony 设备在相机实现上也有差异。有些设备的 CameraManager 在 createSession 时要求先 addInput 再 addOutput,有些则相反,顺序不对会直接 crash。我在适配时把这个逻辑做成可配置的,并在初始化之前先根据设备型号做一次判断。
7. 可以继续优化的方向
这次扫一扫功能上线后,我复盘了一下,觉得还有几个方向值得继续做:
- 接入更强大的码识别库:目前用的 ZXing 对畸形码、彩色码的识别率一般,后续可以考虑接 OpenHarmony 的 Scan Kit 或者华为统一的码识别服务。Scan Kit 在多码识别、离线识别、遮挡识别上的表现很出色,同时支持更多码制。
- 提升帧处理并行度:目前帧处理是单线程串行执行,在低端设备上扫码会有明显延迟。可以考虑用流水线架构,把“取帧-转换-识别”三个步骤并行化,进一步提升吞吐。
- 增加扫码历史记录与常用码管理:这个属于产品层面的迭代,功能本身不复杂,但需要考虑数据存储的选型,在 Flutter 侧用 shared_preferences 或者数据库存储都行。
不过这些都是后话。当前这个版本的扫一扫已经能在 OpenHarmony 设备上稳定运行,打开页面 500ms 内完成相机预览启动,扫码成功响应时间平均 80ms,基本达到了和 Android 端相同的体验水平。
我个人在实际操作中最大的体会是:在 OpenHarmony 上做 Flutter 开发,最大的门槛其实不是 Flutter 本身,而是对 OpenHarmony 这套新生态的熟悉程度。它的权限模型、相机框架、生命周期管理,都和 Android 有微妙差异。但只要能用好平台通道这根“桥”,把 Flutter 的优势和鸿蒙的能力真正连接起来,开发效率并不会比在 Android 上低多少。
最后再分享一个小技巧:如果你在开发中遇到奇怪的渲染问题,先去检查 Texture 控件的 Buffer 格式;如果遇到相机初始化失败,先去看权限申请回调里的 authResults 值,0 才是允许。这两个地方占了我在这次实战中排查时间的六成。希望这篇文章能帮你把这些坑提前绕开。
