将 Flutter 条码扫描插件 `flutter_barcode_scanner` 适配到鸿蒙平台:一次深度实践

将 Flutter 条码扫描插件 flutter_barcode_scanner 适配到鸿蒙平台:一次深度实践

写在前面

鸿蒙生态(HarmonyOS/OpenHarmony)正在快速成长,Flutter 作为跨平台开发框架,对其的支持也在逐步完善。对于开发者来说,将 Flutter 丰富的插件生态移植到鸿蒙平台,不仅能直接丰富应用功能,也是加速项目向鸿蒙迁移的一条捷径。今天,我们就以常用的条形码/二维码扫描插件 flutter_barcode_scanner 为例,来聊聊如何将一个只有 Android 和 iOS 实现的 Flutter 插件,深度适配到 OpenHarmony 标准系统上。我会分享完整的适配思路、具体实现、以及踩坑后总结的优化建议。

一、前期准备:搭好环境,选好"样本"

1. 配置 Flutter 开发环境

首先,确保你的 Flutter SDK 版本在 3.0 或以上(这个版本开始提供了对 OpenHarmony 桌面设备的实验性支持)。打开终端,检查一下环境:

bash 复制代码
# 查看 Flutter 版本和基础环境状态
flutter --version
flutter doctor

# 为 Flutter 启用 OHOS 平台支持(Flutter 3.0+)
flutter config --enable-ohos-desktop

你需要准备好以下几样东西:

  • Flutter SDK: 3.0.0 或更高稳定版。
  • DevEco Studio: 4.0 及以上版本,用于开发和调试鸿蒙原生代码。
  • HarmonyOS SDK: 至少 API 9,建议直接用最新的。
  • Java JDK: 11 或以上,部分工具链会用到。

2. 选择一个合适的插件进行适配

我们选 flutter_barcode_scanner 来"动手术",原因很简单:功能明确(就是调用摄像头扫码)、依赖清晰,而且在社区里一直挺活跃,有参考价值。

bash 复制代码
# 把插件源码克隆到本地
git clone https://github.com/AmolGangadharan/flutter_barcode_scanner.git
cd flutter_barcode_scanner

# 快速看一眼插件的基本信息
cat pubspec.yaml | grep -E "version|description"

插件档案:

  • 名字: flutter_barcode_scanner
  • 干啥用的: 调起手机摄像头,识别并解析 QR Code、EAN-13、UPC-A 等多种格式的条码。
  • 原本支持: Android (Java/Kotlin) 和 iOS (Objective-C/Swift)。
  • 本次目标: 让它能在 OpenHarmony (API 9+) 上跑起来。

二、技术拆解:搞清楚要怎么适配

1. Flutter 插件是如何工作的?

简单回顾一下,Flutter 插件靠的是平台通道(Platform Channel) 来实现 Dart 代码和原生平台代码的"对话"。一个典型的插件包含三层:

  • Dart 层: 给 Flutter 开发者调用的 API。
  • 平台层: Android 和 iOS 的原生实现,干具体的活(比如打开摄像头)。
  • 消息编解码器: 负责在 Dart 和原生数据类型之间做翻译。

2. 鸿蒙适配,路怎么走?

目前 Flutter 官方还没有为鸿蒙提供现成的 MethodChannel 实现,所以我们得自己找路。主流思路有两条:

  • 方案A:通过 FFI(外部函数接口)直接调鸿蒙的 C++ API。 这条路性能最好,直接和系统底层打交道,但对开发者的要求也高,得熟悉鸿蒙 NDK 和 C++。
  • 方案B:在鸿蒙侧仿造一个"平台通道"出来。 相当于自己实现鸿蒙版的 MethodChannel,这需要深入理解 Flutter 引擎的 C++ 层和鸿蒙 UI 框架的交互机制。

flutter_barcode_scanner 的核心是调用摄像头和图像识别,涉及的系统 API 比较复杂。为了追求最佳性能和最直接的操控,我们这次选择走 方案A(FFI) 这条有点挑战但更彻底的路。

3. 看看"手术对象"的原有结构

动手前,先看看插件原来的目录长什么样:

复制代码
flutter_barcode_scanner/
├── android/          # Android 原生实现
├── ios/              # iOS 原生实现
├── lib/              # Dart 接口层
└── pubspec.yaml

为了让鸿蒙能"住进来",我们需要新增一个 ohos/ 目录,同时微调 pubspec.yaml 和 Dart 层的代码来支持多平台判断。

改造后的目标结构如下:

