跳到主內容

編寫自定義平臺相關程式碼

學習如何在應用中編寫自定義平臺相關程式碼。

本指南介紹瞭如何在 Flutter 中使用自定義平臺相關程式碼。

概述

#

你可以在 Flutter 應用中使用平臺相關程式碼。幾種常見的方法包括:

Flutter 支援以下平臺及對應的平臺相關語言:

  • Android:Kotlin, Java
  • iOS:Swift, Objective-C
  • Windows:C++
  • macOS:Objective-C
  • Linux:C

平臺通道架構概述

#

如下圖所示,訊息透過平臺通道在客戶端(UI)和宿主(平臺)之間傳遞:

Platform channels architecture

在上圖中,訊息和響應透過通道非同步傳遞,以確保使用者介面保持響應。在客戶端,用於 Flutter 的 MethodChannel 支援傳送對應於方法呼叫的訊息。在平臺側,用於 Android 的 MethodChannel用於 iOS 的 FlutterMethodChannel 支援接收方法呼叫並返回結果。這些類使你能夠以極少的樣板程式碼開發平臺外掛。

資料型別支援

#

標準的平臺通道 API 和 Pigeon 軟體包使用一種名為 StandardMessageCodec 的標準訊息編解碼器,它支援對類似於 JSON 的簡單值進行高效的二進位制序列化,例如布林值、數字、字串、位元組緩衝區、列表和對映。當你傳送和接收這些值時,訊息的序列化和反序列化會自動發生。

下表顯示了 Dart 值在平臺側的對應關係(反之亦然):

DartKotlin
nullnull
boolBoolean
int (<=32 位)Int
int (>32 位)Long
doubleDouble
StringString
Uint8ListByteArray
Int32ListIntArray
Int64ListLongArray
Float32ListFloatArray
Float64ListDoubleArray
ListList
MapHashMap
DartJava
nullnull
booljava.lang.Boolean
int (<=32 位)java.lang.Integer
int (>32 位)java.lang.Long
doublejava.lang.Double
Stringjava.lang.String
Uint8Listbyte[]
Int32Listint[]
Int64Listlong[]
Float32Listfloat[]
Float64Listdouble[]
Listjava.util.ArrayList
Mapjava.util.HashMap
DartSwift
nullnil (巢狀時為 NSNull)
boolNSNumber(value: Bool)
int (<=32 位)NSNumber(value: Int32)
int (>32 位)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)
boolNSNumber numberWithBool
int (<=32 位)NSNumber numberWithInt
int (>32 位)NSNumber numberWithLong
doubleNSNumber numberWithDouble
StringNSString
Uint8ListFlutterStandardTypedData typedDataWithBytes
Int32ListFlutterStandardTypedData typedDataWithInt32
Int64ListFlutterStandardTypedData typedDataWithInt64
Float32List FlutterStandardTypedData typedDataWithFloat32
Float64List FlutterStandardTypedData typedDataWithFloat64
ListNSArray
MapNSDictionary
DartC++
nullEncodableValue()
boolEncodableValue(bool)
int (<=32 位)EncodableValue(int32_t)
int (>32 位)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>)
Map EncodableValue(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');
  // Get battery level.

接下來,在方法通道上呼叫一個方法,使用 String 識別符號 getBatteryLevel 指定要呼叫的具體方法。呼叫可能會失敗(例如,如果平臺不支援該平臺 API,如在模擬器中執行時),因此請將 invokeMethod 呼叫包裝在 try-catch 語句中。

使用返回的結果在 setState 內部更新 _batteryLevel 以更新使用者介面狀態。

dart
// 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 方法,使其包含一個簡單的使用者介面,顯示電池電量字串,並提供一個重新整理該值的按鈕。

dart
@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. 選擇選單項 File > Open...

  3. 導航至存放 Flutter 應用的目錄,選擇其中的 android 資料夾。點選 OK

  4. 在專案檢視的 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 ->
      // This method is invoked on the main thread.
      // TODO
    }
  }
}

