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)
    }
}
小提示:如果你用的是 Kotlin 协程,可以用 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")
}
注意:Map 的 key 必须是 String 类型。value 的类型支持有限:int、double、bool、String、List、Map 以及它们的 null 值。自定义对象不行,得先序列化。

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")
}
核心思路:Dart 端 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))
    }
}
避坑指南:我曾经犯过一个错误——在原生端 catch 了异常,但忘记调 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 端能拿到具体的错误原因,然后给出友好的提示,而不是直接崩溃或者卡死。

我的建议:每个插件方法都按照「try-catch → result.error() → Dart 端 try-catch」这个模式来写。一开始可能觉得麻烦,但上线后你会感谢自己的。我在生产环境里见过太多因为没做错误处理而导致的线上事故了。

好了,这一章的内容就到这里。异步调用、多参数、JSON 序列化、错误处理,这四个点你掌握好了,MethodChannel 的进阶用法基本就吃透了。下一章我们会聊 EventChannel,也就是原生端主动给 Flutter 发消息的场景——比如监听传感器数据、电池电量变化等。到时候见。