Skip to main content

编写自定义平台特定代码

本指南介绍如何编写自定义平台特定代码。 某些平台特定功能可通过现有软件包获得; 请参阅使用软件包。

Flutter 使用灵活的系统,允许您使用直接与这些 API 协同工作的语言调用平台特定 API:

  • Android 上的 Kotlin 或 Java
  • iOS 上的 Swift 或 Objective-C
  • Windows 上的 C++
  • macOS 上的 Objective-C
  • Linux 上的 C

Flutter 内置的平台特定 API 支持 不依赖于代码生成, 而是依赖于灵活的消息传递方式。 或者,您可以使用Pigeon 软件包用于发送结构化类型安全消息 并进行代码生成:

  • 应用的 Flutter 部分通过平台通道向其 主机(应用的非 Dart 部分)发送消息。

  • 主机 在平台通道上监听并接收消息。 然后,它调用任意数量的平台特定 API(使用本机编程语言),并将响应发送回 客户端(应用的 Flutter 部分)。

架构概述:平台通道

#

如下图所示,使用平台通道在客户端(UI)和主机(平台)之间传递消息:

Platform channels architecture

为了确保用户界面保持响应,消息和响应是异步传递的。

在客户端,MethodChannel 允许发送与方法调用相对应的消息。在平台端, Android 上的 MethodChannel (MethodChannelAndroid) 和 iOS 上的 FlutterMethodChannel (MethodChanneliOS) 允许接收方法调用并发送回结果。这些类允许您开发几乎没有“样板”代码的平台插件。

平台通道数据类型支持和编解码器

#

标准平台通道使用标准消息编解码器,该编解码器支持对类似 JSON 的简单值的有效二进制序列化,例如布尔值、数字、字符串、字节缓冲区以及这些值的列表和映射(有关详细信息,请参阅StandardMessageCodec)。当您发送和接收值时,这些值的序列化和反序列化到消息和从消息中发生。

下表显示了 Dart 值如何在平台端接收以及反之亦然:

DartKotlin
nullnull
boolBoolean
int (<=32 bits)Int
int (>32 bits)Long
doubleDouble
StringString
Uint8ListByteArray
Int32ListIntArray
Int64ListLongArray
Float32ListFloatArray
Float64ListDoubleArray
ListList
MapHashMap
DartJava
nullnull
booljava.lang.Boolean
int (<=32 bits)java.lang.Integer
int (>32 bits)java.lang.Long
doublejava.lang.Double
Stringjava.lang.String
Uint8Listbyte[]
Int32Listint[]
Int64Listlong[]
Float32Listfloat[]
Float64Listdouble[]
Listjava.util.ArrayList
Mapjava.util.HashMap
DartSwift
nullnil (NSNull when nested)
boolNSNumber(value: Bool)
int (<=32 bits)NSNumber(value: Int32)
int (>32 bits)NSNumber(value: Int)
doubleNSNumber(value: Double)
StringString
Uint8ListFlutterStandardTypedData(bytes: Data)
Int32ListFlutterStandardTypedData(int32: Data)
Int64ListFlutterStandardTypedData(int64: Data)
Float32ListFlutterStandardTypedData(float32: Data)
Float64ListFlutterStandardTypedData(float64: Data)
ListArray
MapDictionary
DartObjective-C
nullnil (NSNull when nested)
boolNSNumber numberWithBool:
int (<=32 bits)NSNumber numberWithInt:
int (>32 bits)NSNumber numberWithLong:
doubleNSNumber numberWithDouble:
StringNSString
Uint8ListFlutterStandardTypedData typedDataWithBytes:
Int32ListFlutterStandardTypedData typedDataWithInt32:
Int64ListFlutterStandardTypedData typedDataWithInt64:
Float32ListFlutterStandardTypedData typedDataWithFloat32:
Float64ListFlutterStandardTypedData typedDataWithFloat64:
ListNSArray
MapNSDictionary
DartC++
nullEncodableValue()
boolEncodableValue(bool)
int (<=32 bits)EncodableValue(int32_t)
int (>32 bits)EncodableValue(int64_t)
doubleEncodableValue(double)
StringEncodableValue(std::string)
Uint8ListEncodableValue(std::vector<uint8_t>)
Int32ListEncodableValue(std::vector<int32_t>)
Int64ListEncodableValue(std::vector<int64_t>)
Float32ListEncodableValue(std::vector<float>)
Float64ListEncodableValue(std::vector<double>)
ListEncodableValue(std::vector<EncodableValue>)
MapEncodableValue(std::map<EncodableValue, EncodableValue>)
DartC (GObject)
nullFlValue()
boolFlValue(bool)
intFlValue(int64_t)
doubleFlValue(double)
StringFlValue(gchar*)
Uint8ListFlValue(uint8_t*)
Int32ListFlValue(int32_t*)
Int64ListFlValue(int64_t*)
Float32ListFlValue(float*)
Float64ListFlValue(double*)
ListFlValue(FlValue)
MapFlValue(FlValue, FlValue)

