本指南介紹如何將自定義的特定平臺程式碼與 Flutter 結合使用。

概述

#

您可以在 Flutter 應用中使用特定於平臺的程式碼。以下是一些常見的方法:

Flutter 支援以下平臺和特定平臺語言:

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

平臺通道的架構概述

#

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

Platform channels architecture

在上圖中,訊息和響應是非同步透過通道傳遞的,以確保使用者介面保持響應。在客戶端,MethodChannel for Flutter 允許傳送對應於方法呼叫的訊息。在平臺端,MethodChannel for AndroidFlutterMethodChannel for iOS 允許接收方法呼叫併發送結果。這些類使您能夠開發具有極少樣板程式碼的平臺外掛。

資料型別支援

#

標準的平臺通道 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
Float32ListFlutterStandardTypedData typedDataWithFloat32
Float64ListFlutterStandardTypedData 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>)
MapEncodableValue(std::map<encodablevalue, encodablevalue="">)

</encodablevalue,></int64_t></int32_t></uint8_t>

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.

接下來,在方法通道上呼叫方法,使用字串識別符號 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. 選擇選單項 檔案 > 開啟...

  3. 導航到包含您的 Flutter 應用的目錄,然後選擇其中的 android 資料夾。點選 OK

  4. 開啟位於 Project 檢視的 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 電池 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 {
      // 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 資料夾。點選 OK

  4. 開啟位於 Project 檢視的 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 電池 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) -> {
            // 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 資料夾。點選 OK

為標準模板設定新增 Swift 支援,該模板使用 Objective-C。

  1. 在 Project 導航器中展開 **Runner > Runner**。

  2. 開啟位於 Project 導航器中 **Runner > Runner** 下的 AppDelegate.swift 檔案。

重寫 application:didFinishLaunchingWithOptions: 函式,並建立一個與通道名稱 samples.flutter.dev/battery 關聯的 FlutterMethodChannel

AppDelegate.swift
swift
@main
@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
      // 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
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 資料夾。點選 OK

  4. 確保 Xcode 專案可以成功構建。

  5. 開啟位於 Project 導航器中 **Runner > Runner** 下的 AppDelegate.m 檔案。

application didFinishLaunchingWithOptions: 方法中建立一個 FlutterMethodChannel 並新增一個處理程式。確保使用與 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) {
    // 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
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. 選擇 開啟專案或解決方案

  4. 導航到包含您的 Flutter 應用的目錄,然後進入 build 資料夾,再進入 windows 資料夾,然後選擇 batterylevel.sln 檔案。點選 Open

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

  1. 在 Solution Explorer 中展開 **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. 選擇選單項 檔案 > 開啟...

  3. 導航到包含您的 Flutter 應用的目錄,然後選擇其中的 macos 資料夾。點選 OK

新增平臺通道方法的 Swift 實現

  1. 在 Project 導航器中展開 **Runner > Runner**。

  2. 開啟位於 Project 導航器中 **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. 在詢問“您想配置專案 '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/battery 初始化 battery_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.cc 檔案頂部,緊跟在 G_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(由 Flutter 建立的那個)或已註冊為根 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);
}

在後臺執行緒上執行通道處理程式 (Android)

#

為了使通道的平臺端處理程式在 Android 應用的後臺執行緒上執行,您必須使用任務佇列 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 應用的後臺執行緒上執行,您必須使用任務佇列 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 中,您可以透過將 Runnable post() 到 Android 的 UI 執行緒 Looper 來實現這一點,這會導致 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 中,您可以透過在主 排程佇列上執行一個來實現這一點。

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

補充

#

常用通道和編解碼器

#

以下是一些您可用於編寫特定平臺程式碼的常用平臺通道 API 列表:

  • MethodChannel for Flutter:一個命名通道,您可以使用它透過非同步方法呼叫與平臺外掛進行通訊。預設情況下,此通道使用 StandardMessageCodec 編解碼器。此通道不是型別安全的,這意味著呼叫和接收訊息取決於宿主和客戶端宣告相同的引數和資料型別,以便訊息能夠正常工作。

  • BasicMessageChannel for Flutter:一個支援基本非同步訊息傳遞的命名通道,使用支援的訊息編解碼器。非型別安全。

  • 平臺 Engine Embedder APIs:這些特定於平臺的 API 包含特定於平臺的通道 API。

您可以建立自己的編解碼器或使用現有的編解碼器。以下是您可以在特定平臺程式碼中使用的一些現有編解碼器列表:

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

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

  • StringCodec:一個訊息編解碼器,它使用 UTF-8 編碼來編碼和解碼字串。

  • JSONMessageCodec:一個訊息編解碼器,它使用 UTF-8 編碼來編碼和解碼 JSON 格式的資料。

  • FirestoreMessageCodec:一個訊息編解碼器,它處理您的 Flutter 應用和原生 Firebase Firestore SDK(在 Android 和 iOS 上)之間在平臺通道上傳輸的訊息交換。

將特定平臺程式碼與 UI 程式碼分離

#

如果您希望在多個 Flutter 應用中使用特定於平臺的程式碼,可以考慮將其分離到一個位於主應用程式外部目錄中的平臺外掛中。有關詳細資訊,請參閱 開發外掛

將特定平臺程式碼釋出為外掛

#

要與其他 Flutter 生態系統中的開發者共享您的特定平臺程式碼,請參閱 釋出外掛