3. MethodChannel基础:Dart端发送消息、Android端接收消息、基本数据类型传递

好,咱们今天来啃MethodChannel这块硬骨头。说实话,这是Flutter与原生交互最核心的通道,没有之一。我最早接触Flutter插件开发时,就是被MethodChannel这个概念给绕晕的——后来想明白了,它就是个「消息管道」,Dart和原生各守一边,通过这个管道传数据、调方法。

核心思想:MethodChannel就像一根电话线。Dart端拨号(调用方法),Android端接听(处理调用),然后回传结果。就这么简单。

3.1 什么是MethodChannel?

MethodChannel是Flutter提供的双向通信机制。它允许Dart代码调用原生代码,也允许原生代码主动给Dart发消息。但咱们这节课先聚焦前半部分——Dart主动调用,Android被动响应。

它的工作流程,我画了张图,你一看就明白:

Dart 端 MethodChannel('com.example/channel') invokeMethod('getData', args) ↓ 发送消息 ↑ 接收结果 MethodChannel 二进制编码/解码 Android 端 MethodChannel(flutterView, 'com.example/channel') setMethodCallHandler() onMethodCall() 处理 支持的数据类型:String | int | bool | List | Map 自动序列化/反序列化,无需手动处理

这张图把整个流程讲清楚了。Dart端通过invokeMethod发请求,Android端通过onMethodCall接收处理,处理完再回传结果。中间的数据传输,Flutter框架帮你自动序列化好了。

3.2 在Dart端创建MethodChannel

先看Dart端怎么写。代码其实就几行:

import 'package:flutter/services.dart';

// 创建MethodChannel实例
// 这个字符串就是通道名称,两端必须一致
static const platform = MethodChannel('com.example/battery');

// 调用原生方法
Future<String> getBatteryLevel() async {
  try {
    // invokeMethod的第一个参数是方法名,第二个参数是传给原生的数据
    final String result = await platform.invokeMethod('getBatteryLevel');
    return result;
  } on PlatformException catch (e) {
    // 原生端抛异常时,会走到这里
    return "Failed to get battery level: '${e.message}'";
  }
}

这里有个细节我特别想强调——通道名称。它就是个字符串,但必须保证Dart端和Android端完全一致。我见过太多新手因为这里拼写不一致,调试了半天找不到原因。嗯,我自己也踩过这个坑。

我的习惯:通道名称用反向域名格式,比如 com.yourcompany.yourapp/feature。这样不容易跟其他插件冲突。一个应用里可以有多个MethodChannel,每个负责不同的功能模块。

3.3 在Android端注册并处理消息

Android端的工作稍微多一点。你需要在MainActivity或者FlutterFragment里注册MethodChannel:

// 在MainActivity中
public class MainActivity extends FlutterActivity {
    private static final String CHANNEL = "com.example/battery";

    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        super.configureFlutterEngine(flutterEngine);

        // 创建MethodChannel,绑定到Flutter引擎
        new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
            .setMethodCallHandler(
                (call, result) -> {
                    // call.method 就是Dart端传过来的方法名
                    if (call.method.equals("getBatteryLevel")) {
                        int batteryLevel = getBatteryLevel();

                        if (batteryLevel != -1) {
                            // 成功时调用result.success()
                            result.success(batteryLevel);
                        } else {
                            // 失败时调用result.error()
                            result.error("UNAVAILABLE", "Battery level not available.", null);
                        }
                    } else {
                        // 未识别的方法
                        result.notImplemented();
                    }
                }
            );
    }

    private int getBatteryLevel() {
        // 实际获取电池电量的代码
        // ...
        return 100;
    }
}

注意看result这个对象。它有三种返回方式:

  • result.success() — 正常返回数据
  • result.error() — 返回错误信息
  • result.notImplemented() — 告诉Dart端这个方法没实现