示例:使用平台通道调用平台特定代码

#

以下代码演示如何调用 平台特定 API 来检索和显示 当前电池电量。它使用 Android 的 BatteryManager API、 iOS 的 device.batteryLevel API、 Windows 的 GetSystemPowerStatus API 以及 Linux 的 UPower API,并使用单个 平台消息 getBatteryLevel()。

本示例在主应用程序本身内添加了平台特定的代码。如果您想将平台特定的代码重复用于多个应用程序, 项目创建步骤略有不同 (请参阅开发软件包), 但平台通道代码 仍然以相同的方式编写。

步骤 1:创建新的应用程序项目

#

首先创建一个新的应用程序:

  • 在终端中运行:flutter create batterylevel

默认情况下,我们的模板支持使用 Kotlin 编写 Android 代码, 或使用 Swift 编写 iOS 代码。要使用 Java 或 Objective-C, 请使用 -i 和/或 -a 标志:

  • 在终端中运行:flutter create -i objc -a java batterylevel

步骤 2:创建 Flutter 平台客户端

#

应用程序的 State 类保存当前应用程序状态。 将其扩展为保存当前电池状态。

首先,构造通道。使用具有单个返回电池电量平台方法的 MethodChannel。

通道的客户端和主机端通过在通道构造函数中传递的通道名称连接。单个应用程序中使用的所有通道名称必须 唯一;例如,使用唯一的“域前缀”作为通道名称的前缀:samples.flutter.dev/battery。

dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
dart
class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('samples.flutter.dev/battery');
  // 获取电池电量。

接下来,在方法通道上调用一个方法, 使用 String 标识符 getBatteryLevel 指定要调用的具体方法。 调用可能会失败——例如, 如果平台不支持 平台 API(例如在模拟器中运行时), 因此将 invokeMethod 调用包装在 try-catch 语句中。

使用返回的结果来更新 setState 内 _batteryLevel 中的用户界面状态。

dart
// 获取电池电量。
String _batteryLevel = '未知电池电量。';

Future<void> _getBatteryLevel() async {
  String batteryLevel;
  try {
    final result = await platform.invokeMethod<int>('getBatteryLevel');
    batteryLevel = '电池电量为 $result %。';
  } on PlatformException catch (e) {
    batteryLevel = "未能获取电池电量:'${e.message}'。";
  }

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

最后,替换模板中的 build 方法, 使其包含一个小的用户界面,该界面以字符串形式显示电池 状态,以及一个用于刷新值的按钮。

dart
@override
Widget build(BuildContext context) {
  return Material(
    child: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ElevatedButton(
            onPressed: _getBatteryLevel,
            child: const Text('获取电池电量'),
          ),
          Text(_batteryLevel),
        ],
      ),
    ),
  );
}

步骤 3:添加 Android 平台特定实现

#

首先在 Android Studio 中打开 Flutter 应用的 Android 主机部分:

  1. 启动 Android Studio

  2. 选择菜单项 文件 > 打开...

  3. 导航到包含 Flutter 应用的目录, 并在其中选择 android 文件夹。单击 确定。

  4. 在项目视图的 kotlin 文件夹中打开位于 kotlin 文件夹中的 MainActivity.kt 文件。

在 configureFlutterEngine() 方法中,创建一个 MethodChannel 并调用 setMethodCallHandler()。确保使用与 Flutter 客户端中使用的相同的通道名称。

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

class MainActivity: FlutterActivity() {
  private val CHANNEL = "samples.flutter.dev/battery"

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // 此方法在主线程上调用。
      // TODO
    }
  }
}

