Files
conti-docs/07-native-integration.md
T

6.4 KiB
Raw Blame History

07. 原生能力集成方式

决策

原生能力(扫码、支付、蓝牙等)统一封装成独立的 native_* Dart package(结构见 01-project-structure.md),跨语言接口用 Pigeon^27.3.0,2026-08 快照)生成,不手写裸 MethodChannel/invokeMethod 字符串调用。

依赖

dev_dependencies:
  pigeon: ^27.3.0

包结构规则

native_scan/
  pigeons/
    scan_api.dart       # 接口 schema 定义,唯一手写的源文件
  lib/
    native_scan.dart     # 对外导出:封装好的公共 API 类(feature 只调这个)
    src/
      generated/          # pigeon 生成的 Dart 端代码,不手动修改
  android/
    src/main/kotlin/.../ScanApiImpl.kt   # 生成的 Kotlin host API 接口的具体实现
  ios/
    Classes/ScanApiImpl.swift            # 生成的 Swift host API 接口的具体实现
  ohos/
    src/main/ets/ScanApiImpl.ets         # 生成的 ArkTS host API 接口的具体实现

使用规则

  • pigeons/xxx_api.dart唯一手写的接口定义文件,Dart 端和三端原生的桩代码全部由 dart run pigeon --input pigeons/xxx_api.dart 生成,生成产物不手动修改,改需求就改 schema 重新生成。
  • Dart 调原生用 @HostApi();原生主动推事件给 Dart(比如扫码结果的持续回调)用 @FlutterApi()——不允许为了图省事用 @HostApi() 硬凑双向通信。
  • feature_* 只允许依赖对应 native_*lib/native_xxx.dart 导出的公共 API 类,不允许直接 import src/generated/ 里的生成代码。
  • 原生侧异常需要在生成的 host API 实现里捕获并转换成 Pigeon schema 里声明的错误类型,Dart 侧统一映射成 05-networking.md 里同一套 AppException 体系,不让原生异常类型(如 PlatformException)直接抛到 feature_* 业务代码里。
  • 三端(Android/iOS/OHOS)中若某一端暂未实现,公共 API 类里对应平台分支返回明确的 UnimplementedError,不允许静默返回空值或占位假数据。

参考链接

附录:Pigeon 是什么,日常怎么用

给还没接触过跨语言原生集成的同学看的入门说明。

要解决的问题

Flutter 原生的 MethodChannel 机制本质是"字符串方法名 + 弱类型参数"的消息传递:

// 手写 MethodChannel,容易出的问题:
final result = await MethodChannel('scan_channel').invokeMethod('startScan', {'timeout': 5000});
// 1. 'startScan' 是字符串,原生那边方法名打错了,运行时才报 "not implemented"
// 2. 参数是 Map,字段名/类型对不上,运行时才崩,编译期完全看不出来
// 3. 返回值类型是 dynamic,还要自己强转、自己判断 null

三个问题的共性是:Dart 和原生代码之间没有共享的类型系统,接口的一致性完全靠开发者手动保证、runtime 才能发现错误。

Pigeon 用一个 Dart 文件定义"接口 schema"(有哪些方法、参数和返回值类型),然后生成 Dart 端 + Android(Kotlin) + iOS(Swift) 三端的强类型桩代码——方法名、参数、返回类型三端保持一致,改了 schema 忘记同步实现,编译期就会报错(生成的原生接口是抽象类/协议,没实现完整会编译不过),彻底消灭"方法名打错""参数字段对不上"这类只有运行时才发现的问题。

核心概念

  1. Schema 文件pigeons/xxx_api.dart):用普通 Dart 类和注解描述接口,不是真的可执行代码,只是给 pigeon 生成器读的"接口契约"。
  2. @HostApi():声明一个"Dart 调用原生"的接口,pigeon 生成 Dart 端可直接调用的类,以及原生端需要实现的抽象类/协议。
  3. @FlutterApi():声明一个"原生调用 Dart"的接口(方向相反),用于原生侧主动推送事件(比如蓝牙扫描持续上报发现的设备)。
  4. 生成命令dart run pigeon --input pigeons/xxx_api.dart 会同时生成 Dart、Kotlin、Swift 三份代码,开发者只需要去实现原生那两个抽象类/协议里的方法体。

使用示例(native_scan:扫码能力)

// native_scan/pigeons/scan_api.dart —— 唯一手写的 schema 文件
@HostApi()
abstract class ScanHostApi {
  @async
  ScanResult startScan(ScanOptions options);
  void stopScan();
}

class ScanOptions {
  ScanOptions({required this.timeoutMs});
  final int timeoutMs;
}

class ScanResult {
  ScanResult({required this.code, required this.format});
  final String code;
  final String format;
}
dart run pigeon \
  --input pigeons/scan_api.dart \
  --dart_out lib/src/generated/scan_api.g.dart \
  --kotlin_out android/src/main/kotlin/com/conti/native_scan/ScanApi.g.kt \
  --swift_out ios/Classes/ScanApi.g.swift

Android 端实现生成的抽象类(ScanApiImpl.kt,非生成代码,是需要手写的实现):

class ScanApiImpl(private val activity: Activity) : ScanHostApi {
    override fun startScan(options: ScanOptions, callback: (Result<ScanResult>) -> Unit) {
        // 调用具体的扫码 SDK,拿到结果后:
        callback(Result.success(ScanResult(code = "123456", format = "QR_CODE")))
    }

    override fun stopScan() {
        // 停止扫码 SDK
    }
}

Dart 端对外的公共 APInative_scan.dartfeature_scan 唯一能调用的入口):

class NativeScan {
  final ScanHostApi _api = ScanHostApi();

  Future<ScanResult> startScan({Duration timeout = const Duration(seconds: 5)}) async {
    try {
      return await _api.startScan(ScanOptions(timeoutMs: timeout.inMilliseconds));
    } on PlatformException catch (e) {
      throw NativeCapabilityException('扫码失败: ${e.message}');
    }
  }

  Future<void> stopScan() => _api.stopScan();
}

feature_scan 只 import NativeScan 这一个类,完全不知道底层是 Pigeon 生成的还是手写 MethodChannel——这也是把原生能力做成独立 native_* package(而不是散落在各 feature 里直接写平台通道代码)的意义:原生实现细节被这一层完全封装。