平台能力与原生互通篇(1/6): Flutter 与 iOS/Android 通信

5 阅读5分钟

Flutter 与 iOS/Android 通信(MethodChannel 全链路):从 0 到可上线

系列:平台能力与原生互通篇(1/6)
标签建议:Flutter MethodChannel iOS Android 跨端通信

在业务里,Flutter 不可能永远“纯 Dart”。你迟早要接原生能力:设备信息、系统设置页、相册/蓝牙/定位、厂商 SDK、支付、推送……
这篇用一个完整案例把链路走通:Flutter 发起调用 -> Android/iOS 原生处理 -> 回传结果 -> Flutter 统一错误处理与封装


1. 问题背景:业务场景 + 现象

业务场景

我们需要实现一个“设备能力中心”:

  1. 获取设备基础信息(品牌、系统版本、机型)
  2. 打开系统设置页面(用于权限引导)
  3. 原生返回结构化结果,Flutter 统一展示

常见现象(你可能已经踩过)

  • Flutter 侧写了 invokeMethod,原生没收到(MissingPluginException)。
  • Android 和 iOS 返回字段不一致,Dart 解析分支越来越多。
  • 原生抛异常,Flutter 只拿到一段字符串,无法区分“用户取消”“系统不支持”“参数错误”。
  • 需求迭代后,Channel 名字散落各文件,重构容易断链。

2. 原因分析:核心原理 + 排查过程

MethodChannel 本质

MethodChannelFlutter 与 Native 的 RPC(远程调用)通道

  • Flutter:invokeMethod(method, arguments)
  • Native:注册 setMethodCallHandler,按 call.method 分发
  • Native 回传:result.success(data) / result.error(code, message, details) / result.notImplemented()
  • Flutter 拿到 Future 结果或异常 PlatformException

为什么容易出问题

  1. 通道名不一致:Flutter 与原生字符串必须完全一致。
  2. 注册时机错误:插件/通道没在引擎正确注册。
  3. 协议没定义:字段名、错误码、参数校验没有统一规范。
  4. 异步线程处理混乱:原生耗时逻辑没回主线程、重复回调 result

排查顺序(实战建议)

  1. 先确认 Channel 名:Flutter / Android / iOS 三处逐字对比。
  2. 确认 Native Handler 是否走到(打日志)。
  3. 看 Flutter 侧异常类型:MissingPluginException 还是 PlatformException
  4. 再查参数序列化:Map/List 基本类型是否可编解码。

3. 解决方案:方案对比 + 最终选择

可选方案

  • MethodChannel:请求-响应,最适合“我调一次你回一次”。
  • EventChannel:持续事件流(如传感器、下载进度、推送前台消息)。
  • BasicMessageChannel:自由消息体,双向消息更灵活。

这篇的最终选择

  • 主链路用 MethodChannel(设备信息 + 打开设置页)。
  • 约定统一协议:
    • 成功返回:Map<String, dynamic>
    • 失败返回:PlatformException(code/message/details)
    • 错误码统一(示例):INVALID_ARGS / UNAVAILABLE / SYSTEM_ERROR

4. 关键代码:最小必要但完整可跑

下面代码按“工程化可维护”组织,你可以直接复制改名。


4.1 Flutter 侧:通道常量 + API 封装 + 统一错误

lib/platform/native_channels.dart
class NativeChannels {
  // 建议统一前缀:公司/项目/模块
  static const String deviceChannel = 'com.yckj.xjl/device';
}
lib/platform/device_models.dart
class DeviceInfoModel {
  final String platform; // android / ios
  final String brand;    // Android: MANUFACTURER, iOS: Apple
  final String model;
  final String osVersion;

  DeviceInfoModel({
    required this.platform,
    required this.brand,
    required this.model,
    required this.osVersion,
  });

  factory DeviceInfoModel.fromMap(Map<String, dynamic> map) {
    return DeviceInfoModel(
      platform: (map['platform'] ?? '') as String,
      brand: (map['brand'] ?? '') as String,
      model: (map['model'] ?? '') as String,
      osVersion: (map['osVersion'] ?? '') as String,
    );
  }
}
lib/platform/native_exceptions.dart
import 'package:flutter/services.dart';

