Flutter 平台通道:与原生代码握手

《Flutter 平台通道:与原生代码握手》

本文做什么,不做什么

Flutter 引擎之外的事——电池电量、蓝牙、推送、原生存储——都要穿过一层桥才能用,这层桥就是平台通道。本文用”取电池电量 + 订阅充电状态流”两个场景,把 MethodChannel 和 EventChannel 的 Android(Kotlin)、iOS(Swift)双端实现各写一遍,顺带把参数序列化的边界说清。边界先说死:dart:ffi 的同步 C 绑定不展开;Pigeon 代码生成只点名不展开;封装成可发布的插件不做;生产里拿电量请直接用 battery_plus 这类现成插件——本文手写是为了讲清它底下发生了什么。版本口径:Flutter 3.22(2024-05-14 GA,配套 Dart 3.4)。

什么时候必须写平台代码

先过一道判断,别上来就写桥:

  • pub.dev 有维护良好的插件(battery_plus、path_provider 这类)——直接用,手写是给自己找维护负担;
  • 系统能力没有现成插件,或公司内部只有 iOS/Android 两份原生 SDK——必须自己写;
  • 要嵌入原生视图(地图、广告)——那是 PlatformView 的事,另一套机制,本文不碰。

通道的本质一句话:Dart 与原生之间没有共享内存,一切消息经过序列化、走 binary messenger 异步投递。所以通道调用全是 Future、全是异步,且参数必须是”两边编解码器都认识”的类型——后面有一节专门数这些类型。

MethodChannel:一次请求,一次响应

场景定为拿电池电量。Dart 侧声明通道并发起调用:

// lib/battery.dart
import 'package:flutter/services.dart';

class Battery {
  static const _channel = MethodChannel('com.xrush.battery/level');

  /// 电量百分比;原生侧报错会以 PlatformException 抛出
  Future<int> level() async {
    final v = await _channel.invokeMethod<int>('getLevel');
    if (v == null) {
      throw StateError('通道返回为空');
    }
    return v;
  }
}

Android 侧(Kotlin)在 MainActivity 里接住:

// android/app/src/main/kotlin/.../MainActivity.kt
class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            "com.xrush.battery/level"
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "getLevel" -> {
                    val bm = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
                    val level = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
                    if (level != -1) result.success(level)
                    else result.error("UNAVAILABLE", "电量不可用", null)
                }
                else -> result.notImplemented()
            }
        }
    }
}

iOS 侧(Swift)挂在 AppDelegate 上:

// ios/Runner/AppDelegate.swift
@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.xrush.battery/level",
      binaryMessenger: controller.binaryMessenger)
    channel.setMethodCallHandler { (call, result) in
      switch call.method {
      case "getLevel":
        UIDevice.current.isBatteryMonitoringEnabled = true
        let level = UIDevice.current.batteryLevel // 0~100 的 Int,取不到是 -1
        if level != -1 { result(level) }
        else {
          result(FlutterError(code: "UNAVAILABLE",
                              message: "电量不可用", details: nil))
        }
      default:
        result(FlutterMethodNotImplemented)
      }
    }
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

三条铁律,违反任何一条都是线上事故:通道名双端逐字一致(大小写、斜杠都是名字的一部分);方法名双端一致,未注册的方法走 notImplemented 分支;每个分支都必须回话——success、error、notImplemented 三选一,漏了 Dart 侧的 Future 就永远挂着。还有个新手必撞的现场:通道名写错或 handler 没注册,Dart 侧抛的不是 PlatformException 而是 MissingPluginException——第一排查项永远是”名字对没对、handler 注册了没”。

组织方式也有讲究:一个通道承载多个方法(Kotlin 的 when、Swift 的 switch 分发),别一个方法开一个通道——通道名是路由单位,method 才是语义单位。注册时机放在 configureFlutterEngine 里正合适:引擎每次创建都会调它,热重启、换页面后 handler 依然在位,Dart 侧不用关心原生注册了没有。

序列化边界:什么能过桥

默认的 StandardMessageCodec 认这些类型,清单背下来:

  • null、bool、int、double、String;
  • 数值与二进制数组:Uint8List、Int32List、Int64List、Float32List、Float64List;
  • List 与 Map——但嵌套里面的元素也必须是清单内的类型。

自定义对象过不了桥:要么手动转 Map({'id': 42, 'name': 'x'}),要么走 JSON codec,要么用 Pigeon 让代码生成替你写转换。过桥是双向的:Dart 发的 Map 到 Kotlin 是 HashMap、到 Swift 是 Dictionary,原生侧取参数就是普通集合操作——取不到就 error 回话,别假设参数永远齐整。Dart 侧的类型系统管不到桥对岸,参数校验在原生侧再防一遍是必要的。大整数留个心眼:移动端 Dart 的 int 是 64 位,但编译到 Web 后底层是 double,超过 2^53 就失真——跨端 ID 这类字段走 String 是省心的选择。错误也有约定:原生侧 result.error("UNAVAILABLE", msg, null),Dart 侧收到的就是 PlatformException(code: UNAVAILABLE, message: msg)——code 用大写常量双端对齐,别散落魔法字符串。

EventChannel:平台往 Dart 推流

电量是问一次答一次,充电状态则是”变了就通知”——这是 EventChannel 的舞台:Dart 侧拿到一个 Stream,原生侧在监听开始与结束时注册和注销。

static const _events = EventChannel('com.xrush.battery/charging');

Stream<bool> charging() =>
    _events.receiveBroadcastStream().map((v) => v as bool);

Android 侧用 StreamHandler,onListen 注册广播接收器、onCancel 注销:

EventChannel(
    flutterEngine.dartExecutor.binaryMessenger,
    "com.xrush.battery/charging"
).setStreamHandler(object : EventChannel.StreamHandler {
    private var receiver: BroadcastReceiver? = null

    override fun onListen(args: Any?, sink: EventChannel.EventSink) {
        receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent) {
                val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
                sink.success(status == BatteryManager.BATTERY_STATUS_CHARGING)
            }
        }
        registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    }

    override fun onCancel(args: Any?) {
        receiver?.let { unregisterReceiver(it) }
        receiver = null
    }
})

iOS 侧对称:FlutterEventChannel 配一个实现 FlutterStreamHandler 的对象,onListen 里经 NotificationCenter 监听电池状态变化,onCancel 里移除观察者:

final class ChargingHandler: NSObject, FlutterStreamHandler {
  private var observer: NSObjectProtocol?

  func onListen(withArguments arguments: Any?,
                sink: @escaping FlutterEventSink) -> Void {
    UIDevice.current.isBatteryMonitoringEnabled = true
    observer = NotificationCenter.default.addObserver(
      forName: UIDevice.batteryStateDidChangeNotification,
      object: nil, queue: nil) { _ in
        sink(UIDevice.current.batteryState == .charging)
      }
  }

  func onCancel(withArguments arguments: Any?) -> Void {
    if let o = observer {
      NotificationCenter.default.removeObserver(o)
      observer = nil
    }
  }
}

// AppDelegate 里注册:
let eventChannel = FlutterEventChannel(
  name: "com.xrush.battery/charging",
  binaryMessenger: controller.binaryMessenger)
eventChannel.setStreamHandler(ChargingHandler())

两个坑:onListen 与 onCancel 必须成对,注册了不注销就是接收器/观察者泄漏,Activity 都会被拖着;流可能中途报错——原生侧 sink.error(code, msg, details) 之后流终止,Dart 侧的 onError 收到 PlatformException,UI 层要么重订阅要么提示降级。另外 receiveBroadcastStream 返回的是广播流,多处 listen 会各自触发一次 onListen,平台侧就注册多份——流对象缓存起来复用,别在 build 里现造。

线程模型:handler 跑在哪个线程

默认约定:双端的 handler 都在平台主线程被调用,result/sink 的回话也必须回主线程。这意味着 handler 里不能干重活——读大文件、解码图片这类事要派发到后台线程,干完再回主线程回话。Android 用 Handler(Looper.getMainLooper()).post,iOS 用 DispatchQueue.main.async。漏掉这步的典型症状:UI 掉帧甚至 ANR,而 Dart 侧看起来什么都没做错——因为错在原生侧把主线程堵了。

顺带认识 BasicMessageChannel

通道家族还有第三件套:MethodChannel 是”请求-响应”,EventChannel 是”订阅-推送”,BasicMessageChannel 则是不定式的自由消息——任意一方随时 send,对面 handler 收:

static const _raw =
    BasicMessageChannel<String>('com.xrush.battery/raw', StringMessageCodec());

final reply = await _raw.send('ping');
_raw.setMessageHandler((msg) async => 'pong');

什么时候用它:不需要方法名语义、也不需要流语义的杂项消息,比如双端心跳、简单状态同步。多数业务用前两个就够,知道它存在,遇到场景能想起来就行。

通道怎么测

《Flutter 测试入门:单元测试与 widget test》的心智直接接上:通道测试不需要真机,把消息拦下来即可——

TestWidgetsFlutterBinding.ensureInitialized();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
    .setMockMethodCallHandler(_channel, (call) async => 87);

expect(await Battery().level(), 87);

mock 返回 87、null、抛 PlatformException 三种情况各测一条,Dart 侧的错误处理路径就全钉住了。双端真机的验证留给手动清单:改插拔一次充电线,看流有没有动静。

验收清单

  • 真机上 Dart 侧拿到的电量数值与系统设置里显示的一致;
  • iOS 模拟器 batteryLevel 恒为 -1,走 UNAVAILABLE 错误分支——这条用于验证错误路径,别当 bug 报;
  • 故意把 Dart 侧通道名改一个字母,抛 MissingPluginException——熟悉第一排查项;
  • 调用未注册的方法名,Dart 侧捕获 MissingPluginException 且应用不崩;
  • 插拔充电线,充电状态流收到 true/false 翻转,拔掉监听后无泄漏报错;
  • 通道单测在 flutter test 下全绿,覆盖成功、null、异常三种 mock。

双端联调的一招土办法

通道出问题时,别盯着单端猜。在原生 handler 入口把 method 与参数打出来,Dart 侧在 invokeMethod 前后各打一行,然后把两边日志按时间线对齐——”消息发出去了没、对端收到没、回话回来没”三问立刻见分晓。双端日志一贴,80% 的通道问题(名字不一致、参数类型不认识、handler 没注册)在时间线断点处自己现形,剩下 20% 才轮到看序列化类型清单。

学完之后

手写通道是理解 Flutter 混合开发的地基,往上走三条路:Pigeon 用代码生成把双端签名对齐,省掉手写转换;FFI 走同步 C 绑定,适合算法密集场景;PlatformView 嵌原生视图。本文的位置在《Flutter 学习路线》的”平台互操作”一节——路线里”多数需求有插件兜底”的判断,本文给了它的成本依据。

本文数据核对时间:2024-08-02:Flutter 3.22(2024-05-14 GA,配套 Dart 3.4);MethodChannel/EventChannel/StandardMessageCodec 为 flutter 框架 services 库内置,API 以 api.flutter.dev 为准;Kotlin 侧 configureFlutterEngine 与 Swift 侧 FlutterAppDelegate 挂接方式为该版本模板口径;battery_plus 等现成插件为生产推荐,本文实现仅作教学;iOS 模拟器电量返回 -1 为平台公开行为。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