1. 项目概述与技术选型思考
1.1 这个项目到底在做什么
最近在做一款基于 Flutter for OpenHarmony 的二维码扫描 App,核心功能就两条:一是调用摄像头实时扫码,二是能把短信内容生成二维码让别人扫。其中短信二维码这块,不是简单把一行文字变成图片,而是要按照短信协议去构造内容,再落成二维码图案,最终要能被主流手机相机直接识别、点开就跳转短信发送页面。这个链路听起来不复杂,但落到 OpenHarmony 平台上的时候,从工具链到渲染引擎,处处都有以前在 Android 上没踩过的坑。
这个项目适合谁看?正在做 OpenHarmony 应用开发、想用 Flutter 跨端复用的团队会最有共鸣;想了解二维码生成和解码底层原理、或者想学 Flutter 状态管理(尤其是 Provider 用法)的开发者,也能从中捞到不少可直接抄的代码。我把整个项目的拆解、选型、编码过程和踩坑记录都整理在这篇里了。
项目本身的技术栈是 Flutter + OpenHarmony SDK,扫码部分用了摄像头预览流加二维码解码库,短信二维码生成部分用 Dart 侧二维码编码库直接产出矩阵数据,再通过 Canvas 绘制成图片。UI 层用到了 Provider 做跨页面状态共享,毕竟扫码需要把结果带回上一个页面,历史记录列表还得瞬间刷新,总不能全靠路由传参硬怼。
1.2 为什么选 Flutter 而不是原生或 uni-app
在 OpenHarmony 上开发,第一反应肯定是官方主推的 ArkTS 加 ArkUI。但如果你手里已经有一套成熟的 Flutter 业务代码,或者团队主力是 Dart 开发者,那从零迁移到 ArkTS 的成本反而更高。我的选择逻辑是这样的:Flutter 的渲染引擎是自绘的,UI 一致性跟平台关系不大,OpenHarmony 适配分支上跑起来,界面效果和 Android/iOS 几乎没差;而 ArkTS/ArkUI 虽然原生,但生态和第三方库还在爬坡期,很多组件都要自己造轮子。
那有人会问,RN 和 uni-app 不是也能跨端吗?单看包体积和渲染性能,Flutter 在移动端依然是第一梯队,而且 Dart 的单线程事件循环模型在做相机帧流处理、二维码解码这种高频任务时,配合 Isolate 能绕开 JS 引擎的阻塞问题。OpenHarmony 的 Flutter 分支目前已经能跑主流插件,camera 这类涉及底层能力的,也有对应适配层可以接。所以最终我选了 Flutter,并且把项目直接跑在了 OpenHarmony 真机上,下面的内容全部基于真机实践。
2. 环境准备与 OpenHarmony 适配要点
2.1 工具链与 SDK 版本搭配
这次项目用的 Flutter 仓库是 OpenHarmony 官方维护的 fork 分支,不是谷歌原版 Flutter SDK。这个分支最大的不同是构建目标从 AAR/Android 工程变成了 HAP/OpenHarmony 工程,所以环境变量、构建命令和依赖仓库都要跟着调。我的版本组合如下:
- Flutter SDK:OpenHarmony 分支(基于 Flutter 3.x 代码基线)
- OpenHarmony SDK:API 9 及以上
- 开发工具:DevEco Studio 用于构建 HAP 和做签名
- 包管理:ohpm 用来安装 OpenHarmony 原生依赖,pub 用来装 Dart 包
这里有个比较坑的地方:Flutter 分支要求你在flutter doctor里能识别到 OpenHarmony SDK 路径,否则创建项目时不会生成原生壳工程。我第一次搭建时就是 Flutter 环境配好了,OpenHarmony SDK 装好了,但两边互不认账,最后检查发现是需要手动在环境变量里加OHOS_SDK_HOME指向 SDK 根目录,同时在项目里维护一份local.properties指定hwsdk.dir。
另外必须提一下 OpenHarmony 的兼容性认证。设备要跑带 Flutter 引擎的 HAP,涉及到底层运行时权限和系统 API 调用,建议在开发早期就对照官方兼容性测试要求,把需要申请权限的模块提前列出来。相机、存储这些高风险权限一旦在认证环节出问题,后面返工成本很高。我们项目在扫码功能上主动申请了相机权限,短信生成只涉及图片保存,存储权限也要提前配好。
2.2 OpenHarmony 上 Flutter 应用的基本形态
在 OpenHarmony 上,Flutter 应用不是直接构建出一个 .so 就完事,而是以“原生壳 + Flutter 引擎”的方式存在。项目里会有一个 OpenHarmony 工程作为入口 Ability,它负责生命周期管理,Flutter 引擎挂载到它的窗口上完成渲染。这跟 Android 里 FlutterActivity + FlutterFragment 的思路一脉相承。
实际打包产物里,Flutter 引擎相关代码会编译成 .so,业务代码仍然通过 AOT 或 JIT 方式打包到 HAP 里。如果你的 App 需要被其他 OpenHarmony 原生模块复用,那可以把 Flutter 模块打成 HAR 包引入,这就对应了搜索热词里大家常问的 Flutter AAR 概念——在 OpenHarmony 侧,把它理解为 Flutter HAR(Harmony Archive)集成方式即可。
首次跑通这个壳工程的流程大致是:先用 Flutter 分支创建项目,再用 DevEco 打开生成的 OpenHarmony 工程目录,配置签名后直接 Run。这个阶段最容易遇到编译失败,因为 OpenHarmony 原生侧和 Flutter 侧是两个独立的构建体系,你需要确保 DevEco 打包时把 Flutter 引擎的 .so 文件正确带进 HAP。实测最稳妥的做法是先跑一遍 Flutter 构建脚本,生成引擎产物,再回 DevEco 里点构建。
2.3 二维码能力在 OpenHarmony 侧的落地方案
二维码库的选型,我建议直接在 Dart 层做,而不是依赖原生 SDK 的能力。原因很简单:二维码编码和解码的算法是标准的,纯 Dart 实现的可移植性极强,OpenHarmony、Android、iOS 三端代码完全共享,不用为每个平台各写一套 platform channel。实际在项目里我用的是 barcode 库(也有类似的 qr 库),它内部实现了 QR 码编码器,能够在 Dart 侧生成包含格式信息、版本信息、纠错码字的矩阵数据,然后我用 CustomPainter 把矩阵画出来。
而摄像头扫码的解码部分,走的还是相机原生采集,再把帧数据传给 Dart 侧的解码器。这里有个性能决策要说明:如果把每一帧原始图像都往 Dart 丢,内存和耗时都顶不住。我的方案是先把相机帧按需降采样成灰度图,只把亮度数据传给解码器,这样既省内存又能显著降低解码延迟。更详细的解码链路,我会放到第 4 章里讲。
3. 短信二维码生成实现
3.1 短信二维码的内容格式:sms: 与 SMSTO: 协议
二维码本身只是一张图,真正有价值的是编码进去的内容。短信二维码要做的,就是让手机相机扫到之后直接唤起短信编辑页,并且自动填好收件人和短信内容。这背后用的是 URI 协议,两种常见写法我都实测过了:
| 协议格式 | 示例 | 效果 |
|---|---|---|
sms:电话号码 | sms:13800138000 | 唤起短信页,收件人已填,内容为空 |
sms:电话号码?body=内容 | sms:13800138000?body=你好 | 唤起短信页,收件人和内容都填好 |
SMSTO:电话号码:内容 | SMSTO:13800138000:你好 | 部分老版本手机兼容此格式,同样填好内容和收件人 |
实测中sms:格式在新版系统的兼容性最好,而且参数body要记得做 URL 编码。比如短信内容带中文、空格、& 符号,不编码的话扫码后内容会被截断。我写了一个小程序试过,用Uri.encodeComponent对内容编码之后再拼进 URI,扫出来的短信内容就完整了。这个细节别轻视,很多人生成的二维码能被识别但跳转后内容不完整,基本都是栽在这里。
另外,如果你的场景需要预填多个收件人,可以用分号分隔号码,例如sms:10086;10010?body=查话费。但要注意的是,分号在 URL 里不算安全字符,最好也统一编码一次,免得某些扫码 App 解析不同。
3.2 二维码生成的核心原理:QR 码的结构与纠错
二维码生成不是简单把字符串丢给库然后出图,底层有一套完整的编码逻辑。理解了这套逻辑,你才知道为什么有时候二维码看起来密密麻麻扫不出来,为什么同样内容换不同纠错级别清晰度完全不同。
QR 码的基础结构包括:
- 版本(Version):1 到 40,版本越高,模块数量越多,能容纳的数据量越大。版本 1 是 21×21 模块,每升一级长宽加 4,到版本 40 是 177×177。
- 纠错级别(Error Correction):L、M、Q、H 四级,L 级约可恢复 7% 的码字,M 级约 15%,Q 级约 25%,H 级约 30%。纠错级别越高,图形抗污损能力越强,但能存的内容越少。
- 编码模式:数字模式、字母数字模式、字节模式。纯数字和纯字母内容的压缩率更高,中文和特殊符号走字节模式,需要按 UTF-8 编码。
- 掩码(Mask):对矩阵做 XOR 处理,目的是让二维码图案的明暗分布尽量均匀,方便扫描识别。编码器会尝试 8 种掩码,选惩罚得分最低的一种。
短信二维码这种场景,内容本身不长,一般是版本 1 到 3 就够用了。但我在做 App 时暴露了纠错级别选项给用户,默认值是 M 级,这样既能保证二维码图案不会过于密集,也能容忍一定程度的磨损、折痕或者摄像头离得远模糊的情况。如果你的二维码会印在易磨损坏的介质上(比如贴在设备外壳贴纸上),那应该选 Q 甚至 H 级,稳定性优先。
3.3 实操代码:在 Flutter 中用 barcode 库生成短信二维码
我直接贴一段可以跑通的核心代码。这里用的是barcode库,它提供了Barcode.qrCode()工厂方法,编码后得到BarcodeMatrix,然后我通过CustomPainter把它渲染成任意尺寸的图片。
import 'package:barcode/barcode.dart'; import 'package:flutter/material.dart'; import 'dart:ui' as ui; Uint8List generateSmsQrCode({ required String phoneNumber, required String message, double size = 400, int errorCorrectionLevel = 1, }) { final content = 'sms:${phoneNumber.trim()}?body=${Uri.encodeComponent(message)}'; // 创建QR码生成器,设置默认纠错级别M final qrCode = Barcode.qrCode( errorCorrectLevel: errorCorrectionLevel, // 0=L, 1=M, 2=Q, 3=H ); // 编码成矩阵 final matrix = qrCode.encode(content); // 预留白边 quiet zone const padding = 4; final width = matrix.width + padding * 2; final height = width; // 用Canvas绘制黑白格 final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); final paint = Paint()..color = Colors.black; final cellWidth = size / width; final leftOffset = padding * cellWidth; for (var x = 0; x < matrix.width; x++) { for (var y = 0; y < matrix.height; y++) { if (matrix.get(x, y)) { // 矩阵里为true的格子涂黑 canvas.drawRect( Rect.fromLTWH( leftOffset + x * cellWidth, leftOffset + y * cellWidth, cellWidth, cellWidth, ), paint, ); } } } final picture = recorder.endRecording(); final image = picture.toImage(size.toInt(), size.toInt()); // 后续可以转成ByteData再存为PNG,或直接以Image组件展示 return imageToPng(image); }这段代码最核心的动作是qrCode.encode(content),它会把短信 URI 转成一串布尔矩阵,true 就是黑色模块。后面的绘制过程本质上是放大矩阵,没有任何魔法。如果你用的是qr_flutter这类现成 Widget 库,会更省事,但换来的代价是自定义能力弱,比如想在二维码中心加 Logo、想控制模块形状、想调整颜色渐变,它们都很难做。我自己在项目里最终选择了自绘方案,因为要给二维码加品牌角标和圆角模块样式,这些业务需求决定了不能只靠现成 Widget。
3.4 生成短信二维码的细节优化
先说说错误处理。短信内容为空时,生成出来的二维码内容是sms:号码?body=,扫出来虽然也能唤起短信页,但用户会看到一个空短信,体验很怪。我当时在生成前加了一道校验,内容和号码必须至少有一项非空,否则就提示用户。这个逻辑看着简单,却是产品上线后最容易被打回来的交互细节。
然后是二维码尺寸和留白。二维码周围一定要留白,专业叫 quiet zone,标准要求是至少 4 个模块宽度。代码里我写了padding = 4,翻译成像素后就是二维码总图的一部分。千万别为了贴边排版把白边裁掉,很多扫码失败都是二维码贴到图片边缘、识别器找不到定位图案引起的。
再提一下图片坐标系和 Canvas 的关系。绘制二维码时,尺寸参数size只是逻辑像素,如果你要保存高清图,建议直接用ui.PictureRecorder渲染到更大幅面的 Canvas 上,而不是在 Widget 里用截屏方式生成图片。截屏生成图片会受到屏幕分辨率和 DPR 影响,清晰度不稳定。我在代码里直接指定size = 400或更高(比如 800、1024),这样得到的 PNG 用于打印物料也足够清晰。
最后,加 Logo 的坑。给二维码中心加图标可以增强品牌感,但会遮挡部分数据区域,Abort 风险全靠纠错级别兜着。如果加了 Logo 还选 L 级纠错,大概率现场扫码翻车。我给用户提供的默认配置是:有 Logo 时强制 Q 级以上纠错;同时 Logo 尺寸不要超过二维码总宽度的 1/4,中心遮挡位置尽量控制在固定图案之外的区域。
4. 扫码能力与 Camera 集成
4.1 相机配置与权限申请
OpenHarmony 上的摄像头调用,如果直接写原生代码,需要走 Camera Kit 服务,涉及权限声明、设备回调、会话配置好几层。但在 Flutter 侧,我用了 camera 插件的 OpenHarmony 适配分支,这样 Dart 代码基本不用改,底层能力由插件映射到 OpenHarmony 的相机接口。使用之前必须在module.json5里申请ohos.permission.CAMERA,而且 OpenHarmony 对相机权限的管理比 Android 更严格,运行时还得再动态申请一次。
final cameras = await availableCameras(); final controller = CameraController( cameras.first, ResolutionPreset.medium, enableAudio: false, ); await controller.initialize();摄像头方向是扫码 App 一个隐藏 Bug 重灾区。OpenHarmony 设备上相机传感器的方向和屏幕方向不一定一致,预览画面经常是旋转 90 度或 180 度的,如果不做校正,用户看到的画面是躺着的,解码结果坐标对不上扫码框,扫码体验直接归零。我用的是 controller 提供的value.deviceOrientation加value.sensorOrientation做预览旋转,同时把预览画面用RotationTransition按需旋转,确保画面始终竖直。
对焦策略也要单独说。扫码场景对自动对焦的依赖很高,尤其是近距离扫码时,camera 插件默认的对焦模式经常不够果断。我的做法是启用连续自动对焦:
await controller.setFocusMode(FocusMode.auto); await controller.setExposureMode(ExposureMode.auto);如果设备支持点按对焦,还可以在扫码框里让用户点击画面触发setFocusPoint。这个细节对工业场景特别有用——贴纸上二维码很小,自动对焦容易拉到背景上,点按对焦能救回不少识别率。
4.2 解码流程与性能优化
从摄像头到识别出二维码,整条链路是这样的:
- CameraController 启动预览,通过
startImageStream或者定时抓帧拿CameraImage。 - 把
CameraImage转成解码器需要的灰度图像数据。这一步做了降采样,比如原始分辨率为 1280×720,我通常缩到 320×240 左右再解码,识别速度提升好几倍。 - 灰度图转成二维码解码器的亮度源(LuminanceSource)。
- 解码器返回结果字符串,解析出是否是
sms:协议,是的话跳转短信编辑页。
Dart 侧的二维码解码我用的是zxing2库,它封装了 ZXing 核心解码逻辑。这里重点说性能问题:startImageStream的回调频率非常高,每秒 30 帧左右,如果每一帧都做完整解码,CPU 会瞬间拉满,手机发烫掉帧。我的优化策略是加入一个简单的节流器,只有距离上一帧解码结束超过 200ms 才处理下一帧;同时维护一个失败次数计数器,连续 5 帧解码失败后自动降低抓帧频率到每秒 5 帧,直到画面变化幅度较大再恢复正常频率。
还有一个容易被忽略的点:解码运算如果放在 UI isolate,扫码页面会出现明显的掉帧卡顿。我把解码逻辑放进了独立 Isolate,通过ReceivePort通信把解码结果传回主 isolate。这样做的好处是相机预览线程和解码线程互不阻塞,用户扫码时可以明显感觉到画面流畅度上升,尤其是低端设备上效果非常显著。
final decodeIsolate = await Isolate.spawn(decodeLoop, receivePort.sendPort); void decodeLoop(SendPort sendPort) { final port = ReceivePort(); sendPort.send(port.sendPort); port.listen((message) { // 接收灰度图,执行解码,回传结果 }); }4.3 扫码框与用户体验细节
扫码框 UI 我不建议直接用截图渲染一个半透明黑色遮罩和绿线,那只是表面功夫。真正影响体验的是从“相机图像坐标”到“屏幕显示坐标”的换算关系。解码器返回的二维码位置是基于裁剪后的灰度图的,需要换算回预览画面的坐标系,再换算到屏幕坐标系,最后判断是否落在扫码框内部。
我踩过一个真实案例:扫码框画在屏幕中间偏上,二维码也显示在框内,但解码结果却经常扫到背景里的另一个二维码。原因就是坐标换算少做了一步——我直接用整帧灰度图解码,没有按扫码框区域裁剪后再解码。后来改成先根据扫码框在预览流中的位置裁出子图,再去解码,误扫率一下降到接近零。
5. 状态管理与组件通信
5.1 为什么用 Provider 管理扫码结果
扫码页面和历史记录页面之间需要共享数据:扫到一条短信二维码内容,我要立刻跳转回首页,同时在历史记录里追加一条新记录;用户从历史记录里点击某条记录,要能反向解析出来并再次生成二维码。这个场景如果用setState各管各的,代码很快就会乱成一锅粥。我在项目里用 Provider 做全局扫描历史和配置项的托管,理由是它轻量、官方维护、心智负担小,对 OpenHarmony 上的性能占用也可控。
对比之下,Bloc 和 Riverpod 不是不好,而是配重不同。一个扫码工具类的 App,核心状态就是“历史记录列表”、“当前配置项”、“最后一次扫码结果”,用 ChangeNotifier 足够覆盖,不必要引入繁琐的代码生成。Provider 的好处在于,它对 Dart 开发者来说几乎没有学习曲线,而且调试时能看到notifyListeners被谁触发的调用栈,比 EventBus 的满天飞事件好查得多。
5.2 Provider 核心用法实战
我在项目里的状态类是这么写的:
class ScanHistoryModel extends ChangeNotifier { List<ScanRecord> _records = []; ScanRecord? _lastResult; List<ScanRecord> get records => List.unmodifiable(_records); void addRecord(ScanRecord record) { _records.insert(0, record); _lastResult = record; notifyListeners(); } }页面里通过Consumer<ScanHistoryModel>监听变化,历史列表实时刷新。这里有个细节:列表数据在notifyListeners后如果重建得太过频繁,会导致滑动中的列表跳变。我的建议是历史列表只用records.length变化作为监听触发条件,而扫码结果浮层则单独用lastResult的变化驱动,两者分开监听,避免一次扫码把整个列表和浮层都重建一遍。
跨组件通信方面,我用了三种手段:
- 同页面局部状态:直接
setState,比如扫码框动画。 - 跨页面共享数据:Provider,比如历史记录。
- 一次性事件传递:路由参数,比如用户点开历史记录详情。
我当时看到很多帖子在问 Flutter 组件通信到底用什么,其实没有银弹。同一个页面内兄弟组件要通信,最简单的是把回调函数传下去;跨模块共享数据用 Provider 这类 InheritedWidget 封装;全局一次性消息用 EventBus 也不是不行,但你要能承受“事件无法确定最终有没有被消费”的心智负担。我这边的原则是:能用参数传递就不上全局状态,能用局部状态就不用 Provider,乱用全局状态只会让 bug 更难追踪。
5.3 组件通信与回调的边界
Provider 解决的是“共享”,但很多场景其实只需要“通知”。比如扫码框页面里,相机启动完成后要通知顶栏按钮启用闪光灯开关,这个用回调就很自然:
CameraPreviewWidget( onCameraReady: () { setState(() => _flashEnabled = true); }, )很多新手容易一上来就迁出三四个 Provider,结果页面刷新时中间的无关 Widget 也跟着 rebuild。我在项目里给团队定了个规矩:真正需要持久化的数据,或者跨 Tab 共享的数据才进 Provider;页面内的瞬时状态一律留在页面级 State 里。这也是为什么我的扫码页看起来用了 Provider,但结构性状态并没有全部堆进去。
6. 常见问题与排查技巧实录
6.1 Flutter 运行崩溃:dart_vm_initializer 报错
搜索热词里出现了flutter/runtime/dart_vm_initializer.cc(41)这种报错,我也真遇到过。这类错误本质上是 Flutter 引擎在初始化 Dart VM 时失败,常见原因有三类:Dart 侧缺失依赖库文件、原生壳工程没有正确加载 Flutter 引擎资源、或者 AOT 产物和当前平台架构不匹配。在 OpenHarmony 上最常见的是 HAP 打包时没有把 arm64-v8a 对应的 Flutter .so 带进去。
排查思路是先分两层看:如果一打开 App 就瞬间崩溃,优先检查引擎和 .so 是否正常;如果崩溃发生在使用特定功能时,优先检查是不是动态库在运行时加载失败。可以先用flutter build hap --debug构建一版,再在 DevEco Studio 里看 crash 日志的 native stack。大多数时候,恢复方式是清理本地的build目录、重新生成壳工程文件、重新执行flutter pub get,三步齐下就能解决。
6.2 Impeller 渲染引擎在 OpenHarmony 上的启用情况
Impeller 是 Flutter 新一代渲染引擎,替代旧 Skia 后能解决帧率卡顿问题。在 Android 上有不少机器已经默认启用,但在 OpenHarmony 的 Flutter 分支上,Impeller 的适配并不彻底。我实测发现,OpenHarmony 上开启 Impeller 后,部分设备会出现文字模糊或异常闪烁的现象,尤其是在频繁绘制大图(比如全屏二维码预览)的时候更容易触发。
如果你的项目在 OpenHarmony 上遇到渲染异常,最直接的排查手段是用命令行参数禁用 Impeller:
flutter run --no-enable-impeller如果这种方式能解决,说明问题就出在 Impeller 的 OpenHarmony 后端不完善,而不是业务代码本身。目前我的选择是 OpenHarmony 发布包默认走旧的 Skia 渲染路径,等官方分支把 Impeller 后端的 GPU 队列和纹理回收逻辑稳定了再切过去。
6.3 下拉刷新失效与列表滑动冲突
历史记录页我加了下拉刷新(RefreshIndicator),结果运行起来经常出现“列表到底了往下拽不出刷新指示器”的问题。原因很清楚:列表内容太少时,ListView 没有填满屏幕,默认physics不允许滚动,RefreshIndicator 收不到触发手势。
解决办法是在 ListView 上强制开启滚动:
ListView( physics: const AlwaysScrollableScrollPhysics(), )另外,如果列表嵌套了横向滑动组件或者可缩放组件,手势竞技场会优先匹配子组件的滑动,导致下拉刷新响应变慢。我后来在扫码历史记录页做了一个横向滑动的时间筛选栏,这个栏目里滑动的 GestureDetector 会和 RefreshIndicator 抢手势,最终我通过给筛选栏设置behavior: HitTestBehavior.opaque加上对纵向滚动手势不做处理,才解决冲突。
这类问题不如编译报错显眼,但实际开发中出现频率极高,建议大家在设计 UI 时就把手势冲突考虑进去。
6.4 Flutter AAR 集成到原生工程时的 Gradle 插件问题
热词里有人问you are applying flutter's main gradle plugin imperatively using the apply这个报错,这是把 Flutter module 以 AAR 方式集成到既有原生工程时,因为新建工程默认用的是新版本 Gradle 插件解析方式,而你的原生工程还在用老式apply写法导致的。
在 OpenHarmony 语境里,类似的问题会发生在“既有 OpenHarmony 工程接入 Flutter 模块”的场景。我的建议是直接统一使用 plugins DSL 方式,不要混用apply。具体可以看原生工程的settings.gradle里是否正确声明了 Flutter SDK 路径和 module 依赖,OpenHarmony 构建链路里还要保证 ohpm 的仓库配置和 Flutter 产物目录在同一个项目结构下。这类问题排查起来头绪多,但从业者最重要的经验是:不要在一个工程里维护两套插件加载逻辑,能统一就统一。
6.5 新建项目跑不起来的典型原因
很多人在 OpenHarmony 上刚创建完 Flutter 项目,直接跑就报错。我遇到的典型场景是:创建项目后没有执行flutter pub get,导致.dart_tool缺失,编译器找不到包;或者是 DevEco 的 SDK 版本和 flutter 分支要求的 API 不匹配;再或者就是签名配置没做,设备上安装时报 INSTALL_PARSE_FAILED。
我的建议是:创建项目后第一件事跑flutter pub get,然后再用 DevEco 打开工程;真机连接后先跑一个最小 Demo 验证设备链路正常,再逐步加入 camera、Provider 这些依赖。不要一上来就堆功能,否则出了问题你会分不清是设备问题、依赖问题还是业务代码问题。
7. 结尾:一点个人感受
这个项目从环境搭建到扫码和短信二维码生成跑通,前后折腾了我差不多两个周末。最大的体会是:Flutter 在 OpenHarmony 上做工具类应用已经完全可行,但你必须接受“有些插件需要手动找适配分支、有些本地能力要用 platform channel 自己搭桥”的现状。二维码生成和扫码解码这种标准算法能力,尽量留在 Dart 层实现,可移植性强,也少操原生适配的心;而摄像头、存储这类强平台能力,则该用插件就用插件,不要自己从零封装。最后再分享一个小技巧:你在做短信二维码的时候,务必要分别拿微信、系统相机和各类扫码 App 都扫一遍,不同 App 对 URI 的解析宽容度差异很大,提前测一轮能省掉不少售后问题。