添加使用 Android 电池 API 检索电池电量的 Android Kotlin 代码。此代码与您 在原生 Android 应用中编写的代码完全相同。

首先,在文件顶部添加所需的导入:

MainActivity.kt
kotlin
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

接下来,在 MainActivity 类中添加以下方法, 位于 configureFlutterEngine() 方法下方:

MainActivity.kt
kotlin
  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
  }

最后,完成前面添加的 setMethodCallHandler() 方法。 您需要处理单个平台方法 getBatteryLevel(), 因此在 call 参数中对其进行测试。 此平台方法的实现调用 前面步骤中编写的 Android 代码,并使用 result 参数返回成功和错误情况的响应。 如果调用了未知方法,则改为报告该方法。

删除以下代码:

MainActivity.kt
kotlin
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // This method is invoked on the main thread.
      // TODO
    }

并替换为以下内容:

MainActivity.kt
kotlin
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      // 此方法在主线程上调用。
      call, result ->
      if (call.method == "getBatteryLevel") {
        val batteryLevel = getBatteryLevel()

        if (batteryLevel != -1) {
          result.success(batteryLevel)
        } else {
          result.error("UNAVAILABLE", "Battery level not available.", null)
        }
      } else {
        result.notImplemented()
      }
    }

首先在 Android Studio 中打开 Flutter 应用的 Android 主机部分:

  1. 启动 Android Studio

  2. 选择菜单项 文件 > 打开...

  3. 导航到包含 Flutter 应用的目录, 并在其中选择 android 文件夹。单击 确定。

  4. 在项目视图的 java 文件夹中打开 MainActivity.java 文件。

接下来,在 configureFlutterEngine() 方法中创建一个 MethodChannel 并设置 MethodCallHandler。 确保使用与 Flutter 客户端中使用的相同的通道名称。

MainActivity.java
java
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "samples.flutter.dev/battery";

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    super.configureFlutterEngine(flutterEngine);
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            // 此方法在主线程上调用。
            // TODO
          }
        );
  }
}

添加使用 Android 电池 API 检索电池电量的 Android Java 代码。此代码与您 在原生 Android 应用中编写的代码完全相同。

首先,在文件顶部添加所需的导入:

MainActivity.java
java
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;
import android.os.Bundle;

然后在 activity 类中添加以下内容作为新方法, 位于 configureFlutterEngine() 方法下方:

MainActivity.java
java
  private int getBatteryLevel() {
    int batteryLevel = -1;
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
      batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
    } else {
      Intent intent = new ContextWrapper(getApplicationContext()).
          registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
      batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
          intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
    }

    return batteryLevel;
  }

最后,完成前面添加的 setMethodCallHandler() 方法。 您需要处理单个平台方法 getBatteryLevel(), 因此在 call 参数中对其进行测试。此平台方法的实现调用 前面步骤中编写的 Android 代码,并使用 result 参数返回成功和错误情况的响应。 如果调用了未知方法,则改为报告该方法。

删除以下代码:

MainActivity.java
java
      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            // This method is invoked on the main thread.
            // TODO
          }
      );

并替换为以下内容:

MainActivity.java
java
      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            // 此方法在主线程上调用。
            if (call.method.equals("getBatteryLevel")) {
              int batteryLevel = getBatteryLevel();

              if (batteryLevel != -1) {
                result.success(batteryLevel);
              } else {
                result.error("UNAVAILABLE", "Battery level not available.", null);
              }
            } else {
              result.notImplemented();
            }
          }
      );

您现在应该能够在 Android 上运行该应用。如果使用 Android 模拟器,请在工具栏中从 ... 按钮访问的扩展控件面板中设置电池电量。

步骤 4:添加 iOS 平台特定实现

#

首先在 Xcode 中打开 Flutter 应用的 iOS 主机部分:

  1. 启动 Xcode。

  2. 选择菜单项 文件 > 打开...。

  3. 导航到包含 Flutter 应用的目录,然后选择其内部的 ios 文件夹。单击 确定。

在使用 Objective-C 的标准模板设置中添加对 Swift 的支持:

  1. 在项目导航器中展开 Runner > Runner。

  2. 在项目导航器中打开位于 Runner > Runner 下的 AppDelegate.swift 文件。

