与原生互操作:MethodChannel 与插件开发
Flutter 覆盖不了所有系统能力,遇到只有原生 SDK 才有的东西(厂商推送、蓝牙协议栈、金融风控、老业务模块)时,就要穿过平台通道调用原生代码。这条通道叫 MethodChannel:Dart 侧发一个方法名和参数,原生侧处理后回一个结果。本章给出 Dart、Kotlin、Swift 三侧的最小可用例子,再讲持续事件、原生视图与自建插件的边界。
什么时候需要写原生
| 场景 | 建议 | 理由 |
|---|---|---|
| 社区已有成熟插件 | 直接用插件 | 自己写要承担多端长期维护成本 |
| 官方 SDK 只有原生版本 | 写通道调用 | 这是通道最主要的用途 |
| 逐帧级性能要求 | 评估原生开发 | 通道通信是异步的,不适合高频小数据传输 |
MethodChannel:Dart 侧
import 'package:flutter/services.dart';
class DeviceChannel {
// 通道名必须与原生侧逐字一致,建议统一用 "反向域名/模块" 格式
static const MethodChannel _channel = MethodChannel('com.example.app/device');
static Future<int> batteryLevel() async {
try {
return await _channel.invokeMethod<int>('getBatteryLevel') ?? -1; // 泛型决定返回值类型
} on MissingPluginException {
return -1; // 该平台没实现,走降级而不是崩溃
} on PlatformException catch (e) {
debugPrint('原生错误: ${e.code} ${e.message}'); // 原生侧主动报的错
return -1;
}
}
static Future<void> vibrate({int ms = 200}) =>
_channel.invokeMethod<void>('vibrate', <String, dynamic>{'ms': ms});
}
Android 侧(Kotlin)
// android/app/src/main/kotlin/com/example/app/MainActivity.kt(import 省略)
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) // 先调父类,否则其他插件不生效
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.app/device").setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> result.success(readBatteryLevel()) // 用 BATTERY_SERVICE 读容量
"vibrate" -> {
vibrate(call.argument<Int>("ms") ?: 200) // 参数类型须与 Dart 侧一致
result.success(null) // 无返回值也要回,否则 Dart 侧 Future 永不完成
}
else -> result.notImplemented() // 未实现的方法必须显式告知
}
}
}
}
iOS 侧(Swift)
// ios/Runner/AppDelegate.swift
import Flutter
import UIKit
@main @objc class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: "com.example.app/device", binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { call, result in
switch call.method {
case "getBatteryLevel":
UIDevice.current.isBatteryMonitoringEnabled = true
result(Int(UIDevice.current.batteryLevel * 100)) // 返回值须在类型映射表内
default:
result(FlutterMethodNotImplemented) // 与 Kotlin 的 notImplemented 对应
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
参数与返回值的类型映射
| Dart | Android(Kotlin) | iOS(Swift) |
|---|---|---|
int | Int / Long | NSNumber(int:) |
String | String | NSString |
List<dynamic> | List / ArrayList | NSArray |
Uint8List | ByteArray | FlutterStandardTypedData(bytes:) |
表外的自定义类型不会自动序列化,统一转成 Map<String, dynamic> 再传;图片、音频走 Uint8List,但跨通道是有复制的,大文件别来回搬。
EventChannel:持续事件
const EventChannel _sensor = EventChannel('com.example.app/sensor');
late StreamSubscription<dynamic> _sub;
// 订阅原生持续推送;页面退出必须 cancel,否则原生侧会一直回调
_sub = _sensor.receiveBroadcastStream().listen(
(event) => debugPrint('传感器: $event'),
onError: (Object e) => debugPrint('流错误: $e'),
);
原生侧用 EventChannel.StreamHandler(Kotlin)/ FlutterStreamHandler(Swift)实现 onListen 与 onCancel,把 EventSink 存起来,数据源有变化时 sink.success(value);onCancel 里要真正停止原生监听。
PlatformView:嵌入原生视图
| 维度 | 说明 |
|---|---|
| 适用 | 已有成熟原生视图(地图、播放器、WebView),且性能要求高 |
| 代价 | 视图合成有额外开销,滚动可能不同步;只是取数据应用 MethodChannel,且视图生命周期必须跟随 Widget 释放 |
写自己的插件
# 生成插件工程;本地联调时宿主工程用路径依赖:my_plugin: { path: ../my_plugin }
flutter create --template=plugin --org com.example --platforms=android,ios -a kotlin -i swift my_plugin
# my_plugin/pubspec.yaml:声明各平台实现类,宿主工程才能自动注册
flutter:
plugin:
platforms:
android: { package: com.example.my_plugin, pluginClass: MyPluginPlugin }
ios: { pluginClass: MyPluginPlugin }
调试原生代码
Android 侧用 flutter run -v 打印通道调用细节、用 adb logcat -s flutter:* MyPlugin:* 按 tag 过滤日志;Xcode 侧用 open ios/Runner.xcworkspace 打开工程并在 setMethodCallHandler 里打断点,应用已在运行时可用 flutter attach 挂载调试器。断点不生效时优先检查是否用 flutter run 启动、以及 Swift 文件是否在 Runner target 内。
常见坑
| 坑 | 现象 | 正确做法 |
|---|---|---|
| 通道名两侧不一致 | MissingPluginException | 抽成常量或放进插件内部统一管理 |
忘记回 result | Dart 侧一直 await 不返回 | 每个分支都要 success / error / notImplemented |
| 通道回调里直接操作 UI | 崩溃或界面不刷新 | Android 用 runOnUiThread,iOS 回主队列 |
小结:原生互操作的默认答案是“先用插件”,确需自己写时用 MethodChannel 做一次性方法调用、EventChannel 做持续事件、只有嵌原生视图才用 PlatformView;通道名、类型映射、result 唯一性与主线程切换是四个必查点,插件用 flutter create --template=plugin 生成并按路径依赖联调,原生改动一律完整重启验证。