class NativeError implements Exception {
  final String code;
  final String message;
  final dynamic details;

  NativeError({
    required this.code,
    required this.message,
    this.details,
  });

  factory NativeError.fromPlatformException(PlatformException e) {
    return NativeError(
      code: e.code,
      message: e.message ?? 'Unknown native error',
      details: e.details,
    );
  }

  @override
  String toString() => 'NativeError(code: $code, message: $message, details: $details)';
}
lib/platform/device_native_service.dart
import 'package:flutter/services.dart';
import 'native_channels.dart';
import 'device_models.dart';
import 'native_exceptions.dart';

class DeviceNativeService {
  static const MethodChannel _channel = MethodChannel(NativeChannels.deviceChannel);

  Future<DeviceInfoModel> getDeviceInfo() async {
    try {
      final result = await _channel.invokeMapMethod<String, dynamic>('getDeviceInfo');
      if (result == null) {
        throw NativeError(code: 'EMPTY_RESULT', message: 'Native returned null');
      }
      return DeviceInfoModel.fromMap(result);
    } on PlatformException catch (e) {
      throw NativeError.fromPlatformException(e);
    }
  }

  Future<void> openAppSettings() async {
    try {
      await _channel.invokeMethod('openAppSettings');
    } on PlatformException catch (e) {
      throw NativeError.fromPlatformException(e);
    }
  }
}
Flutter UI 调用示例(可放页面里)
final service = DeviceNativeService();

Future<void> loadInfo() async {
  try {
    final info = await service.getDeviceInfo();
    // TODO: setState / Riverpod state update
    print('Device: ${info.platform} ${info.brand} ${info.model} ${info.osVersion}');
  } catch (e) {
    print('loadInfo error: $e');
  }
}

Future<void> toSettings() async {
  try {
    await service.openAppSettings();
  } catch (e) {
    print('open settings error: $e');
  }
}

4.2 Android 侧(Kotlin):MethodChannel Handler

文件:android/app/src/main/kotlin/<your_package>/MainActivity.kt

package com.yourcompany.yourapp

import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.yckj.xjl/device"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
                when (call.method) {
                    "getDeviceInfo" -> handleGetDeviceInfo(result)
                    "openAppSettings" -> handleOpenAppSettings(result)
                    else -> result.notImplemented()
                }
            }
    }

    private fun handleGetDeviceInfo(result: MethodChannel.Result) {
        try {
            val data = hashMapOf(
                "platform" to "android",
                "brand" to (Build.MANUFACTURER ?: "unknown"),
                "model" to (Build.MODEL ?: "unknown"),
                "osVersion" to (Build.VERSION.RELEASE ?: "unknown")
            )
            result.success(data)
        } catch (e: Exception) {
            result.error(
                "SYSTEM_ERROR",
                "Failed to get device info on Android",
                e.message
            )
        }
    }

    private fun handleOpenAppSettings(result: MethodChannel.Result) {
        try {
            val intent = Intent(
                Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
                Uri.parse("package:$packageName")
            )
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            startActivity(intent)
            result.success(null)
        } catch (e: Exception) {
            result.error(
                "UNAVAILABLE",
                "Cannot open app settings on Android",
                e.message
            )
        }
    }
}

4.3 iOS 侧(Swift):MethodChannel Handler

文件:ios/Runner/AppDelegate.swift

import UIKit
import Flutter

@main
@objc class AppDelegate: FlutterAppDelegate {
  private let channelName = "com.yckj.xjl/device"

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel(
      name: channelName,
      binaryMessenger: controller.binaryMessenger
    )

    channel.setMethodCallHandler { [weak self] call, result in
      switch call.method {
      case "getDeviceInfo":
        self?.handleGetDeviceInfo(result: result)
      case "openAppSettings":
        self?.handleOpenAppSettings(result: result)
      default:
        result(FlutterMethodNotImplemented)
      }
    }

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  private func handleGetDeviceInfo(result: @escaping FlutterResult) {
    let device = UIDevice.current
    let data: [String: Any] = [
      "platform": "ios",
      "brand": "Apple",
      "model": device.model,
      "osVersion": device.systemVersion
    ]
    result(data)
  }

