编写自定义特定于平台的代码

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

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 值在平台端接收的方式,反之亦然

Dart Java
null null
bool java.lang.Boolean
int java.lang.Integer
int,如果 32 位不够 java.lang.Long
double java.lang.Double
String java.lang.String
Uint8List byte[]
Int32List int[]
Int64List long[]
Float32List float[]
Float64List double[]
List java.util.ArrayList
Map java.util.HashMap
Dart Kotlin
null null
bool Boolean
int Int
int,如果 32 位不够 Long
double Double
String String
Uint8List ByteArray
Int32List IntArray
Int64List LongArray
Float32List FloatArray
Float64List DoubleArray
List List
Map HashMap
Dart Objective-C
null nil(嵌套时为 NSNull)
bool NSNumber numberWithBool
int NSNumber numberWithInt
int,如果 32 位不够 NSNumber numberWithLong
double NSNumber numberWithDouble
String NSString
Uint8List FlutterStandardTypedData typedDataWithBytes
Int32List FlutterStandardTypedData typedDataWithInt32
Int64List FlutterStandardTypedData typedDataWithInt64
Float32List FlutterStandardTypedData typedDataWithFloat32
Float64List FlutterStandardTypedData typedDataWithFloat64
List NSArray
Map NSDictionary
Dart Swift
null nil
bool NSNumber(value: Bool)
int NSNumber(value: Int32)
int,如果 32 位不够 NSNumber(value: Int)
double NSNumber(value: Double)
String String
Uint8List FlutterStandardTypedData(bytes: Data)
Int32List FlutterStandardTypedData(int32: Data)
Int64List FlutterStandardTypedData(int64: Data)
Float32List FlutterStandardTypedData(float32: Data)
Float64List FlutterStandardTypedData(float64: Data)
List Array
Map Dictionary
Dart C++
null EncodableValue()
bool EncodableValue(bool)
int EncodableValue(int32_t)
int,如果 32 位不够 EncodableValue(int64_t)
double EncodableValue(double)
String EncodableValue(std::string)
Uint8List EncodableValue(std::vector)
Int32List EncodableValue(std::vector)
Int64List EncodableValue(std::vector)
Float32List EncodableValue(std::vector)
Float64List EncodableValue(std::vector)
List EncodableValue(std::vector)
Map EncodableValue(std::map<EncodableValue, EncodableValue>)
Dart C (GObject)
null FlValue()
bool FlValue(bool)
int FlValue(int64_t)
double FlValue(double)
String FlValue(gchar*)
Uint8List FlValue(uint8_t*)
Int32List FlValue(int32_t*)
Int64List FlValue(int64_t*)
Float32List FlValue(float*)
Float64List FlValue(double*)
List FlValue(FlValue)
Map FlValue(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

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('samples.flutter.dev/battery');
  // Get battery level.

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

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

// Get battery level.
String _batteryLevel = 'Unknown battery level.';

Future<void> _getBatteryLevel() async {
  String batteryLevel;
  try {
    final result = await platform.invokeMethod<int>('getBatteryLevel');
    batteryLevel = 'Battery level at $result % .';
  } on PlatformException catch (e) {
    batteryLevel = "Failed to get battery level: '${e.message}'.";
  }

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

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

@override
Widget build(BuildContext context) {
  return Material(
    child: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ElevatedButton(
            onPressed: _getBatteryLevel,
            child: const Text('Get Battery Level'),
          ),
          Text(_batteryLevel),
        ],
      ),
    ),
  );
}

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

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

  1. 启动 Android Studio

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

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

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

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

MainActivity.kt
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 ->
      // This method is invoked on the main thread.
      // TODO
    }
  }
}

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

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

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

并替换为以下内容

MainActivity.kt
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
  // This method is invoked on the main thread.
  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
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) -> {
            // This method is invoked on the main thread.
            // TODO
          }
        );
  }
}

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

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

MainActivity.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;

然后在活动类中将以下内容作为新方法添加到 configureFlutterEngine() 方法下方

MainActivity.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
(call, result) -> {
  // This method is invoked on the main thread.
  // TODO
}

并替换为以下内容

MainActivity.java
(call, result) -> {
  // This method is invoked on the main thread.
  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/batteryFlutterMethodChannel

AppDelegate.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({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // This method is invoked on the UI thread.
      // Handle battery messages.
    })

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

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

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

AppDelegate.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
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
#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) {
    // This method is invoked on the UI thread.
    // TODO
  }];

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

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

AppDelegate 类中添加以下方法,紧靠在 @end 之前

AppDelegate.m
- (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
__weak typeof(self) weakSelf = self;
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
  // This method is invoked on the UI thread.
  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
#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 方法并创建一个 flutter::MethodChannel,将其绑定到通道名称 samples.flutter.dev/battery

flutter_window.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
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
channel.SetMethodCallHandler(
    [](const flutter::MethodCall<>& call,
       std::unique_ptr<flutter::MethodResult<>> result) {
      // TODO
    });

并替换为以下内容

flutter_window.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
import IOKit.ps

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

MainFlutterWindow.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
      // This method is invoked on the UI thread.
      // Handle battery messages.
    }

    RegisterGeneratedPlugins(registry: flutterViewController)

    super.awakeFromNib()
  }
}

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

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

MainFlutterWindow.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
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. 打开文件my_application.cc

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

my_application.cc
#include <math.h>
#include <upower.h>

_MyApplication结构添加FlMethodChannel

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

确保在my_application_dispose中清理它

my_application.cc
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方法,并在调用fl_register_plugins之后,使用通道名称samples.flutter.dev/battery初始化battery_channel

my_application.cc
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行之后

my_application.cc
static FlMethodResponse* get_battery_level() {
  // Find the first available battery and report that.
  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 = (UpDevice*)(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函数之后添加以下代码

flutter_window.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 生成消息传递代码。你可以在 pub.dev 上的 pigeon 页面上找到更完整的示例和更多信息。

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

Pigeon 示例

Pigeon 文件

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 用法

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,它使用自定义消息编解码器支持基本的异步消息传递。你还可以使用专门的 BinaryCodecStringCodecJSONMessageCodec 类,或创建自己的编解码器。

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

通道和平台线程

当在平台端调用针对 Flutter 的通道时,请在平台的主线程上调用它们。当在 Flutter 中调用针对平台端的通道时,可以从任何 Isolate 调用它们,该 Isolate 是根 Isolate已注册为后台 Isolate。平台端的处理程序可以在平台的主线程上执行,也可以在使用任务队列时在后台线程上执行。你可以异步地在任何线程上调用平台端的处理程序。

从后台隔离区使用插件和通道

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

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

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 上受支持。

在 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);
}

在 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)
}

在 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 中

+ (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 在下次有机会时在主线程上执行。

在 Java 中

new Handler(Looper.getMainLooper()).post(new Runnable() {
  @Override
  public void run() {
    // Call the desired channel message here.
  }
});

在 Kotlin 中

Handler(Looper.getMainLooper()).post {
  // Call the desired channel message here.
}

在 iOS 中跳转到主线程

为了遵守频道的 UI 线程要求,您可能需要从后台线程跳转到 iOS 的主线程来执行频道方法。您可以在 iOS 中通过在主 dispatch 队列 上执行 来实现此目的

在 Objective-C 中

dispatch_async(dispatch_get_main_queue(), ^{
  // Call the desired channel message here.
});

在 Swift 中

DispatchQueue.main.async {
  // Call the desired channel message here.
}