新增 Android Kotlin 程式碼,使用 Android 電池 API 獲取電池電量。這段程式碼與你在原生 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 {
      // 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. 選擇選單項 File > Open...

  3. 導航至存放 Flutter 應用的目錄,選擇其中的 android 資料夾。點選 OK

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

新增 Android Java 程式碼,使用 Android 電池 API 獲取電池電量。這段程式碼與你在原生 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) -> {
            // 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 模擬器,請在工具欄 ... 按鈕中開啟的“擴充套件控制元件 (Extended Controls)”面板中設定電池電量。

第 4 步:新增 iOS 平臺相關實現

#

首先在 Xcode 中開啟 Flutter 應用的 iOS 宿主部分

  1. 啟動 Xcode。

  2. 選擇選單項 File > Open...

  3. 導航至存放 Flutter 應用的目錄,選擇其中的 ios 資料夾。點選 OK

在預設使用 Objective-C 的標準模板設定中新增對 Swift 的支援。

  1. 在專案導航器中展開 Runner > Runner

  2. 在專案導航器的 Runner > Runner 下開啟 AppDelegate.swift 檔案。

重寫 application:didFinishLaunchingWithOptions: 函式,並建立一個繫結到通道名稱 samples.flutter.dev/batteryFlutterMethodChannel

AppDelegate.swift
swift
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)

    let batteryChannel = FlutterMethodChannel(
      name: "samples.flutter.dev/battery",
      binaryMessenger: engineBridge.applicationRegistrar.messenger()
    )

    batteryChannel.setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
      // This method is invoked on the UI thread.
      // Handle battery messages.
    })
  }
}

接下來,新增使用 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. 選擇選單項 File > Open...

  3. 導航至存放 Flutter 應用的目錄,選擇其中的 ios 資料夾。點選 OK

  4. 確保 Xcode 專案構建無誤。

  5. 在專案導航器的 Runner > Runner 下開啟 AppDelegate.m 檔案。

application:didFinishLaunchingWithOptions: 方法內建立 FlutterMethodChannel 並新增處理器。確保使用與 Flutter 客戶端側相同的通道名稱。

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

@implementation AppDelegate

- (void)didInitializeImplicitFlutterEngine:(NSObject<FlutterImplicitEngineBridge>*)engineBridge {
  [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry];

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

  [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    // This method is invoked on the UI thread.
    // TODO
  }];
}
@end

接下來,新增使用 iOS 電池 API 獲取電池電量的 iOS Objective-C 程式碼。這段程式碼與你在原生 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) {
  // 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,應用會顯示“Battery level not available”。

第 5 步:新增 Windows 平臺相關實現

#

首先在 Visual Studio 中開啟 Flutter 應用的 Windows 宿主部分

  1. 在專案目錄中執行 flutter build windows 一次,以生成 Visual Studio 解決方案檔案。

  2. 啟動 Visual Studio。

  3. 選擇 Open a project or solution

  4. 導航至存放 Flutter 應用的目錄,進入 build 資料夾,再進入 windows 資料夾,選中 batterylevel.sln 檔案。點選 Open

新增平臺通道方法的 C++ 實現

  1. 在解決方案資源管理器中展開 batterylevel > Source Files

  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 上執行該應用了。如果你的裝置沒有電池,它會顯示“Battery level not available”。

第 6 步:新增 macOS 平臺相關實現

#

首先在 Xcode 中開啟 Flutter 應用的 macOS 宿主部分

  1. 啟動 Xcode。

  2. 選擇選單項 File > Open...

  3. 導航至存放 Flutter 應用的目錄,選擇其中的 macos 資料夾。點選 OK

新增平臺通道方法的 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
      // 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
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 上執行該應用了。如果你的裝置沒有電池,它會顯示“Battery level not available”。

第 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. 在提示 Would you like to configure project "linux"? 時選擇 Yes。這將啟用 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 方法,並在 fl_register_plugins 呼叫之後初始化使用通道名稱 samples.flutter.dev/batterybattery_channel

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.ccG_DEFINE_TYPE 行之後新增以下函式。

runner/my_application.cc
c
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 = 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 上執行該應用了。如果你的裝置沒有電池,它會顯示“Battery level not available”。

使用 Pigeon 軟體包呼叫平臺相關程式碼

#

你可以使用 Pigeon 軟體包作為 Flutter 平臺通道 API 的替代方案,以生成能夠以結構化、型別安全的方式傳送訊息的程式碼。Pigeon 的工作流程如下:

  • Flutter 應用透過平臺通道向其宿主(即應用的非 Dart 部分)傳送結構化的、型別安全的訊息。

  • 宿主監聽該平臺通道並接收訊息。然後,它使用原生程式語言呼叫任意數量的平臺相關 API,並將響應傳送回客戶端(即應用的 Flutter 部分)。

使用此軟體包消除了在宿主和客戶端之間匹配訊息名稱和資料型別字串的需要。它支援巢狀類、將訊息分組到 API 中、生成非同步包裝程式碼以及在兩個方向上傳送訊息。生成的程式碼易於閱讀,並能保證不同版本的多個客戶端之間不會產生衝突。

使用 Pigeon 時,訊息傳遞協議定義在 Dart 的一個子集中,然後為 Android、iOS、macOS 或 Windows 生成訊息傳遞程式碼。例如:

pigeon_source.dart
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);
}
use_pigeon.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}');
}