覆盖 application:didFinishLaunchingWithOptions: 函数并创建一个与通道名称 samples.flutter.dev/battery 绑定的 FlutterMethodChannel:

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

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
      // 此方法在 UI 线程上调用。
      // 处理电池消息。
    })

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

接下来,添加使用 iOS 电池 API 检索电池电量的 iOS Swift 代码。此代码与您 在原生 iOS 应用中编写的代码完全相同。

在 AppDelegate.swift 的底部添加以下内容作为新方法:

AppDelegate.swift
swift
private func receiveBatteryLevel(result: FlutterResult) {
  let device = UIDevice.current
  device.isBatteryMonitoringEnabled = true
  if device.batteryState == UIDevice.BatteryState.unknown {
    result(FlutterError(code: "UNAVAILABLE",
                        message: "Battery level not available.",
                        details: nil))
  } else {
    result(Int(device.batteryLevel * 100))
  }
}

最后,完成前面添加的 setMethodCallHandler() 方法。 您需要处理单个平台方法 getBatteryLevel(), 因此在 call 参数中对其进行测试。 此平台方法的实现调用 前面步骤中编写的 iOS 代码。如果调用了未知方法,则改为报告该方法。

AppDelegate.swift
swift
batteryChannel.setMethodCallHandler({
  [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
  // This method is invoked on the UI thread.
  guard call.method == "getBatteryLevel" else {
    result(FlutterMethodNotImplemented)
    return
  }
  self?.receiveBatteryLevel(result: result)
})

首先在 Xcode 中打开 Flutter 应用的 iOS 主机部分:

  1. 启动 Xcode。

  2. 选择菜单项 文件 > 打开...。

  3. 导航到包含 Flutter 应用的目录, 然后选择其内部的 ios 文件夹。单击 确定。

  4. 确保 Xcode 项目构建时没有错误。

  5. 在项目导航器中打开位于 Runner > Runner 下的 AppDelegate.m 文件。

创建一个 FlutterMethodChannel,并在 application didFinishLaunchingWithOptions: 方法中添加一个处理程序。 确保使用与 Flutter 客户端中使用的相同的通道名称。

AppDelegate.m
objc
#import <Flutter/Flutter.h>
#import "GeneratedPluginRegistrant.h"

@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;

  FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
                                          methodChannelWithName:@"samples.flutter.dev/battery"
                                          binaryMessenger:controller.binaryMessenger];

  [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    // 此方法在 UI 线程上调用。
    // TODO
  }];

  [GeneratedPluginRegistrant registerWithRegistry:self];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

接下来,添加使用 iOS 电池 API 检索电池电量的 iOS ObjectiveC 代码。此代码与您 在原生 iOS 应用中编写的代码完全相同。

在 AppDelegate 类中,就在 @end 之前添加以下方法:

AppDelegate.m
objc
- (int)getBatteryLevel {
  UIDevice* device = UIDevice.currentDevice;
  device.batteryMonitoringEnabled = YES;
  if (device.batteryState == UIDeviceBatteryStateUnknown) {
    return -1;
  } else {
    return (int)(device.batteryLevel * 100);
  }
}

最后,完成前面添加的 setMethodCallHandler() 方法。 您需要处理单个平台方法 getBatteryLevel(), 因此在 call 参数中对其进行测试。此平台方法的实现调用 前面步骤中编写的 iOS 代码,并使用 result 参数返回成功和错误情况的响应。如果调用了未知方法,则改为报告该方法。

AppDelegate.m
objc
__weak typeof(self) weakSelf = self;
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
  // 此方法在 UI 线程上调用。
  if ([@"getBatteryLevel" isEqualToString:call.method]) {
    int batteryLevel = [weakSelf getBatteryLevel];

    if (batteryLevel == -1) {
      result([FlutterError errorWithCode:@"UNAVAILABLE"
                                 message:@"Battery level not available."
                                 details:nil]);
    } else {
      result(@(batteryLevel));
    }
  } else {
    result(FlutterMethodNotImplemented);
  }
}];

您现在应该能够在 iOS 上运行该应用。 如果使用 iOS 模拟器, 请注意它不支持电池 API, 并且应用会显示“电池电量不可用”。

步骤 5:添加 Windows 平台特定实现

#

