3、MethodChannel 基础:Dart端发起调用,Android端接收与响应

好,咱们正式开始聊 MethodChannel。这是 Flutter 与原生通信最经典、最常用的一条路。说白了,它就是一条“双向车道”,Dart 端发个消息过去,Android 端接住,处理完再回传结果。我最早接触 Flutter 时,第一个搞明白的就是它。

3.1 核心思路:命名通道 + 方法调用

MethodChannel 的设计其实很朴素。你给它一个名字,比如 com.example.flutter/channel,然后 Dart 端和 Android 端都拿着这个名字去注册。两边约定好:你调用 getBatteryLevel,我就去读电池电量;你调用 getDeviceInfo,我就返回设备信息。

嗯,这里有个关键点:通道名字必须完全一致,大小写、分隔符都不能错。我在项目中遇到过两次,因为通道名多打了个下划线,结果调试了一下午才发现。你想想看,这种低级错误真的很坑。

核心流程:

  1. Dart 端通过 MethodChannel 调用一个方法名,并携带参数。
  2. Android 端在 MainActivity(或自定义 Plugin)中监听同一个通道。
  3. Android 端根据方法名执行对应逻辑,返回结果。
  4. Dart 端通过 Future 拿到结果,继续执行。

说白了,这就是一个 RPC(远程过程调用)的简化版。只不过两端跑在不同的进程里,靠二进制消息来传递。

3.2 Dart 端:发起调用

Dart 端的代码非常简洁。你只需要创建一个 MethodChannel 实例,然后调用 invokeMethod 即可。

// Dart 端
import 'package:flutter/services.dart';

class BatteryPlugin {
  static const _channel = MethodChannel('com.example.flutter/battery');

  Future<String> getBatteryLevel() async {
    try {
      final int result = await _channel.invokeMethod('getBatteryLevel');
      return '电池电量: $result%';
    } on PlatformException catch (e) {
      return '获取失败: ${e.message}';
    }
  }
}

你看,就这么几行。我习惯把通道定义成静态常量,这样整个 App 里只用一份实例,避免重复创建。另外,invokeMethod 返回的是一个 Future,所以你得用 async/await 来处理。如果 Android 端没响应或者抛异常,Dart 端会收到 PlatformException

个人经验: 调用时尽量把参数类型写明确。比如传一个 Map,里面 key 用字符串,value 用基本类型。Flutter 的序列化机制对复杂对象支持有限,我吃过亏——传了一个自定义对象进去,Android 端收到的是乱码。后来统一用 Map 传参,再也没出过问题。

3.3 Android 端:接收与响应

Android 端需要实现 MethodChannel.MethodCallHandler 接口。通常在 MainActivityconfigureFlutterEngine 方法里注册。

// Android 端 (MainActivity.kt)
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example.flutter/battery"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                if (call.method == "getBatteryLevel") {
                    val batteryLevel = getBatteryLevel()
                    if (batteryLevel != -1) {
                        result.success(batteryLevel)
                    } else {
                        result.error("UNAVAILABLE", "无法获取电池电量", null)
                    }
                } else {
                    result.notImplemented()
                }
            }
    }

    private fun getBatteryLevel(): Int {
        val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
        return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    }
}

这里有几个细节要注意:

  • 通道名必须和 Dart 端一致,否则两边对不上。
  • result 对象必须调用一次,要么 success,要么 error,要么 notImplemented。如果不调用,Dart 端的 Future 会一直挂起,直到超时。我曾经犯过这个错——忘记调 result.success,结果 App 卡死在那,用户点按钮没反应。
  • 方法名匹配用字符串比较,建议用常量定义,避免拼写错误。

避坑指南: 不要在 setMethodCallHandler 里做耗时操作,比如网络请求或数据库读写。因为 MethodChannel 的回调默认跑在 UI 线程。如果你需要做耗时操作,请另开线程,或者用 AsyncTask / Coroutine 处理完再调 result。否则,你的 App 会 ANR。

3.4 数据传递:支持的类型

MethodChannel 支持的数据类型是有限的。Flutter 和 Android 之间通过 StandardMessageCodec 进行序列化。说白了,就是基本类型 + 列表 + 字典。

Dart 类型 Android 类型 说明
null null 空值
bool Boolean 布尔值
int Integer / Long 整型(32位以内用 Integer,超过用 Long)
double Double 浮点数
String String 字符串
Uint8List byte[] 字节数组
Int32List int[] 整型数组
Int64List long[] 长整型数组
Float64List double[] 双精度浮点数组
List ArrayList 列表(元素必须是支持的类型)
Map HashMap 字典(key 必须是 String)

嗯,这里要注意:Map 的 key 必须是 String,否则序列化会失败。我见过有人传 Map<int, String>,结果 Android 端收到的是空 Map。另外,自定义对象是不支持的,你得手动转成 Map 再传。

3.5 流程图:一次完整的调用过程

下面我用一张 SVG 图来展示整个调用链路。你一看就明白。

Dart 端 Android 端 1. 调用 invokeMethod 方法名 + 参数 2. 接收 MethodCall 3. 执行原生逻辑 4. 调用 result.success 返回结果 5. Future 完成 整个过程是异步的,Dart 端不会阻塞 UI 线程

这张图把整个流程串起来了。你看,Dart 端发起调用后,不会卡住 UI,而是等 Android 端处理完再回来。这就是异步的魅力。

3.6 实战中的几个坑

最后,我分享几个实际踩过的坑,希望能帮你少走弯路。

  • 通道注册时机: 一定要在 configureFlutterEngine 里注册,不要在 onCreate 里。因为 FlutterEngine 还没准备好,注册了也收不到消息。
  • 多通道管理: 如果功能多了,建议一个功能一个通道,不要所有方法挤在一个通道里。我见过一个项目,一个通道里塞了 30 多个方法,维护起来简直噩梦。
  • 错误处理: Android 端抛异常时,记得调 result.error,而不是直接 crash。否则 Dart 端会收到 PlatformException,但错误信息是空的,排查起来很麻烦。
  • 线程安全: 如果你在子线程里调 result.success,记得切回主线程。MethodChannel 的回调虽然跑在 UI 线程,但 result 对象本身不是线程安全的。

我的习惯: 每次写完 MethodChannel 代码,我都会先写一个简单的单元测试,模拟 Dart 端调用,看 Android 端能不能正确响应。这样能提前发现通道名不匹配、参数类型不对等问题,而不是等到集成测试才发现。

好了,MethodChannel 的基础用法就这些。说白了,它就是一条命名通道,两边约定好方法名和参数格式,就能愉快地通信了。下一节我们会深入聊聊参数传递的细节,以及如何处理复杂的数据结构。


公众号:蓝海资料掘金营,微信deep3321