你可以在 pub.dev 上的 pigeon 頁面找到完整的示例和更多資訊。

通道與平臺執行緒

#

當在平臺側呼叫指向 Flutter 的通道時,請在平臺的**主執行緒**上進行呼叫。當在 Flutter 中呼叫指向平臺側的通道時,你可以從作為根 Isolate 的任意 Isolate 或已註冊為後臺 IsolateIsolate 中呼叫。平臺側的處理器可以在平臺的主執行緒執行,或者如果使用任務佇列(Task Queue),也可以在後臺執行緒執行。你可以非同步並在任何執行緒上呼叫平臺側的處理器。

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

在後臺執行緒執行通道處理器(Android)

#

為了讓通道的平臺側處理器在 Android 應用的後臺執行緒上執行,你必須使用任務佇列(Task Queue)API。

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

在後臺執行緒執行通道處理器(iOS)

#

為了讓通道的平臺側處理器在 iOS 應用的後臺執行緒上執行,你必須使用任務佇列(Task Queue)API。

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

跳轉至 UI 執行緒(Android)

#

為了符合通道對 UI 執行緒的要求,你可能需要從後臺執行緒跳轉到 Android 的 UI 執行緒來執行通道方法。在 Android 中,你可以透過向 Android UI 執行緒的 Looper 傳送一個 Runnable (post()) 來實現,這會使 Runnable 在下次機會時在主執行緒上執行。

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

跳轉至主執行緒(iOS)

#

為了符合通道對主執行緒的要求,你可能需要從後臺執行緒跳轉到 iOS 的主執行緒來執行通道方法。在 iOS 中,你可以透過在主 排程佇列 (dispatch queue) 上執行一個 程式碼塊 (block) 來實現。

objc
dispatch_async(dispatch_get_main_queue(), ^{
  // Call the desired channel message here.
});
swift
DispatchQueue.main.async {
  // Call the desired channel message here.
}

補充說明

#

常用通道與編解碼器

#

以下是一些常用的平臺通道 API 列表,你可以使用它們來編寫平臺相關程式碼:

你可以建立自己的編解碼器或使用現有的編解碼器。以下是一些可以與平臺相關程式碼一起使用的現有編解碼器列表:

  • StandardMessageCodec:一種常用的訊息編解碼器,它將廣泛的資料型別編碼和解碼為一種平臺無關的二進位制格式,以便在平臺通道上傳輸。當你傳送和接收值時,值的序列化和反序列化會自動發生。有關支援的資料型別列表,請參閱 平臺通道資料型別支援

  • BinaryCodec:一種在 Flutter 應用的 Dart 側與原生平臺側之間傳遞原始二進位制資料的訊息編解碼器。它不對資料結構執行任何更高級別的編碼或解碼。

  • StringCodec:一種使用 UTF-8 編碼對字串進行編碼和解碼的訊息編解碼器。

  • JSONMessageCodec:一種使用 UTF-8 編碼對 JSON 格式資料進行編碼和解碼的訊息編解碼器。

  • FirestoreMessageCodec:一種處理 Flutter 應用與原生 Firebase Firestore SDK(在 Android 和 iOS 上)之間透過平臺通道傳送的訊息交換的訊息編解碼器。

將平臺相關程式碼與 UI 程式碼分離

#

如果你期望在多個 Flutter 應用中使用平臺相關程式碼,可以考慮將程式碼分離到一個位於主應用目錄之外的平臺外掛中。詳情請參閱 開發軟體包

以軟體包形式釋出平臺相關程式碼

#

要與 Flutter 生態系統中的其他開發者共享你的平臺相關程式碼,請參閱 釋出軟體包