首先在 Visual Studio 中打开 Flutter 应用的 Windows 主机部分:

  1. 在项目目录中运行一次 flutter build windows 以生成 Visual Studio 解决方案文件。

  2. 启动 Visual Studio。

  3. 选择 打开项目或解决方案。

  4. 导航到包含 Flutter 应用的目录,然后进入 build 文件夹,然后进入 windows 文件夹,然后选择 batterylevel.sln 文件。 单击 打开。

添加平台通道方法的 C++ 实现:

  1. 在解决方案资源管理器中展开 batterylevel > 源文件。

  2. 打开 flutter_window.cpp 文件。

首先,在文件顶部添加必要的包含,就在 #include "flutter_window.h" 之后:

flutter_window.cpp
cpp
#include <flutter/event_channel.h>
#include <flutter/event_sink.h>
#include <flutter/event_stream_handler_functions.h>
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>
#include <windows.h>

#include <memory>

编辑 FlutterWindow::OnCreate 方法并创建一个与通道名称 samples.flutter.dev/battery 绑定的 flutter::MethodChannel:

flutter_window.cpp
cpp
bool FlutterWindow::OnCreate() {
  // ...
  RegisterPlugins(flutter_controller_->engine());

  flutter::MethodChannel<> channel(
      flutter_controller_->engine()->messenger(), "samples.flutter.dev/battery",
      &flutter::StandardMethodCodec::GetInstance());
  channel.SetMethodCallHandler(
      [](const flutter::MethodCall<>& call,
         std::unique_ptr<flutter::MethodResult<>> result) {
        // TODO
      });

  SetChildContent(flutter_controller_->view()->GetNativeWindow());
  return true;
}

接下来,添加使用 Windows 电池 API 检索电池电量的 C++ 代码。此代码与您 在原生 Windows 应用程序中编写的代码完全相同。

在 flutter_window.cpp 的顶部,#include 部分之后,添加以下内容作为新函数:

flutter_window.cpp
cpp
static int GetBatteryLevel() {
  SYSTEM_POWER_STATUS status;
  if (GetSystemPowerStatus(&status) == 0 || status.BatteryLifePercent == 255) {
    return -1;
  }
  return status.BatteryLifePercent;
}

最后,完成前面添加的 setMethodCallHandler() 方法。 您需要处理单个平台方法 getBatteryLevel(), 因此在 call 参数中对其进行测试。 此平台方法的实现调用 前面步骤中编写的 Windows 代码。如果调用了未知方法,则改为报告该方法。

删除以下代码:

flutter_window.cpp
cpp
  channel.SetMethodCallHandler(
      [](const flutter::MethodCall<>& call,
         std::unique_ptr<flutter::MethodResult<>> result) {
        // TODO
      });

并替换为以下内容:

flutter_window.cpp
cpp
  channel.SetMethodCallHandler(
      [](const flutter::MethodCall<>& call,
         std::unique_ptr<flutter::MethodResult<>> result) {
        if (call.method_name() == "getBatteryLevel") {
          int battery_level = GetBatteryLevel();
          if (battery_level != -1) {
            result->Success(battery_level);
          } else {
            result->Error("UNAVAILABLE", "Battery level not available.");
          }
        } else {
          result->NotImplemented();
        }
      });

您现在应该能够在 Windows 上运行该应用程序。 如果您的设备没有电池, 它会显示“电池电量不可用”。

步骤 6:添加 macOS 平台特定实现

#

首先在 Xcode 中打开 Flutter 应用的 macOS 主机部分:

  1. 启动 Xcode。

  2. 选择菜单项 文件 > 打开...。

  3. 导航到包含 Flutter 应用的目录,然后选择其内部的 macos 文件夹。单击 确定。

添加平台通道方法的 Swift 实现:

  1. 在项目导航器中展开 Runner > Runner。

  2. 在项目导航器中打开位于 Runner > Runner 下的 MainFlutterWindow.swift 文件。

首先,在文件顶部添加必要的导入,就在 import FlutterMacOS 之后:

MainFlutterWindow.swift
swift
import IOKit.ps

在 awakeFromNib 方法中创建一个与通道名称 samples.flutter.dev/battery 绑定的 FlutterMethodChannel:

MainFlutterWindow.swift
swift
  override func awakeFromNib() {
    // ...
    self.setFrame(windowFrame, display: true)
  
    let batteryChannel = FlutterMethodChannel(
      name: "samples.flutter.dev/battery",
      binaryMessenger: flutterViewController.engine.binaryMessenger)
    batteryChannel.setMethodCallHandler { (call, result) in
      // 此方法在 UI 线程上调用。
      // 处理电池消息。
    }

    RegisterGeneratedPlugins(registry: flutterViewController)

    super.awakeFromNib()
  }
}