这三种情况,Dart端都能通过try-catch捕获到。我建议你养成习惯,每个方法调用都包一层try-catch,不然原生端崩了,Dart端直接傻眼。

3.4 基本数据类型传递

MethodChannel支持的数据类型,说白了就是JSON能表达的那些。我整理了个表格:

Dart类型 Android类型 说明
String String 字符串,最常用
int Integer 32位整数,注意范围
bool Boolean 布尔值
double Double 浮点数
List ArrayList 列表,元素类型需一致
Map HashMap 键值对,键必须是String
null null 空值

举个例子,如果你要传一个复杂点的数据:

// Dart端
Map<String, dynamic> userInfo = {
  'name': '张三',
  'age': 28,
  'isVip': true,
  'tags': ['developer', 'flutter'],
};

await platform.invokeMethod('updateUser', userInfo);
// Android端接收
if (call.method.equals("updateUser")) {
    Map<String, Object> userInfo = (Map<String, Object>) call.arguments;
    String name = (String) userInfo.get("name");
    int age = (int) userInfo.get("age");
    boolean isVip = (boolean) userInfo.get("isVip");
    List<Object> tags = (List<Object>) userInfo.get("tags");
    // 处理业务逻辑...
}

我曾经踩过的坑:Android端接收Map时,键的类型必须是String。如果你在Dart端用了int作为Map的键,传过去会直接崩溃。还有,List里的元素类型要一致,混着传String和int虽然不会崩,但取出来时类型转换很麻烦。

3.5 同步调用 vs 异步调用

这个问题很多人搞混。我直接说结论:MethodChannel本质上都是异步的

Dart端调用invokeMethod返回的是Future,所以你得用await或者.then()来拿结果。但Android端处理时,可以同步处理,也可以异步处理。

看个同步处理的例子:

// Android端同步处理
if (call.method.equals("getDeviceInfo")) {
    // 直接返回,不需要额外线程
    Map<String, String> info = new HashMap<>();
    info.put("model", Build.MODEL);
    info.put("version", Build.VERSION.RELEASE);
    result.success(info);
}

再看异步处理的场景:

// Android端异步处理
if (call.method.equals("scanBluetooth")) {
    // 蓝牙扫描是耗时操作,必须异步
    new Thread(() -> {
        List<String> devices = bluetoothScanner.scan();
        // 注意:result必须在主线程调用
        new Handler(Looper.getMainLooper()).post(() -> {
            result.success(devices);
        });
    }).start();
}

这里有个关键点——result.success()result.error()必须在主线程调用。如果你在子线程里直接调,会抛异常。我刚开始做蓝牙插件时就因为这个折腾了一下午。

记住这个原则:Dart端永远用await等结果,Android端根据耗时决定同步还是异步。耗时操作必须开子线程,但回调result时切回主线程。

3.6 实战小技巧

最后分享几个我平时写MethodChannel时的小习惯:

  • 方法名用驼峰命名,比如getBatteryLevelupdateUserInfo。两端保持一致。
  • 参数尽量用Map,这样扩展性好。比如一开始只传name,后来要加age,直接往Map里加字段就行,不用改方法签名。
  • 错误码要规范,别随便传个字符串。我一般用UNAVAILABLEPERMISSION_DENIEDINVALID_ARGUMENT这种语义明确的。
  • 调试时打印日志,两端都打。Dart端用debugPrint,Android端用Log.d。出了问题能快速定位是哪个环节。

MethodChannel这块内容,说白了就是「约定大于配置」。两端约定好通道名、方法名、参数结构,剩下的就是Flutter框架帮你搞定。但约定一旦出错,调试起来确实头疼。所以我的建议是——先写一个最简单的Hello World跑通,再逐步加复杂逻辑。

好了,MethodChannel的基础就讲到这里。你先把上面的代码敲一遍,感受一下Dart和Android之间是怎么「对话」的。下一节咱们会深入MethodChannel的底层原理,看看消息到底是怎么编码和解码的。


专注资料整理