4. MethodChannel进阶:异步方法调用、多参数传递、复杂对象序列化、错误处理与异常传递
好,咱们继续往下走。上一章我们把 MethodChannel 的基本用法捋了一遍,说白了就是 Dart 端发个消息,原生端接住,再回个结果。但实际项目里,哪有这么简单?
你想想看,真实场景下,你要传一个用户对象过去,或者要调用一个需要 3 秒才能返回结果的 native 方法,再或者原生端突然抛了个异常……这些情况怎么处理?
这一章,我就把 MethodChannel 的进阶玩法一次性讲透。内容有点干,但都是我在项目里踩过的坑,你耐心看完,绝对值得。
4.1 异步方法调用:别让 UI 线程卡死
先聊异步。Flutter 是单线程模型,UI 线程一旦被阻塞,页面直接掉帧甚至白屏。所以,原生端那些耗时操作——比如读取文件、网络请求、数据库查询——必须异步执行。
我个人习惯的做法是:在原生端用 async/await 或者回调,把结果延迟返回给 Flutter。Dart 端这边,调用方法时加上 await,或者用 .then() 处理结果。
来看一个 Android 端的例子。假设我们要从原生端读取一个很大的文件:
// Android - MainActivity.kt
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.file").setMethodCallHandler { call, result ->
when (call.method) {
"readLargeFile" -> {
// 这里必须异步,否则会卡死 UI
Thread {
try {
val content = readFileFromDisk() // 耗时操作
// 回到主线程返回结果
Handler(Looper.getMainLooper()).post {
result.success(content)
}
} catch (e: Exception) {
Handler(Looper.getMainLooper()).post {
result.error("FILE_ERROR", e.message, null)
}
}
}.start()
}
else -> result.notImplemented()
}
}
}
嗯,这里要注意:result.success() 和 result.error() 必须在主线程调用。我刚开始做的时候,直接在子线程里调了 result.success(),结果 Flutter 端死活收不到回调,排查了半天才发现是线程问题。
Dart 端调用就很简单了:
// Dart
String content = await platform.invokeMethod('readLargeFile');
print('文件内容: $content');
iOS 端也是类似的思路,用 GCD 切到后台线程,处理完再切回主线程:
// iOS - AppDelegate.swift
let channel = FlutterMethodChannel(name: "com.example.file", binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { [weak self] (call, result) in
if call.method == "readLargeFile" {
DispatchQueue.global().async {
let content = self?.readFileFromDisk() ?? ""
DispatchQueue.main.async {
result(content)
}
}
} else {
result(FlutterMethodNotImplemented)
}
}
GlobalScope.launch(Dispatchers.IO) 来切线程,但记得最后用 withContext(Dispatchers.Main) 返回结果。我个人更推荐用协程,代码会清爽很多。
4.2 多参数传递:别再拼字符串了
很多新手传多个参数时,喜欢把参数拼成一个字符串,比如 "name=张三&age=18",然后在原生端再解析。说实话,这种做法太原始了,而且容易出错。
MethodChannel 原生支持传递 Map 和 List,这才是正道。你想想看,直接传一个 Map 过去,原生端按 key 取值,多清晰。
Dart 端这样写:
// Dart
Map<String, dynamic> userInfo = {
'name': '张三',
'age': 25,
'isVip': true,
'scores': [98, 87, 92]
};
String result = await platform.invokeMethod('submitUserInfo', userInfo);
Android 端接收:
// Android
when (call.method) {
"submitUserInfo" -> {
val name = call.argument<String>("name") ?: ""
val age = call.argument<Int>("age") ?: 0
val isVip = call.argument<Boolean>("isVip") ?: false
val scores = call.argument<List<Int>>("scores") ?: emptyList()
// 处理业务逻辑...
result.success("ok")
}
}
iOS 端接收:
// iOS
if call.method == "submitUserInfo" {
guard let args = call.arguments as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGS", message: "参数格式错误", details: nil))
return
}
let name = args["name"] as? String ?? ""
let age = args["age"] as? Int ?? 0
let isVip = args["isVip"] as? Bool ?? false
let scores = args["scores"] as? [Int] ?? []
// 处理业务逻辑...
result("ok")
}
4.3 复杂对象序列化:JSON 是万能钥匙
传个 Map 还好说,但如果你要传一个嵌套好几层的复杂对象呢?比如一个订单对象,里面包含商品列表、地址信息、优惠券……这时候再用 Map 一层层嵌套,代码会变得非常难看。
我的做法是:在 Dart 端把对象序列化成 JSON 字符串,原生端再反序列化。反过来也一样。
举个例子,假设我们有一个 User 类:
// Dart
class User {
final int id;
final String name;
final List<String> tags;
User({required this.id, required this.name, required this.tags});
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'tags': tags,
};
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'],
name: json['name'],
tags: List<String>.from(json['tags']),
);
}
发送时:
// Dart
User user = User(id: 1, name: '李四', tags: ['vip', 'new_user']);
String jsonStr = jsonEncode(user.toJson());
String result = await platform.invokeMethod('processUser', jsonStr);
Android 端用 Gson 或 Kotlinx.serialization 反序列化:
// Android
when (call.method) {
"processUser" -> {
val jsonStr = call.arguments as String
val user = Gson().fromJson(jsonStr, User::class.java)
// 处理 user 对象...
result.success("ok")
}
}
iOS 端用 Codable:
// iOS
if call.method == "processUser" {
guard let jsonStr = call.arguments as? String,
let jsonData = jsonStr.data(using: .utf8),
let user = try? JSONDecoder().decode(User.self, from: jsonData) else {
result(FlutterError(code: "PARSE_ERROR", message: "JSON解析失败", details: nil))
return
}
// 处理 user 对象...
result("ok")
}
toJson() → jsonEncode → 原生端 jsonDecode → 原生对象。反过来也一样。JSON 字符串是跨平台的通用语言。
4.4 错误处理与异常传递:别让 App 静默崩溃
这是最容易被忽视的部分。很多新手写插件时,原生端出了异常,直接 result.success(null) 或者干脆不调 result。结果 Flutter 端一直等着,最后超时崩溃。
正确的做法是:原生端捕获所有异常,通过 result.error() 把错误信息传回 Dart 端。
来看一个完整的错误处理模板:
// Android
when (call.method) {
"riskyOperation" -> {
try {
val data = doSomethingRisky() // 可能抛异常
result.success(data)
} catch (e: SecurityException) {
result.error("PERMISSION_DENIED", "没有权限", e.localizedMessage)
} catch (e: IOException) {
result.error("IO_ERROR", "读写失败", e.localizedMessage)
} catch (e: Exception) {
result.error("UNKNOWN", "未知错误", e.localizedMessage)
}
}
}
Dart 端用 try-catch 捕获:
// Dart
try {
var data = await platform.invokeMethod('riskyOperation');
print('成功: $data');
} on PlatformException catch (e) {
print('错误码: ${e.code}');
print('错误消息: ${e.message}');
print('错误详情: ${e.details}');
// 根据错误码做不同处理
if (e.code == 'PERMISSION_DENIED') {
// 引导用户去设置里开权限
} else if (e.code == 'IO_ERROR') {
// 重试或者提示用户
}
} catch (e) {
print('其他异常: $e');
}
iOS 端也一样:
// iOS
if call.method == "riskyOperation" {
do {
let data = try doSomethingRisky()
result(data)
} catch let error as NSError {
result(FlutterError(code: "\(error.code)", message: error.domain, details: error.localizedDescription))
}
}
result.error()。结果 Flutter 端一直挂起,用户点了个按钮,App 就卡死了。记住:每一个方法调用,最终都必须调用一次 result,要么 success,要么 error,要么 notImplemented。千万别漏。
4.5 综合实战:一个带错误处理的用户登录插件
最后,我把前面讲的知识点串起来,写一个完整的登录插件示例。这个例子包含了异步调用、多参数传递、JSON 序列化、错误处理,你直接拿去用都没问题。
Dart 端:
// Dart
class LoginPlugin {
static const _channel = MethodChannel('com.example.login');
static Future<User> login(String username, String password) async {
try {
Map<String, dynamic> credentials = {
'username': username,
'password': password,
};
String resultJson = await _channel.invokeMethod('login', credentials);
Map<String, dynamic> userMap = jsonDecode(resultJson);
return User.fromJson(userMap);
} on PlatformException catch (e) {
if (e.code == 'NETWORK_ERROR') {
throw Exception('网络连接失败,请检查网络');
} else if (e.code == 'AUTH_FAILED') {
throw Exception('用户名或密码错误');
} else {
throw Exception('登录失败: ${e.message}');
}
}
}
}
Android 端:
// Android
when (call.method) {
"login" -> {
val username = call.argument<String>("username") ?: ""
val password = call.argument<String>("password") ?: ""
// 异步执行网络请求
Thread {
try {
val user = networkService.login(username, password)
val json = Gson().toJson(user)
Handler(Looper.getMainLooper()).post {
result.success(json)
}
} catch (e: IOException) {
Handler(Looper.getMainLooper()).post {
result.error("NETWORK_ERROR", e.message, null)
}
} catch (e: AuthException) {
Handler(Looper.getMainLooper()).post {
result.error("AUTH_FAILED", e.message, null)
}
}
}.start()
}
}
你看,这样写出来的插件,健壮性就好很多了。用户登录失败时,Flutter 端能拿到具体的错误原因,然后给出友好的提示,而不是直接崩溃或者卡死。
好了,这一章的内容就到这里。异步调用、多参数、JSON 序列化、错误处理,这四个点你掌握好了,MethodChannel 的进阶用法基本就吃透了。下一章我们会聊 EventChannel,也就是原生端主动给 Flutter 发消息的场景——比如监听传感器数据、电池电量变化等。到时候见。