文档扫描在移动端看起来像一个“打开相机、拍几页、返回文件”的小功能,实际实现却分成了两条完全不同的技术路线:Android 侧依赖 ML Kit Document Scanner,iOS 侧依赖 VisionKit。cunning_document_scanner 把这些平台差异包装成一套 Dart API。把它带到鸿蒙时,最容易踩的坑不是写不出 ArkTS 插件注册,而是把“接口能调用”误认为“扫描能力已经完成”。
本文以 cunning_document_scanner 3.0.3 为例,记录我如何保留原有 Dart API、补齐 Flutter OH 插件入口、让鸿蒙端明确返回能力边界,并在 HarmonyOS API 26 模拟器上完成构建、安装、启动和按钮调用验证。当前分支可以运行 Demo,但鸿蒙系统扫描器 UI 尚未接入,所以模拟器最终显示 UNSUPPORTED。这个结果是适配过程的一部分,也是比返回空列表更可靠的结论。
适配仓库: oh-flutter/cunning_document_scanner
适配分支: feat/ohos_cunning_document_scanner_3.0.3
适配 TAG: 3.0.3-ohos.1
一、先看最终效果
图 1:HarmonyOS API 26 模拟器启动 Demo。页面显示插件返回的 OpenHarmony 3.47.4,状态为等待操作。
图 2:点击“扫描文档”后的真实结果。ArkTS 返回 UNSUPPORTED,Dart 层把错误码和原因显示在页面上。
| 验证点 | 实测结果 | 证据 |
|---|---|---|
| Flutter OH 3.47 工程构建 | 通过 | entry-default-signed.hap |
| 签名、安装和启动 | 通过 | 图 1 |
getPlatformVersion | 返回 OpenHarmony 3.47.4 | 图 1 |
getPictures | 返回 UNSUPPORTED | 图 2 |
cleanCache | 返回成功,无异常 | 图 3 |
| 鸿蒙扫描器 UI | 尚未接入 | 适配边界 |
为了确认缓存清理入口也可以单独复现,我又在模拟器上点击了“清理扫描缓存”按钮:
图 3:调用 cleanCache 后返回成功,没有伪造扫描文件残留。
二、这个库原来是怎么工作的
上游包的公共入口是 CunningDocumentScanner。它的 getPictures 支持页数上限、来源(相机或相册)、Android 的扫描模式、iOS 的扫描选项,以及是否直接返回 PDF。Dart 层通过名为 cunning_document_scanner 的 MethodChannel 把参数传给原生端。
Dart CunningDocumentScanner
-> MethodChannel('cunning_document_scanner')
-> Android ML Kit / iOS VisionKit
-> 图片路径列表或 PDF 路径
读源码时有一个细节值得单独记下来:getPictures 对“取消”的语义做了归一化,Android 历史上把取消报告为空列表,iOS 报告为 null,插件在 Dart 层把两者都转换成 null。这意味着业务代码里 null 的含义是“用户取消了调用”,而不是“平台不支持”。本次适配把这个区别原样保留:取消走 null,平台能力缺失走标准错误。
// cunning_document_scanner 3.0.3 的取消语义(上游源码)
final List<dynamic>? pictures;
try {
pictures = await _channel.invokeMethod<List<dynamic>>('getPictures', {
'noOfPages': noOfPages,
'isGalleryImportAllowed': isGalleryImportAllowed,
'scannerSource': resolvedSource.methodChannelValue,
'androidScannerMode': androidScannerMode.methodChannelValue,
'asPdf': asPdf,
if (iosScannerOptions != null)
'iosScannerOptions': iosScannerOptions.toMap(),
});
} on PlatformException catch (e) {
throw CunningDocumentScannerException(
e.message ?? 'The document scan failed',
code: e.code,
);
}
// Android 空列表和 iOS null 都归一为 null:取消不等于失败
if (pictures == null || pictures.isEmpty) {
return null;
}
OHOS 端没有把 Android 的 ML Kit 或 iOS 的 VisionKit 直接搬过来,也没有伪造一个空的扫描结果,而是把当前能力状态通过标准错误返回给 Dart 层。
三、实际环境
Flutter 鸿蒙环境搭建不在本文重复展开,直接参考 Flutter OH 环境搭建指南。
| 组件 | 版本 |
|---|---|
| Flutter OH | 3.47.4-ohos-1.0.4 |
| Dart | 3.13.3 |
| DevEco Studio | 26.0.0 Release |
| HarmonyOS SDK | API 26 |
| 测试设备 | DevEco 模拟器 Pura X View(HarmonyOS 7.0.0.105,API 26) |
本次验证使用 DevEco Studio 26.0.0 自带的 Pura X View 模拟器,系统版本与目标真机同为 HarmonyOS 7.0.0.105 / API 26;HAP 的签名、安装与启动链路和真机一致。
| 目标库 | cunning_document_scanner 3.0.3 |
flutter --version
hdc list targets
四、确认上游版本并同步到 AtomGit
上游包地址是 GitHub vicajilau/cunning_document_scanner,版本 3.0.3 使用 MIT License。适配前我在 AtomGit 目标组织中搜索了同名库,没有找到可以直接使用的 OHOS 实现,因此从上游基线开始增加平台目录。
仓库初始适配提交为 1f21bd6,完成后分支最新提交为 c212fdc。
git clone https://atomgit.com/oh-flutter/cunning_document_scanner.git
cd cunning_document_scanner
git log --oneline -5
git switch feat/ohos_cunning_document_scanner_3.0.3
git remote -v
先确认仓库、版本和远程地址,再开始改代码,后面出现编译问题时才容易区分是上游问题、工具链问题还是本次改动引入的问题。
五、创建适配分支并检查工程结构
git switch -c feat/ohos_cunning_document_scanner_3.0.3
git branch --show-current
关键目录如下:
cunning_document_scanner/
├── lib/ # 原有 Dart API 和 MethodChannel
├── ohos/
│ ├── index.ets # OHOS 插件导出入口
│ └── src/main/ets/components/plugin/
│ └── CunningDocumentScannerPlugin.ets # MethodChannel 实现
├── example/lib/main.dart # 上机验证 Demo
├── example/ohos/ # Flutter OH 宿主工程
├── pubspec.yaml # 声明 ohos 平台
└── README.OpenHarmony_CN.md # 鸿蒙适配说明
pubspec.yaml 中的关键声明是:
flutter:
plugin:
platforms:
ohos:
pluginClass: CunningDocumentScannerPlugin
pluginClass 必须和 ohos/index.ets 导出的类名一致,否则 Dart 端能编译,运行时却不会注册插件。
六、适配过程:先对齐契约,再写 ArkTS
我先从 lib/cunning_document_scanner.dart 和 lib/cunning_document_scanner_method_channel.dart 读取现有接口,没有改动公开方法名和参数:
| Dart 方法 | 参数 | OHOS 本次策略 |
|---|---|---|
getPlatformVersion | 无 | 返回 OpenHarmony 3.47.4 |
getPictures | 页数、来源、扫描模式、iOS 选项、asPdf | 返回 UNSUPPORTED |
cleanCache | 无 | 安全无操作,返回成功 |
这里有一个容易忽略的区别:null 是“调用完成但没有结果”的正常数据形态,在这条库的语义里专指用户取消扫描;UNSUPPORTED 是“当前平台没有这项能力”的错误形态。两者混用会让业务层把“鸿蒙尚未适配”误认为“用户取消”。
OHOS 插件骨架使用 Flutter OH 的 FlutterPlugin、MethodCallHandler 和 MethodChannel:
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel?.setMethodCallHandler(null);
this.channel = new MethodChannel(
binding.getBinaryMessenger(),
'cunning_document_scanner',
);
this.channel.setMethodCallHandler(this);
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
this.channel?.setMethodCallHandler(null);
this.channel = null;
}
注册时使用的通道字符串必须和 Dart 层完全相同。解绑时清理 handler,避免引擎重复创建后出现重复回调。
核心方法分支如下:
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case 'getPlatformVersion':
result.success('OpenHarmony 3.47.4');
return;
case 'cleanCache':
result.success(null);
return;
case 'getPictures':
result.error(
'UNSUPPORTED',
'Document scanning is not available on OpenHarmony yet.',
null,
);
return;
default:
result.notImplemented();
}
}
result.error 会被 Dart MethodChannel 捕获为 PlatformException,插件 Dart 实现再把它转换成 CunningDocumentScannerException 并保留 code。Demo 只捕获这个异常类型并展示 code 和 message,所以运行页面能看到 UNSUPPORTED,而不是一串难以判断的空值。
cleanCache 是这条库里少有的可以直接映射的能力:OHOS 端当前没有插件私有缓存文件需要清理,返回成功可以保证调用方生命周期代码不必为平台差异写分支。
七、构建配置与签名
示例工程的目标 SDK 分为 compatibleSdkVersion 和 targetSdkVersion,两者都要对齐到 26,才能和 @ohos/flutter_ohos 的最低要求一致:
{
"name": "default",
"compatibleSdkVersion": "26.0.0",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS"
}
为了让本机 DevEco 调试签名可以直接为设备安装服务,我把 Demo 的 AppScope/app.json5 统一成 com.example.flutter_oh_demo,并在 example/ohos/build-profile.json5 中配置调试签名。证书、p12、p7b 和密码没有提交到仓库,读者应在自己的 DevEco Studio 中生成调试签名。
八、通过 AtomGit 引入适配版
在另一个 Flutter OH 应用中使用时,建议锁定 TAG:
dependencies:
cunning_document_scanner:
git:
url: https://atomgit.com/oh-flutter/cunning_document_scanner.git
ref: 3.0.3-ohos.1
flutter pub get
然后在 pubspec.lock 检查 source: git、AtomGit 地址和 resolved-ref,确认没有解析到 pub.dev 的同名版本。
九、构建、安装和设备验证
cd example
flutter pub get
flutter analyze
flutter build hap --debug
hdc install -r build/ohos/hap/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.example.flutter_oh_demo
构建输出为 ✓ Built build/ohos/hap/entry-default-signed.hap.,随后 hdc 报告安装成功并启动 Ability。进入页面后先看到图 1,再点击“扫描文档”,得到图 2;点击“清理扫描缓存”,得到图 3。
flutter analyze 在示例工程和插件本体都通过。设备验证覆盖的是插件注册、通道连通、版本查询和两条方法的返回语义,不代表扫描 UI 已经可用。
十、适配结果和当前边界
| 层次 | 已完成内容 | 尚未完成内容 |
|---|---|---|
| Dart API | 保留原方法、模型和异常类型 | 无 |
| MethodChannel | 通道名和方法名全部对齐 | 无 |
| OHOS 插件 | 注册、版本查询、缓存清理、错误回传 | 真正的扫描 UI |
| Demo | API 26 模拟器启动和按钮调用 | 拍照、裁剪、生成图片/PDF |
| 文档 | README.OpenHarmony_CN.md、CHANGELOG | 无 |
Android 的 ML Kit 和 iOS 的 VisionKit 不能通过改一个平台声明就变成鸿蒙能力。下一步如果要完成真正扫描,需要选定 HarmonyOS 的相机、图像处理和文件导出 API,补充相机权限,处理前后台切换和用户取消,并为图片、PDF 两种返回结果分别做设备回归。当前提交明确告诉使用者“这个能力还没有接入”,比让业务误以为扫描成功更容易维护。
十一、FAQ
Q1:为什么 HAP 构建成功,页面仍然显示不支持?
- 现象: HAP 可以签名、安装并启动,但点击扫描后显示
UNSUPPORTED。 - 原因: 当前 OHOS ArkTS 端只完成插件注册、版本查询和错误语义,没有接入系统扫描器 UI。
- 解决方法: 保留明确错误,后续接入真正的 HarmonyOS 扫描实现后再替换对应分支。
- 验证结果: 图 2 证明错误从 ArkTS 经 MethodChannel 到达 Dart 页面。
Q2:为什么不能直接返回空列表?
- 现象: 如果 OHOS 端返回空列表,Dart 业务会把
getPictures结果当成用户取消。 - 原因: 这条库把空列表归一化为
null,null专指“用户取消”。 - 解决方法:
getPictures统一调用result.error('UNSUPPORTED', ...),把能力缺失交给异常通道。 - 验证结果: Demo 显示错误码和英文原因,状态可追踪。
Q3:cleanCache 为什么可以直接返回成功?
- 现象: 鸿蒙端没有真实扫描文件,但调用
cleanCache不报错。 - 原因: 插件在 OHOS 端没有写入私有缓存,清理动作是安全无操作。
- 解决方法: 返回
result.success(null),保证调用方的释放代码在所有平台行为一致。 - 验证结果: 图 3 显示调用完成后页面状态正常,没有异常。
十二、总结
这次适配保留了 cunning_document_scanner 3.0.3 原有 Dart 接口和 cunning_document_scanner 通道,在 OHOS 侧增加了标准 FlutterPlugin 注册、版本查询、缓存清理和 getPictures 的明确错误回传。Demo 通过 Flutter OH 3.47.4 构建成签名 HAP,并在 HarmonyOS API 26 模拟器上完成安装、启动和按钮验证。
当前成果是“可构建、可安装、接口行为清楚”的鸿蒙适配基线,不代表鸿蒙扫描器已经完成。真正接入扫描能力时,应继续补充系统 API、权限、文件导出、取消流程和图片/PDF 结果回归。适配代码和说明都在 AtomGit 适配分支,复现时建议锁定 3.0.3-ohos.1 TAG。
十三、参考资料和仓库地址
- 适配仓库主页
- 适配分支
- 适配 TAG
- 最终提交 c212fdc
- OHOS 插件导出入口
- ArkTS MethodChannel 实现
- Dart 公共入口
- 鸿蒙适配说明
- pub.dev 包页面
- pub.dev 3.0.3 版本页
- 上游 GitHub 仓库
- 上游 LICENSE
- Flutter 官方插件与平台通道文档
- Flutter OH 鸿蒙 SDK 仓库
- HarmonyOS 开发者官方文档
欢迎加入CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2502_93915572/article/details/168128075



