3. MethodChannel基础:Dart端发送消息、Android端接收消息、基本数据类型传递
好,咱们今天来啃MethodChannel这块硬骨头。说实话,这是Flutter与原生交互最核心的通道,没有之一。我最早接触Flutter插件开发时,就是被MethodChannel这个概念给绕晕的——后来想明白了,它就是个「消息管道」,Dart和原生各守一边,通过这个管道传数据、调方法。
核心思想:MethodChannel就像一根电话线。Dart端拨号(调用方法),Android端接听(处理调用),然后回传结果。就这么简单。
3.1 什么是MethodChannel?
MethodChannel是Flutter提供的双向通信机制。它允许Dart代码调用原生代码,也允许原生代码主动给Dart发消息。但咱们这节课先聚焦前半部分——Dart主动调用,Android被动响应。
它的工作流程,我画了张图,你一看就明白:
这张图把整个流程讲清楚了。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时的小习惯:
- 方法名用驼峰命名,比如
getBatteryLevel、updateUserInfo。两端保持一致。 - 参数尽量用Map,这样扩展性好。比如一开始只传name,后来要加age,直接往Map里加字段就行,不用改方法签名。
- 错误码要规范,别随便传个字符串。我一般用
UNAVAILABLE、PERMISSION_DENIED、INVALID_ARGUMENT这种语义明确的。 - 调试时打印日志,两端都打。Dart端用
debugPrint,Android端用Log.d。出了问题能快速定位是哪个环节。
MethodChannel这块内容,说白了就是「约定大于配置」。两端约定好通道名、方法名、参数结构,剩下的就是Flutter框架帮你搞定。但约定一旦出错,调试起来确实头疼。所以我的建议是——先写一个最简单的Hello World跑通,再逐步加复杂逻辑。
好了,MethodChannel的基础就讲到这里。你先把上面的代码敲一遍,感受一下Dart和Android之间是怎么「对话」的。下一节咱们会深入MethodChannel的底层原理,看看消息到底是怎么编码和解码的。