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

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

不知道你们在做 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 界面也不会卡顿,用户体验丝滑依旧。

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

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

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

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

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

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

Dart 类型 原生端收到的类型 (C 语言 GObject 示例)
null FlValue() (空值)
bool FlValue(bool)
int FlValue(int64_t)
double FlValue(double)
String FlValue(gchar*)
Uint8List FlValue(uint8_t*) (字节数组)
List FlValue(FlValue) (列表/数组)
Map FlValue(FlValue, FlValue) (字典/哈希表)

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

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

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

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

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

dart 复制代码
// 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 版本)。

kotlin 复制代码
// 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

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 实现的原生功能是什么呢?去试试吧!

相关推荐
七灵微17 分钟前
【后端】单点登录
服务器·前端
持久的棒棒君4 小时前
npm安装electron下载太慢,导致报错
前端·electron·npm
why1515 小时前
微服务商城-商品微服务
数据库·后端·golang
crary,记忆6 小时前
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
前端·webpack·angular·angular.js
漂流瓶jz7 小时前
让数据"流动"起来!Node.js实现流式渲染/流式传输与背后的HTTP原理
前端·javascript·node.js
SamHou07 小时前
手把手 CSS 盒子模型——从零开始的奶奶级 Web 开发教程2
前端·css·web
我不吃饼干7 小时前
从 Vue3 源码中了解你所不知道的 never
前端·typescript
結城7 小时前
mybatisX的使用,简化springboot的开发,不用再写entity、mapper以及service了!
java·spring boot·后端
开航母的李大7 小时前
【中间件】Web服务、消息队列、缓存与微服务治理:Nginx、Kafka、Redis、Nacos 详解
前端·redis·nginx·缓存·微服务·kafka
Bruk.Liu7 小时前
《Minio 分片上传实现(基于Spring Boot)》
前端·spring boot·minio