与原生互操作: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)
  }
}

参数与返回值的类型映射

DartAndroid(Kotlin)iOS(Swift)
intInt / LongNSNumber(int:)
StringStringNSString
List<dynamic>List / ArrayListNSArray
Uint8ListByteArrayFlutterStandardTypedData(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)实现 onListenonCancel,把 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抽成常量或放进插件内部统一管理
忘记回 resultDart 侧一直 await 不返回每个分支都要 success / error / notImplemented
通道回调里直接操作 UI崩溃或界面不刷新Android 用 runOnUiThread,iOS 回主队列

小结:原生互操作的默认答案是“先用插件”,确需自己写时用 MethodChannel 做一次性方法调用、EventChannel 做持续事件、只有嵌原生视图才用 PlatformView;通道名、类型映射、result 唯一性与主线程切换是四个必查点,插件用 flutter create --template=plugin 生成并按路径依赖联调,原生改动一律完整重启验证。

笔记加载中…