Flutter 调用原生代码,看这篇就够了:从零教你搭起通信的桥

181 阅读7分钟

嘿,兄弟们,我是你们的老朋友,一个混迹全栈江湖多年的大前端架构师小张。

不知道你们在做 Flutter 开发时,有没有遇到过这样的场景:你的 App 界面用 Flutter 写得飞起,动画流畅,逻辑清晰。但产品经理突然走过来说:“咱们加个功能,实时显示手机当前的电量吧!” 或者 “我们要做一个功能,需要用到 Android 特有的一个系统服务。”

你心里一咯噔,这玩意儿 Dart 可直接搞不定啊!它得去调用 Android 或 iOS 系统的原生 API 才行。这时候,你是不是有点懵?感觉 Flutter 的跨平台能力好像在这里“断了层”?

别慌。今天,我就带你彻底搞懂这个问题。学完这篇文章,你就能掌握一项核心技能:在 Flutter 和原生平台之间,搭起一座坚固的通信桥梁。以后再遇到类似需求,你就能笑着说:“小意思,放着我来!”

核心思想:一座看不见的“跨界大桥”

想象一下,你的 Flutter 应用(运行在 Dart 虚拟机里)和原生平台(Android 或 iOS)是两个独立的“国家”。它们语言不通,一个说 Dart,一个说 Kotlin/Java 或者 Swift/Objective-C。想让它们对话,就得有个“大使馆”或者“翻译官”。

在 Flutter 的世界里,这个翻译官就叫做 Platform Channels(平台通道)

说白了,Platform Channels 就是 Flutter 提供的一套机制,允许你的 Dart 代码向原生平台发送消息,并接收原生平台返回的结果。这个过程是异步的,这样可以保证即使原生代码在执行一些耗时操作(比如读取文件、访问硬件),你的 App 界面也不会卡顿,用户体验丝滑依旧。

它的工作流程可以用下面这张图来简单表示:

image.png 你看,整个过程就像一次流畅的委托和汇报。Flutter 端发出请求,原生端处理后返回结果,中间的 Platform Channel 就是那个可靠的信使。

这座桥上能运送什么“货物”?

既然是通信,那我们得知道能在通道里传递什么类型的数据。总不能什么都一股脑儿往里扔吧?

Flutter 的标准平台通道使用了一种叫做 StandardMessageCodec 的编解码器。它非常高效,能处理我们日常开发中最常用的数据类型,基本上就是 JSON 能干的事,它都能干。

我给你列个表,看看 Dart 的数据类型和原生平台是怎么对应的(以 C 语言为例,其他平台类似):

Dart 类型原生端收到的类型 (C 语言 GObject 示例)
nullFlValue() (空值)
boolFlValue(bool)
intFlValue(int64_t)
doubleFlValue(double)
StringFlValue(gchar*)
Uint8ListFlValue(uint8_t*) (字节数组)
ListFlValue(FlValue) (列表/数组)
MapFlValue(FlValue, FlValue) (字典/哈希表)

基本上,只要你的数据是这些基础类型或者由它们组合成的列表和字典,就能在这座桥上畅通无阻。

实战演练:三步获取手机电量

光说不练假把式。接下来,我们就跟着官方文档的例子,一步步实现前面提到的“获取手机电量”功能。Talk is cheap, show me the code!

第一步:在 Flutter 端建立“呼叫中心”

首先,在你的 Flutter 页面代码里,我们需要创建一个 MethodChannel。你可以把它理解成一条专线电话,它需要一个独一无二的名字,以免和 App 里其他的通道“串线”。官方推荐用域名反转的方式来命名,比如 samples.flutter.dev/battery

// main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class _MyHomePageState extends State<MyHomePage> {
  // 1. 定义平台通道,名字要和原生端保持一致
  static const platform = MethodChannel('samples.flutter.dev/battery');

  String _batteryLevel = '未知电量';

  // 2. 定义一个异步方法来调用原生功能
  Future<void> _getBatteryLevel() async {
    String batteryLevel;
    try {
      // 3. 使用 invokeMethod 发起调用,'getBatteryLevel' 是我们约定的方法名
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = '当前电量: $result %';
    } on PlatformException catch (e) {
      // 4. 如果原生端出错(比如模拟器不支持),会抛出异常,必须捕获
      batteryLevel = "获取电量失败: '${e.message}'.";
    }

    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

  @override
  Widget build(BuildContext context) {
    // ... UI 代码,一个按钮和一个显示电量的文本 ...
    return Material(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            ElevatedButton(
              onPressed: _getBatteryLevel,
              child: const Text('获取电量'),
            ),
            Text(_batteryLevel),
          ],
        ),
      ),
    );
  }
}

