跳到主內容

編寫自定義平臺特定程式碼

瞭解如何在您的應用程式中編寫自定義平臺特定程式碼。

本指南介紹瞭如何在 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
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;

然後將以下內容新增為活動類中的新方法,位於 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. 選擇選單項 File > Open...

  3. 導航到包含 Flutter 應用程式的目錄,然後選擇其中的 ios 資料夾。單擊 OK

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

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

  2. 在專案導航器中,位於 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. 選擇選單項 File > Open...

  3. 導航到包含 Flutter 應用程式的目錄,然後選擇其中的 ios 資料夾。單擊 OK

  4. 確保 Xcode 專案可以成功構建,沒有錯誤。

  5. 開啟檔案 AppDelegate.m,該檔案位於專案導航器中的 Runner > Runner 下。

建立一個 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" 之後

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

#include <memory>

編輯 FlutterWindow::OnCreate 方法,並建立一個與通道名稱 samples.flutter.dev/battery 繫結的 flutter::MethodChannel

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

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

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

接下來,新增使用 Windows 電池 API 獲取電池電量的 C++ 程式碼。這段程式碼與你在原生 Windows 應用程式中編寫的程式碼完全相同。

將以下程式碼作為新函式新增到 flutter_window.cpp 的頂部,緊接 #include 部分之後

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

最後,完成之前新增的 setMethodCallHandler() 方法。你需要處理一個平臺方法,getBatteryLevel(),因此在 call 引數中進行測試。該平臺方法的實現呼叫之前步驟中編寫的 Windows 程式碼。如果呼叫了未知方法,則報告該方法。

刪除以下程式碼

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

並替換為以下程式碼

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

現在你應該能夠在 Windows 上執行該應用程式。如果你的裝置沒有電池,它將顯示“電池電量不可用”。

步驟 6:新增 macOS 平臺特定實現

#

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

  1. 啟動 Xcode。

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

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

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

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

  2. 開啟檔案 MainFlutterWindow.swift,該檔案位於專案導航器中的 Runner > Runner 下。

首先,在檔案的頂部新增必要的匯入項,緊接 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 上執行該應用程式。如果你的裝置沒有電池,它將顯示“電池電量不可用”。

步驟 7:新增 Linux 平臺特定實現

#

對於此示例,你需要安裝 upower 開發標頭檔案。這可能可以從你的發行版獲得,例如使用

sudo apt install libupower-glib-dev

首先,在您選擇的編輯器中開啟 Flutter 應用的 Linux 主體部分。以下說明適用於安裝了“C/C++”和“CMake”擴充套件的 Visual Studio Code,但可以針對其他 IDE 進行調整。

  1. 啟動 Visual Studio Code。

  2. 開啟專案內部的 linux 目錄。

  3. 在提示中選擇 是否要配置專案“linux”?。這將啟用 C++ 自動完成功能。

  4. 開啟檔案 runner/my_application.cc

首先,在檔案的頂部新增必要的包含項,緊接 #include <flutter_linux/flutter_linux.h> 之後

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

FlMethodChannel 新增到 _MyApplication 結構體

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 上執行該應用程式。如果你的裝置沒有電池,它將顯示“電池電量不可用”。

使用 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 的平臺側通道時,在平臺的 main 執行緒上呼叫它們。在 Flutter 中呼叫目的地為平臺側的通道時,要麼從是根 Isolate 的任何 Isolate 呼叫它們,或者從註冊為後臺 IsolateIsolate 呼叫它們。平臺側的處理程式可以在平臺的 main 執行緒上執行,或者如果使用任務佇列,可以在後臺執行緒上執行。你可以非同步地在任何執行緒上呼叫平臺側的處理程式。

從後臺隔離區使用外掛和通道

#

外掛和通道可以由任何 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 來實現此目的,這將在下一次機會在 main 執行緒上執行該 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)

#

為了符合通道的 main 執行緒要求,你可能需要從後臺執行緒跳轉到 iOS 的 main 執行緒來執行通道方法。你可以在 iOS 中透過在 main block 上執行 dispatch queue 來實現此目的

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 for Platforms:這些特定於平臺的 API 包含特定於平臺的通道 API。

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

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

  • BinaryCodec:一種訊息編碼器,它在 Flutter 應用程式的 Dart 端和本機平臺端之間傳遞原始二進位制資料。它不執行任何更高層次的資料結構編碼或解碼。

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

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

  • FirestoreMessageCodec:一種訊息編碼器,用於處理在 Flutter 應用程式和本機 Firebase Firestore SDK(在 Android 和 iOS 上)之間透過平臺通道傳送的訊息的交換。

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

#

如果您預計在多個 Flutter 應用程式中使用您的特定於平臺的程式碼,您可能需要將程式碼分離到位於主應用程式外部目錄中的平臺外掛中。有關詳細資訊,請參閱 開發包

將平臺特定程式碼釋出為包

#

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