复制代码
flutter_barcode_scanner/
├── android/                    # 原样保留
├── ios/                        # 原样保留
├── ohos/                       # 【新增】鸿蒙的家
│   ├── include/                # 放 FFI 头文件
│   ├── src/                    # C++ 实现源码
│   ├── resources/              # 权限声明等资源配置
│   └── BUILD.gn                # 鸿蒙的构建脚本
├── lib/
│   ├── src/
│   │   ├── flutter_barcode_scanner_ffi.dart # 【新增】FFI接口定义
│   │   └── flutter_barcode_scanner.dart     # 修改,整合多平台
│   └── flutter_barcode_scanner.dart
└── pubspec.yaml                # 修改,声明 ffi 依赖

三、动手实现:为鸿蒙打造原生心脏

1. 定义 FFI 交互接口 (lib/src/flutter_barcode_scanner_ffi.dart)

这个文件是 Dart 和鸿蒙 C++ 代码之间的"接线图"。

dart 复制代码
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';

// 对应 C++ 层的结构体,用来装扫描结果
class _ScanResult extends ffi.Struct {
  external ffi.Pointer<Utf8> barcode;
  external ffi.Int32 format;
  external ffi.Pointer<Utf8> error;
}

// 定义好我们要调用的 C++ 函数长什么样
typedef _scanBarcodeNativeFunc = ffi.Pointer<_ScanResult> Function();
typedef _releaseResultNativeFunc = ffi.Void Function(ffi.Pointer<_ScanResult>);
typedef ScanBarcode = ffi.Pointer<_ScanResult> Function();
typedef ReleaseResult = void Function(ffi.Pointer<_ScanResult>);

class FlutterBarcodeScannerFfi {
  // 1. 加载编译好的鸿蒙动态库
  static final ffi.DynamicLibrary _ohosLib = _loadLibrary();
  static ffi.DynamicLibrary _loadLibrary() {
    try {
      // 注意:实际库名和路径要根据你的构建配置来定
      return ffi.DynamicLibrary.open('libflutter_barcode_scanner_ohos.z.so');
    } catch (e) {
      throw Exception('加载 OpenHarmony 扫码库失败: $e');
    }
  }

  // 2. 查找到具体的函数并转换成 Dart 可调用的格式
  static final ScanBarcode _scan = _ohosLib
    .lookup<ffi.NativeFunction<_scanBarcodeNativeFunc>>('scanBarcode')
    .asFunction<ScanBarcode>();

  static final ReleaseResult _release = _ohosLib
    .lookup<ffi.NativeFunction<_releaseResultNativeFunc>>('releaseScanResult')
    .asFunction<ReleaseResult>();

  /// 3. 给上层调用的扫码方法
  static Future<Map<String, dynamic>> scanBarcode() async {
    // 调用 C++ 函数
    final resultPtr = _scan();
    final result = resultPtr.ref;

    // 把 C 字符串转换成 Dart 字符串
    final String? barcode = result.barcode.address != 0 ? result.barcode.toDartString() : null;
    final String? error = result.error.address != 0 ? result.error.toDartString() : null;
    final int format = result.format;

    // 千万别忘了释放 C++ 那边申请的内存!
    _release(resultPtr);

    if (error != null && error.isNotEmpty) {
      throw PlatformException(code: 'SCAN_FAILED', message: error);
    }

    return {
      'barcode': barcode ?? '',
      'format': _formatToString(format), // 格式码转成文字
    };
  }

  static String _formatToString(int code) {
    const formatMap = {
      0: 'unknown',
      1: 'qrCode',
      2: 'ean13',
      // ... 其他格式
    };
    return formatMap[code] ?? 'unknown';
  }
}

2. 鸿蒙 C++ 核心实现 (ohos/src/scanner_impl.cpp)

这里是真正的"心脏",直接使用鸿蒙的媒体和 AI 接口。

cpp 复制代码
#include "scanner_impl.h"
#include <multimedia/player_framework/native_avcapability.h>
#include <multimedia/player_framework/native_avcodec_base.h>
#include <ai/image_analysis/native_image_analyzer.h>
#include <foundation/ability/ability_context.h>
#include <hilog/log.h>
#include <memory>
#include <string>

#define LOG_TAG "FlutterBarcodeScanner"
#define LOGI(...) OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, __VA_ARGS__)

using namespace OHOS;
using namespace OHOS::Media;
using namespace OHOS::AI;

// 全局上下文,实际应从 Flutter Engine 获取并传递进来
static std::weak_ptr<AbilityRuntime::Context> g_context;

// 和 Dart 层对应的结构体
struct ScanResult {
    char* barcode;
    int32_t format;
    char* error;
};

