# 07. 原生能力集成方式 ## 决策 原生能力(扫码、支付、蓝牙等)统一封装成独立的 `native_*` Dart package(结构见 [01-project-structure.md](./01-project-structure.md)),跨语言接口用 **[Pigeon](https://pub.dev/packages/pigeon)**(`^27.3.0`,2026-08 快照)生成,不手写裸 `MethodChannel`/`invokeMethod` 字符串调用。 ## 依赖 ```yaml 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](./05-networking.md) 里同一套 `AppException` 体系,不让原生异常类型(如 `PlatformException`)直接抛到 `feature_*` 业务代码里。 - 三端(Android/iOS/OHOS)中若某一端暂未实现,公共 API 类里对应平台分支返回明确的 `UnimplementedError`,不允许静默返回空值或占位假数据。 ## 参考链接 - [Pigeon 官方文档](https://pub.dev/packages/pigeon) - [pigeon | Dart package](https://pub.dev/packages/pigeon) - [Flutter 平台通道官方文档](https://docs.flutter.dev/platform-integration/platform-channels) ## 附录:Pigeon 是什么,日常怎么用 给还没接触过跨语言原生集成的同学看的入门说明。 ### 要解决的问题 Flutter 原生的 [`MethodChannel`](https://docs.flutter.dev/platform-integration/platform-channels) 机制本质是"字符串方法名 + 弱类型参数"的消息传递: ```dart // 手写 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`:扫码能力) ```dart // 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; } ``` ```bash 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`,非生成代码,是需要手写的实现): ```kotlin class ScanApiImpl(private val activity: Activity) : ScanHostApi { override fun startScan(options: ScanOptions, callback: (Result) -> Unit) { // 调用具体的扫码 SDK,拿到结果后: callback(Result.success(ScanResult(code = "123456", format = "QR_CODE"))) } override fun stopScan() { // 停止扫码 SDK } } ``` Dart 端对外的公共 API(`native_scan.dart`,`feature_scan` 唯一能调用的入口): ```dart class NativeScan { final ScanHostApi _api = ScanHostApi(); Future 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 stopScan() => _api.stopScan(); } ``` `feature_scan` 只 import `NativeScan` 这一个类,完全不知道底层是 Pigeon 生成的还是手写 `MethodChannel`——这也是把原生能力做成独立 `native_*` package(而不是散落在各 feature 里直接写平台通道代码)的意义:原生实现细节被这一层完全封装。