看,Dart 端的逻辑很清晰:建通道、发请求、处理结果(包括成功和失败)。try-catch 非常重要,因为原生调用随时可能因为各种原因失败。

第二步:在 Android 端实现“接线员”

现在,我们去 Android 项目里,让它能响应我们的呼叫。

打开 android/app/src/main/kotlin/.../MainActivity.kt (或者 Java 版本)。

// MainActivity.kt (Kotlin 示例)
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES

class MainActivity: FlutterActivity() {
    // 1. 定义通道名,必须和 Flutter 端完全一样
    private val CHANNEL = "samples.flutter.dev/battery"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        // 2. 创建 MethodChannel 实例,并设置 MethodCallHandler
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            // 3. 判断 Flutter 调用的是哪个方法
            if (call.method == "getBatteryLevel") {
                val batteryLevel = getBatteryLevel()

                if (batteryLevel != -1) {
                    // 4. 调用成功,返回结果
                    result.success(batteryLevel)
                } else {
                    // 5. 调用失败,返回错误信息
                    result.error("UNAVAILABLE", "Battery level not available.", null)
                }
            } else {
                // 6. 如果是未实现的方法,告知 Flutter
                result.notImplemented()
            }
        }
    }

    // 这是纯粹的原生 Android 代码,用来获取电量
    private fun getBatteryLevel(): Int {
        val batteryLevel: Int
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }
        return batteryLevel
    }
}

Android 端的逻辑也很直接:监听来自特定通道的呼叫,根据方法名(call.method)执行相应的原生代码,然后通过 result 对象把成功或失败的结果传回去。

第三步:在 iOS 端实现“接线员”

iOS 端的流程大同小异,只是语法换成了 Swift。

打开 ios/Runner/AppDelegate.swift

// AppDelegate.swift
import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    // 1. 获取 FlutterViewController,它是 Flutter 和 iOS 之间的关键连接点
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    // 2. 定义通道名,必须和 Flutter 端完全一样
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)

    // 3. 设置 MethodCallHandler
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // 4. 判断方法名
      guard call.method == "getBatteryLevel" else {
        result(FlutterMethodNotImplemented)
        return
      }
      // 5. 调用原生方法并返回结果
      self.receiveBatteryLevel(result: result)
    })

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

  // 这是纯粹的原生 iOS 代码,用来获取电量
  private func receiveBatteryLevel(result: FlutterResult) {
    let device = UIDevice.current
    device.isBatteryMonitoringEnabled = true
    if device.batteryState == .unknown {
      result(FlutterError(code: "UNAVAILABLE",
                          message: "Battery level not available.",
                          details: nil))
    } else {
      result(Int(device.batteryLevel * 100))
    }
  }
}

现在,你可以在 Android 和 iOS设备上运行你的 App 了。点击按钮,就能看到各自平台返回的真实电量。一座跨平台的通信桥梁就这么搭好了!

进阶选择:用 Pigeon 生成“安全通道”

手动写 Platform Channel 很灵活,但也有缺点。当方法和参数一多,你需要在 Dart、Kotlin/Java、Swift/OC 三个地方手动保持方法名、参数类型和数量的一致,很容易出错,而且是运行时错误,调试起来很头疼。

为了解决这个问题,Flutter 团队推出了一个神器:Pigeon

Pigeon 是一个代码生成工具。你只需要定义一个 Dart 文件,描述清楚你要通信的接口(方法名、参数、返回值),Pigeon 就能自动为你生成 Dart、Kotlin/Java 和 Swift/Objective-C 的所有模板代码。

特性 / 对比项手动 Platform Channel使用 Pigeon
开发效率低,需要手写三端代码,容易出错高,只需定义一次接口,自动生成模板代码
类型安全,依赖字符串匹配,参数类型靠自觉强类型安全,编译器会检查,错误在编译期暴露
维护成本高,修改接口需要同步修改三处代码低,修改接口定义后,重新运行生成命令即可
学习曲线较低,概念直接略高,需要学习 Pigeon 的接口定义语法和命令行工具
适用场景简单、少量的通信复杂、多接口、需要长期维护的插件或项目

对于复杂的项目,或者你想把原生功能封装成一个可复用的插件,我强烈推荐使用 Pigeon。它能帮你省去大量重复劳动,并从根本上保证通信的可靠性。

写在最后

现在,回到我们开头的问题。当你的 Flutter 应用需要调用原生功能时,你不再是一个无助的开发者了。你是一名掌握了“建桥”技术的工程师。

Platform Channel 是 Flutter 强大跨平台能力的重要补充,它让你既能享受 Flutter 带来的开发效率和一致性体验,又不会被平台限制束缚住手脚,可以随时调用底层平台的全部能力。

这把“钥匙”,你现在已经拿到了。下一个你想用 Flutter 实现的原生功能是什么呢?去试试吧!