extern "C" {
    // 1. 扫码主入口,Dart FFI 会调用这个函数
    __attribute__((visibility("default"))) ScanResult* scanBarcode() {
        auto result = new (std::nothrow) ScanResult;
        if (!result) return nullptr;
        result->barcode = nullptr;
        result->error = nullptr;
        result->format = 0;

        // 获取鸿蒙的上下文(这里需要 Flutter 侧传过来)
        auto context = g_context.lock();
        if (!context) {
            LOGE("Ability context 为空!");
            result->error = strdup("平台上下文未初始化。");
            return result;
        }

        // 初始化图像分析器(示例伪代码)
        int32_t ret = InitializeImageAnalyzer(context);
        if (ret != 0) {
            LOGE("初始化图像分析器失败: %d", ret);
            result->error = strdup("扫码器初始化失败。");
            return result;
        }

        // 捕获图像并解码
        std::string capturedBarcode;
        int32_t capturedFormat = 0;
        ret = CaptureAndDecodeFrame(capturedBarcode, capturedFormat);
        if (ret == 0 && !capturedBarcode.empty()) {
            result->barcode = strdup(capturedBarcode.c_str());
            result->format = capturedFormat;
            LOGI("扫描成功: %s", capturedBarcode.c_str());
        } else {
            result->error = strdup("未检测到条码或用户取消。");
        }

        // 清理资源
        ReleaseImageAnalyzer();
        return result;
    }

    // 2. 释放内存,防止泄漏
    __attribute__((visibility("default"))) void releaseScanResult(ScanResult* result) {
        if (result) {
            if (result->barcode) free(result->barcode);
            if (result->error) free(result->error);
            delete result;
        }
    }

    // 3. 初始化函数,用于接收 Flutter 传来的 Context
    __attribute__((visibility("default"))) void setNativeContext(void* nativeContext) {
        // 将 nativeContext 转换为 OHOS Context 并存入 g_context
        // 具体转换依赖 Flutter 鸿蒙嵌入层的实现
    }
}

// 以下是需要你实际实现的鸿蒙平台功能(这里给个框架)
static int32_t InitializeImageAnalyzer(std::shared_ptr<AbilityRuntime::Context>& context) {
    // 调用 OHOS AI SDK 创建 ImageAnalyzer,配置条码识别插件
    // 申请相机权限 (`ohos.permission.CAMERA`)
    // 申请存储权限(如果需要)
    return 0;
}

static int32_t CaptureAndDecodeFrame(std::string& outBarcode, int32_t& outFormat) {
    // 实现步骤:
    // 1. 用 `OHOS::Media::CameraKit` 打开后置摄像头。
    // 2. 配置预览,在 `OnFrameStarted` 回调里拿到图像 Buffer。
    // 3. 把 `OHOS::SurfaceBuffer` 转成 `OHOS::AI::ImageInfo`。
    // 4. 调用 `ImageAnalyzer::AnalyzeImage()` 分析条码。
    // 5. 在回调里解析出内容和类型。
    // 6. 停止预览,关闭相机。
    
    // 这里先模拟一个成功结果
    outBarcode = "1234567890128";
    outFormat = 2; // EAN-13
    return 0;
}

static void ReleaseImageAnalyzer() {
    // 释放 ImageAnalyzer 实例
}

3. 整合多平台的 Dart API (lib/flutter_barcode_scanner.dart)

修改主入口文件,让它能智能选择使用哪个平台的实现。

dart 复制代码
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
import 'src/flutter_barcode_scanner_ffi.dart'; // 引入鸿蒙实现

class FlutterBarcodeScanner {
  static const MethodChannel _channel = MethodChannel('flutter_barcode_scanner');

  /// 统一的扫码接口
  static Future<Map<String, dynamic>> scanBarcode() async {
    try {
      if (Platform.isAndroid || Platform.isIOS) {
        // 走原来的 Android/iOS 通道
        final String result = await _channel.invokeMethod('scan');
        return {'barcode': result, 'format': 'unknown'};
      } else if (_isOpenHarmony()) {
        // 鸿蒙平台,走我们新的 FFI 通道
        return await FlutterBarcodeScannerFfi.scanBarcode();
      } else {
        throw PlatformException(
          code: 'UNSUPPORTED_PLATFORM',
          message: '当前平台不支持条码扫描。',
        );
      }
    } on PlatformException catch (e) {
      throw Exception('扫码失败: ${e.message}');
    } catch (e) {
      throw Exception('发生意外错误: $e');
    }
  }

  // 判断是否为 OpenHarmony 环境(实际判断逻辑可能更复杂)
  static bool _isOpenHarmony() {
    return Platform.isLinux && Platform.environment.containsKey('OHOS');
  }
}

4. 鸿蒙侧的构建脚本 (ohos/BUILD.gn)

告诉鸿蒙的编译系统如何把我们的 C++ 代码打成动态库。

gn 复制代码
import("//build/ohos.gni")