接下来,添加使用 IOKit 电池 API 检索电池电量的 macOS Swift 代码。此代码与您 在原生 macOS 应用中编写的代码完全相同。

在 MainFlutterWindow.swift 的底部添加以下内容作为新方法:

MainFlutterWindow.swift
swift
private func getBatteryLevel() -> Int? {
  let info = IOPSCopyPowerSourcesInfo().takeRetainedValue()
  let sources: Array<CFTypeRef> = IOPSCopyPowerSourcesList(info).takeRetainedValue() as Array
  if let source = sources.first {
    let description =
      IOPSGetPowerSourceDescription(info, source).takeUnretainedValue() as! [String: AnyObject]
    if let level = description[kIOPSCurrentCapacityKey] as? Int {
      return level
    }
  }
  return nil
}

最后,完成前面添加的 setMethodCallHandler 方法。 您需要处理单个平台方法 getBatteryLevel(), 因此在 call 参数中对其进行测试。 此平台方法的实现调用 前面步骤中编写的 macOS 代码。如果调用了未知方法,则改为报告该方法。

MainFlutterWindow.swift
swift
batteryChannel.setMethodCallHandler { (call, result) in
  switch call.method {
  case "getBatteryLevel":
    guard let level = getBatteryLevel() else {
      result(
        FlutterError(
          code: "UNAVAILABLE",
          message: "Battery level not available",
          details: nil))
     return
    }
    result(level)
  default:
    result(FlutterMethodNotImplemented)
  }
}

您现在应该能够在 macOS 上运行该应用程序。 如果您的设备没有电池, 它会显示“电池电量不可用”。

步骤 7:添加 Linux 平台特定实现

#

对于此示例,您需要安装 upower 开发人员头文件。 这可能可以通过您的发行版获得,例如:

sudo apt install libupower-glib-dev

首先在您选择的编辑器中打开 Flutter 应用的 Linux 主机部分。以下说明适用于安装了“C/C++”和“CMake”扩展的 Visual Studio Code,但可以针对其他 IDE 进行调整。

  1. 启动 Visual Studio Code。

  2. 打开项目内部的 linux 目录。

  3. 在询问:是否要配置项目“linux”? 的提示中选择 是。 这将启用 C++ 自动完成。

  4. 打开 runner/my_application.cc 文件。

首先,在文件顶部添加必要的包含,就在 #include <flutter_linux/flutter_linux.h> 之后:

runner/my_application.cc
c
#include <math.h>
#include <upower.h>

向 _MyApplication 结构体添加一个 FlMethodChannel:

runnner/my_application.cc
c
struct _MyApplication {
  GtkApplication parent_instance;
  char** dart_entrypoint_arguments;
  FlMethodChannel* battery_channel;
};

确保在 my_application_dispose 中清理它:

runner/my_application.cc
c
static void my_application_dispose(GObject* object) {
  MyApplication* self = MY_APPLICATION(object);
  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
  g_clear_object(&self->battery_channel);
  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}

编辑 my_application_activate 方法并使用通道名称 samples.flutter.dev/battery 初始化 battery_channel,就在调用 fl_register_plugins 之后:

runner/my_application.cc
c
static void my_application_activate(GApplication* application) {
  // ...
  fl_register_plugins(FL_PLUGIN_REGISTRY(self->view));

  g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
  self->battery_channel = fl_method_channel_new(
      fl_engine_get_binary_messenger(fl_view_get_engine(view)),
      "samples.flutter.dev/battery", FL_METHOD_CODEC(codec));
  fl_method_channel_set_method_call_handler(
      self->battery_channel, battery_method_call_handler, self, nullptr);

  gtk_widget_grab_focus(GTK_WIDGET(self->view));
}

接下来,添加使用 Linux 电池 API 检索电池电量的 C 代码。此代码与您 在原生 Linux 应用程序中编写的代码完全相同。

在 my_application.cc 的顶部,G_DEFINE_TYPE 行之后,添加以下内容作为新函数:

