跳至主要內容

撰寫自訂平台特定程式碼

本指南說明如何撰寫自訂平台特定程式碼。某些平台特定功能可透過現有套件取得;請參閱使用套件

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 位元)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>)
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. 選取選單項目檔案 > 開啟...

  3. 導覽至您的 Flutter 應用程式所在的目錄,並選取其中的 android 資料夾。點擊確定

  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 電池 API 來擷取電池電量的 Android Kotlin 程式碼。這段程式碼與您在原生 Android 應用程式中撰寫的程式碼完全相同。

首先,在檔案頂端新增所需的 import

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 資料夾。點擊確定

  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 電池 API 來擷取電池電量的 Android Java 程式碼。這段程式碼與您在原生 Android 應用程式中撰寫的程式碼完全相同。

首先,在檔案頂端新增所需的 import

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 資料夾。點擊確定

在使用 Objective-C 的標準範本設定中新增 Swift 支援

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

  2. 開啟位於專案導覽器中 Runner > Runner 下方的 AppDelegate.swift 檔案。

覆寫 application:didFinishLaunchingWithOptions: 函式,並建立繫結至通道名稱 samples.flutter.dev/batteryFlutterMethodChannel

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
      // 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 資料夾。點擊確定

  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) {
    // 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,且應用程式會顯示「電池電量無法使用」。

步驟 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" 之後,新增必要的 include

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/batteryflutter::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 之後,新增必要的 import

MainFlutterWindow.swift
swift
import IOKit.ps

awakeFromNib 方法中,建立繫結至通道名稱 samples.flutter.dev/batteryFlutterMethodChannel

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 上執行應用程式。如果您的裝置沒有電池,則會顯示「電池電量無法使用」。

步驟 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> 之後,新增必要的 include

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

FlMethodChannel 新增至 _MyApplication 結構

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

請務必在 my_application_dispose 中清除它

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

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 行之後,新增以下程式碼作為新的函式

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 函式之後新增以下程式碼

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 產生訊息傳輸程式碼。您可以在 pub.dev 上的 pigeon 頁面上找到更完整的範例和更多資訊。

使用 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,它支援使用自訂訊息編碼器的基本非同步訊息傳遞。您也可以使用特定的 BinaryCodecStringCodecJSONMessageCodec 類別,或者建立您自己的編碼器。

您也可以查看 cloud_firestore 外掛程式中自訂編碼器的範例,它能夠序列化和反序列化比預設類型更多的類型。

通道和平台執行緒

#

當在平台端調用目標為 Flutter 的通道時,請在平台的主執行緒上調用它們。當在 Flutter 中調用目標為平台端的通道時,可以從任何根 Isolate(root Isolate)中調用,或者從已註冊為背景 IsolateIsolate 中調用。平台端的處理程式可以在平台的主執行緒上執行,如果使用任務佇列(Task Queue),則也可以在背景執行緒上執行。您可以非同步地在任何執行緒上調用平台端的處理程式。

從背景隔離區使用外掛程式和通道

#

外掛程式和通道可以被任何 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 {
  // Call the desired channel message here.
}

在 Java 中

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

在 iOS 中跳轉至主執行緒

#

為了符合通道的主執行緒要求,您可能需要從背景執行緒跳轉到 iOS 的主執行緒來執行通道方法。您可以在 iOS 中透過在主 dispatch queue 上執行一個 區塊(block) 來完成此操作。

在 Objective-C 中

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

在 Swift 中

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