ohos_shared_library("flutter_barcode_scanner_ohos") {
  sources = [
    "src/scanner_impl.cpp",
  ]
  include_dirs = [
    "include",
    "//foundation/ability/ability_runtime/interfaces/innerkits/napi/include",
    "//multimedia/media_standard/interfaces/innerkits/native",
    "//ai/engine/interfaces/innerkits/native",
  ]
  deps = [
    "//foundation/ability/ability_runtime:ace_kit_native",
    "//multimedia/media_standard:media_native",
    "//ai/engine:ai_client_native",
  ]
  external_deps = [
    "hilog_native:libhilog",
    "hdf_core:libhdf_utils",
  ]
  part_name = "flutter_barcode_scanner"
  subsystem_name = "flutter"
}

四、优化与实践建议:让插件跑得更稳更快

适配完成后,如果直接上线,可能会遇到性能或稳定性问题。下面是一些优化方向:

1. 图像处理优化

  • 平衡帧率与分辨率:CaptureAndDecodeFrame 中,相机输出设为 1080p@30fps 通常比 4K 更明智,能在识别率和功耗间取得更好平衡。
  • 只扫关键区域: 可以修改代码,只对相机预览画面的中心区域(比如 60%)进行分析,能显著减少每帧要处理的数据量,提升速度。

2. 处理好线程与异步

  • 别卡住 UI: 所有摄像头操作和图像分析都必须放在后台线程。好在鸿蒙的 ImageAnalyzer 本身提供异步接口,一定要用对。
  • 结果回调要排队: 确保从 C++ 层返回到 Dart 层的扫描结果是顺序的,并且是线程安全的,避免数据混乱。

3. 管好内存和资源

  • 用完就释放: releaseScanResultReleaseImageAnalyzer 函数里的释放操作必须做到位,相机句柄、分析器实例、图像 Buffer 一个都不能漏,这是防止内存泄漏的关键。
  • 安全地传递上下文: setNativeContext 的实现要特别注意生命周期管理,确保从 Flutter 侧传过来的 context 在原生层使用时始终有效。

4. 利用好调试工具

  • 鸿蒙的 Hiview 日志系统很好用。像示例里那样打好 LOGILOGE 日志,在 DevEco Studio 的 HiLog 窗口里过滤 FlutterBarcodeScanner 标签,跟踪原生层的问题会方便很多。

五、总结与接下来的路

通过上面的步骤,我们基本上完成了 flutter_barcode_scanner 插件向鸿蒙平台的深度迁移。这次实践的核心思路是绕开传统的 MethodChannel,通过 FFI 直接调用鸿蒙的原生 C++ API。这条路虽然一开始走起来有点费力,但带来的性能优势和对系统能力的直接掌控是值得的。

这种模式不仅仅适用于扫码插件,对于其他需要调用复杂系统功能(比如传感器、高级图形处理、专用 AI 能力)的 Flutter 插件来说,也是一个可行的技术参考。

当然,目前这条路还不算平坦:

  1. 工具链还在成长: Flutter for OpenHarmony 毕竟还在早期,插件开发、调试、打包的自动化工具还不够完善,很多事需要手动处理。
  2. 需要社区合力: 单打独斗效率低。如果能有更多开发者分享不同类别插件的适配经验,形成一些"最佳实践",对整个生态会大有裨益。
  3. 期待官方支持: 最理想的状况,还是未来 Flutter 官方或 OpenHarmony 社区能推出标准的插件平台通道鸿蒙实现,把适配成本降到最低。

这次深度适配至少证明了一件事:把 Flutter 成熟的插件生态迁移到鸿蒙系统,在技术上是完全可行的。希望这个具体的案例能为你自己的项目带来一些启发,也期待看到更多优秀的 Flutter 插件在鸿蒙生态里焕发生机。

相关推荐
月光下的丝瓜20 小时前
Flutter 国内安装指南
前端·flutter
TrisighT1 天前
我用 AI 逆向了 ArkTS @Builder 的编译产物,看完再也不敢乱写嵌套了
ai编程·harmonyos·arkts
看谷秀3 天前
鸿蒙-part3-arkts下
arkts
TrisighT3 天前
ArkTS 的 @BuilderParam 你八成只用了皮毛——那个尾随闭包写法差点被我当 bug 删了
harmonyos·arkts·arkui
恋猫de小郭3 天前
Amper 正式转正 Kotlin Toolchain ,Gradle 未来何去何从
android·前端·flutter
张风捷特烈3 天前
Flutter 类库大揭秘#02 | path_provider 各平台实现
前端·flutter
TT_Close4 天前
别劝退了!5秒搞定 Flutter 鸿蒙 FVM 起跑线
flutter·harmonyos·visual studio code
TrisighT4 天前
ArkTS 列表滚动时为什么会闪现旧数据?我扒了 LazyForEach 的复用逻辑
harmonyos·arkts·arkui
你听得到114 天前
用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台
前端·flutter·性能优化
TrisighT5 天前
一个下午搞定 ArkTS 折叠面板?结果我从两点写到晚上九点
harmonyos·arkts·arkui