runner/my_application.cc
c
static FlMethodResponse* get_battery_level() {
  // 查找第一个可用的电池并报告该电池。
  g_autoptr(UpClient) up_client = up_client_new();
  g_autoptr(GPtrArray) devices = up_client_get_devices2(up_client);
  if (devices->len == 0) {
    return FL_METHOD_RESPONSE(fl_method_error_response_new(
        "UNAVAILABLE", "Device does not have a battery.", nullptr));
  }

  UpDevice* device = UP_DEVICE(g_ptr_array_index(devices, 0));
  double percentage = 0;
  g_object_get(device, "percentage", &percentage, nullptr);

  g_autoptr(FlValue) result =
      fl_value_new_int(static_cast<int64_t>(round(percentage)));
  return FL_METHOD_RESPONSE(fl_method_success_response_new(result));
}

最后,添加前面调用 fl_method_channel_set_method_call_handler 时引用的 battery_method_call_handler 函数。 您需要处理单个平台方法 getBatteryLevel, 因此在 method_call 参数中对其进行测试。 此函数的实现调用 前面步骤中编写的 Linux 代码。如果调用了未知方法,则改为报告该方法。

在 get_battery_level 函数之后添加以下代码:

runner/my_application.cpp
cpp
static void battery_method_call_handler(FlMethodChannel* channel,
                                        FlMethodCall* method_call,
                                        gpointer user_data) {
  g_autoptr(FlMethodResponse) response = nullptr;
  if (strcmp(fl_method_call_get_name(method_call), "getBatteryLevel") == 0) {
    response = get_battery_level();
  } else {
    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
  }

  g_autoptr(GError) error = nullptr;
  if (!fl_method_call_respond(method_call, response, &error)) {
    g_warning("Failed to send response: %s", error->message);
  }
}

您现在应该能够在 Linux 上运行该应用程序。 如果您的设备没有电池, 它会显示“电池电量不可用”。

使用 Pigeon 的类型安全平台通道

#

前面的示例使用 MethodChannel 在主机和客户端之间进行通信, 它不是类型安全的。调用和接收 消息取决于主机和客户端声明 相同的参数和数据类型才能使消息正常工作。 您可以使用 Pigeon 软件包作为 MethodChannel 的替代方法 来生成以结构化、类型安全的方式发送消息的代码。

使用 Pigeon,消息协议是在 Dart 的一个子集中定义的,然后它会为 Android、iOS、macOS 或 Windows 生成消息代码。您可以在pigeon 中找到更完整的示例和更多信息

pub.dev 上的页面。

使用 Pigeon 消除了主机和客户端之间匹配 消息名称和数据类型的字符串的需要。 它支持:嵌套类、将消息分组 到 API 中、生成 异步包装器代码和双向发送消息。生成的代码可读性强, 并保证不同版本的多个客户端之间不会发生冲突。 支持的语言包括 Objective-C、Java、Kotlin、C++ 和 Swift(与 Objective-C 交互操作)。

Pigeon 示例

#

Pigeon 文件:

dart
import 'package:pigeon/pigeon.dart';

class SearchRequest {
  final String query;

  SearchRequest({required this.query});
}

class SearchReply {
  final String result;

  SearchReply({required this.result});
}

@HostApi()
abstract class Api {
  @async
  SearchReply search(SearchRequest request);
}

Dart 使用:

dart
import 'generated_pigeon.dart';

Future<void> onClick() async {
  SearchRequest request = SearchRequest(query: 'test');
  Api api = SomeApi();
  SearchReply reply = await api.search(request);
  print('reply: ${reply.result}');
}

将平台特定代码与 UI 代码分离

#

如果您希望在多个 Flutter 应用中使用平台特定代码, 您可能需要考虑 将代码分离到位于主应用程序目录之外的平台插件中。 有关详细信息,请参阅开发软件包。

将平台特定代码发布为软件包

#

要与 Flutter 生态系统中的其他开发者共享您的平台特定代码, 请参阅发布软件包。

自定义通道和编解码器

#

除了上面提到的 MethodChannel 之外, 您还可以使用更基本的 BasicMessageChannel,它支持使用自定义消息编解码器的基本 异步消息传递。您还可以使用专门的 BinaryCodec、 StringCodec 和 JSONMessageCodec 类,或创建您自己的编解码器。

您还可以查看cloud_firestore 插件中的自定义编解码器示例, 该插件能够序列化和反序列化比默认类型更多的类型。