  private func handleOpenAppSettings(result: @escaping FlutterResult) {
    guard let url = URL(string: UIApplication.openSettingsURLString) else {
      result(
        FlutterError(
          code: "UNAVAILABLE",
          message: "Invalid settings URL",
          details: nil
        )
      )
      return
    }

    if UIApplication.shared.canOpenURL(url) {
      UIApplication.shared.open(url, options: [:]) { success in
        if success {
          result(nil)
        } else {
          result(
            FlutterError(
              code: "UNAVAILABLE",
              message: "Failed to open iOS settings",
              details: nil
            )
          )
        }
      }
    } else {
      result(
        FlutterError(
          code: "UNAVAILABLE",
          message: "Settings not available",
          details: nil
        )
      )
    }
  }
}

4.4 一步进阶:参数传递与校验(以 vibrate 为例)

Flutter
await _channel.invokeMethod('vibrate', {'milliseconds': 80});
Android(示意)
"vibrate" -> {
    val ms = call.argument<Int>("milliseconds") ?: 50
    if (ms <= 0) {
        result.error("INVALID_ARGS", "milliseconds must be > 0", null)
        return@setMethodCallHandler
    }
    // TODO: Vibrator 逻辑
    result.success(null)
}
iOS(示意)
case "vibrate":
  guard let args = call.arguments as? [String: Any],
        let ms = args["milliseconds"] as? Int, ms > 0 else {
    result(FlutterError(code: "INVALID_ARGS", message: "milliseconds must be > 0", details: nil))
    return
  }
  // TODO: 触发震动
  result(nil)

5. 效果验证:数据/截图/日志

你可以这样验证“链路真实可用”:

验证项 1:通道可达

  • Flutter 调用 getDeviceInfo,打印完整信息。
  • Android Logcat / Xcode Console 能看到进入 handler 的日志。

验证项 2:错误回传规范

  • 人为传错参数(如 milliseconds = -1)。
  • Flutter 应收到 NativeError(code: INVALID_ARGS, ...),而不是笼统字符串。

验证项 3:双端字段一致

  • Android / iOS 都返回:platform brand model osVersion 四个键。
  • Flutter 不需要平台分支解析。

建议日志格式

[NATIVE][device][req] method=getDeviceInfo args={}
[NATIVE][device][resp] success data={...}
[NATIVE][device][resp] error code=INVALID_ARGS message=...

6. 可复用结论:通用经验 + 避坑清单

通用经验

  1. 先定协议再写代码:方法名、参数、返回结构、错误码先写成文档。
  2. Flutter 只依赖服务层:页面不要直接 MethodChannel,统一进 DeviceNativeService
  3. 错误码要可运营INVALID_ARGSUNAVAILABLESYSTEM_ERROR 能直接映射埋点/告警。
  4. 跨端字段要“最小稳定集”:宁可字段少,也别今天 modelName 明天 deviceModel

避坑清单

  • Channel 名三端不一致。
  • Native 端回调 result 多次(会崩或报错)。
  • 在耗时线程里直接操作 UI / Activity。
  • Flutter 侧直接 dynamic 强转,不做空值保护。
  • 方法不断增多却没有“模块化 Channel”(后期维护灾难)。

附:什么时候该从 MethodChannel 升级到插件化?

当你出现以下任意两条,建议提炼成独立 Flutter Plugin:

  • 多个 App 共用同一原生能力
  • iOS/Android 实现逻辑已超过 300 行
  • 需要对外暴露可复用 API(含示例与测试)
  • 版本升级频繁,需要明确发布节奏

下期预告

第二篇会写:iOS 真机调试常见问题(flutter run vs Xcode),重点讲清:

  • 为什么 flutter run 能跑但 Xcode Archive 失败
  • 签名、Provisioning、Pods、Build Config 差异
  • 一套稳定的“调试/打包双轨流程”