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

138 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<ScanResult>) -> 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<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 里直接写平台通道代码)的意义:原生实现细节被这一层完全封装。