138 lines
6.4 KiB
Markdown
138 lines
6.4 KiB
Markdown
# 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 里直接写平台通道代码)的意义:原生实现细节被这一层完全封装。
|