通道和平台线程

#

当在平台端调用目标为 Flutter 的通道时, 请在平台的主线程上调用它们。 当在 Flutter 中调用目标为平台端的通道时, 请从任何作为根 Isolate 的 Isolate 或注册为根 Isolate 的后台 Isolate 调用它们。 平台端的处理程序可以在平台的主线程上执行, 或者如果使用任务队列,则可以在后台线程上执行。 您可以异步地在任何线程上调用平台端的处理程序。

从后台 Isolate 使用插件和通道

#

任何 Isolate 都可以使用插件和通道,但该 Isolate 必须是 根 Isolate(由 Flutter 创建)或注册为根 Isolate 的后台 Isolate。

以下示例演示如何注册后台 Isolate 以便 从后台 Isolate 使用插件。

dart
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';

void _isolateMain(RootIsolateToken rootIsolateToken) async {
  BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
  SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
  print(sharedPreferences.getBool('isDebug'));
}

void main() {
  RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
  Isolate.spawn(_isolateMain, rootIsolateToken);
}

在后台线程上执行通道处理程序

#

为了使通道的平台端处理程序 在后台线程上执行,您必须使用 任务队列 API。目前,此功能仅 在 iOS 和 Android 上受支持。

在 Kotlin 中:

kotlin
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
  val taskQueue =
      flutterPluginBinding.binaryMessenger.makeBackgroundTaskQueue()
  channel = MethodChannel(flutterPluginBinding.binaryMessenger,
                          "com.example.foo",
                          StandardMethodCodec.INSTANCE,
                          taskQueue)
  channel.setMethodCallHandler(this)
}

在 Java 中:

java
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
  BinaryMessenger messenger = binding.getBinaryMessenger();
  BinaryMessenger.TaskQueue taskQueue =
      messenger.makeBackgroundTaskQueue();
  channel =
      new MethodChannel(
          messenger,
          "com.example.foo",
          StandardMethodCodec.INSTANCE,
          taskQueue);
  channel.setMethodCallHandler(this);
}

在 Swift 中:

swift
public static func register(with registrar: FlutterPluginRegistrar) {
  let taskQueue = registrar.messenger.makeBackgroundTaskQueue()
  let channel = FlutterMethodChannel(name: "com.example.foo",
                                     binaryMessenger: registrar.messenger(),
                                     codec: FlutterStandardMethodCodec.sharedInstance,
                                     taskQueue: taskQueue)
  let instance = MyPlugin()
  registrar.addMethodCallDelegate(instance, channel: channel)
}

在 Objective-C 中:

objc
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  NSObject<FlutterTaskQueue>* taskQueue =
      [[registrar messenger] makeBackgroundTaskQueue];
  FlutterMethodChannel* channel =
      [FlutterMethodChannel methodChannelWithName:@"com.example.foo"
                                  binaryMessenger:[registrar messenger]
                                            codec:[FlutterStandardMethodCodec sharedInstance]
                                        taskQueue:taskQueue];
  MyPlugin* instance = [[MyPlugin alloc] init];
  [registrar addMethodCallDelegate:instance channel:channel];
}

在 Android 中跳转到 UI 线程

#

为了符合通道的 UI 线程要求, 您可能需要从后台线程 跳转到 Android 的 UI 线程来执行通道方法。 在 Android 中,您可以通过将 Runnable post() 到 Android 的 UI 线程 Looper 来实现此目的, 这会导致 Runnable 在下一次机会时在 主线程上执行。

在 Kotlin 中:

kotlin
Handler(Looper.getMainLooper()).post {
  // 在此处调用所需的通道消息。
}

在 Java 中:

java
new Handler(Looper.getMainLooper()).post(new Runnable() {
  @Override
  public void run() {
    // 在此处调用所需的通道消息。
  }
});

在 iOS 中跳转到主线程

#

为了符合通道的主线程要求, 您可能需要从后台线程跳转到 iOS 的主线程来执行通道方法。 您可以在 iOS 中通过在主调度队列 上执行 块 来实现此目的:

在 Objective-C 中:

objc
dispatch_async(dispatch_get_main_queue(), ^{
  // 在此处调用所需的通道消息。
});

在 Swift 中:

swift
DispatchQueue.main.async {
  // 在此处调用所需的通道消息。
}