撰寫雙端平台程式碼(外掛編寫實現)

本指南介紹瞭如何編寫自訂的平台相關程式碼,某些平台相關功能可透過已有的軟體包獲得,具體細節可檢視: 在 Flutter 裡使用 Packages

Flutter 建構了一個靈活的系統,你可以呼叫各種相關平台的 API,具體如下所示:

  • Android 中的 Java 或 Kotlin API

  • iOS 中的 Objective-C 或 Swift API

  • Windows 作業系統中的 C++ API

  • macOS 系統中的 Objective-C

  • Linux 作業系統中的 C

Flutter 使用了靈活系統,無論是在 Android 上的 Kotlin 還是 Java,亦或是 iOS 上的 Swift 或 Objective-C,它都允許你呼叫平台特定 API。

Flutter 內建的平台特定 API 支援不依賴於任何產生程式碼,而是靈活的依賴於傳遞訊息格式。或者,你也可以使用 Pigeon 這個 package,透過產生程式碼來 傳送結構化型別安全訊息

  • 應用中的 Flutter 部分透過平台通道向其宿主 (非 Dart 部分) 傳送訊息。

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

架構概述:平台通道

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

Platform channels architecture

訊息和響應以非同步的形式進行傳遞,以確保使用者介面能夠保持響應。

客戶端做方法呼叫的時候 MethodChannel 會負責響應,從平台一側來講,Android 系統上使用 MethodChannelAndroid、 iOS 系統使用 MethodChanneliOS 來接收和返回來自 MethodChannel 的方法呼叫。在開發平台外掛的時候,可以減少樣板程式碼。

平台通道資料型別及編解碼器

標準平台通道使用標準訊息編解碼器,它支援簡單的類似 JSON 值的高效二進位制序列化,例如布林值、數字、字串、位元組緩衝區及這些型別的列表和對映(詳情請參閱 StandardMessageCodec)。當你傳送和接收值時,它會自動對這些值進行序列化和反序列化。

下表展示瞭如何在平臺端接收 Dart 值,反之亦然:

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

範例: 透過平台通道呼叫平台的 iOS、Android 和 Windows 程式碼

以下程式碼示範瞭如何呼叫平台相關 API 來檢索並顯示當前的電池電量。它透過平台訊息 getBatteryLevel() 來呼叫 Android 的 BatteryManager API、 iOS 的 device.batteryLevel API、以及 indows 上的 GetSystemPowerStatus

該範例在主應用程式中新增平台相關程式碼。如果想要將該程式碼重用於多個應用程式,那麼專案的建立步驟將略有差異(檢視 Flutter Packages 的開發和提交),但平台通道程式碼仍以相同方式編寫。

第一步:建立一個新的應用專案

首先建立一個新的應用:

  • 在終端中執行:flutter create batterylevel

預設情況下,我們的範本使用 Kotlin 編寫 Android 或使用 Swift 編寫 iOS 程式碼。要使用 Java 或 Objective-C,請使用 -i 和/或 -a 標誌:

  • 在終端中執行:flutter create -i objc -a java batterylevel

第二步:建立 Flutter 平臺客戶端

應用程式的 State 類保持當前應用的狀態。擴充它以保持當前的電池狀態。

首先,建構通道。在返回電池電量的單一平台方法中使用 MethodChannel

通道的客戶端和宿主端透過傳遞給通道建構函式的通道名稱進行連線。一個應用中所使用的所有通道名稱必須是唯一的;使用唯一的 域字首 為通道名稱新增字首,比如:samples.flutter.dev/battery

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

接下來,在方法通道上呼叫方法(指定透過 String 識別符號 getBatteryLevel 呼叫的具體方法)。呼叫可能會失敗—比如,如果平台不支援此平台 API(比如在模擬器中執行),所以將 invokeMethod 呼叫包裹在 try-catch 陳述式中。

setState 中使用返回結果來更新 _batteryLevel 內的使用者介面狀態。

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

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

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

最後,將範本中的 build 方法替換為包含以字串形式顯示電池狀態、幷包含一個用於重新整理該值的按鈕的小型使用者介面。

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

步驟 3: 新增 Android 平台的實現

首先在 Android Studio 中開啟 Flutter 應用的 Android 宿主部分:

  1. 啟動 Android Studio

  2. 選擇選單項 File > Open…

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

  4. 在專案檢視中開啟 kotlin 資料夾下的 MainActivity.kt 檔案。

configureFlutterEngine() 方法中建立一個 MethodChannel 並呼叫 setMethodCallHandler()。確保使用的通道名稱與 Flutter 客戶端使用的一致。

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 battery API 來檢索電池電量的 Android Kotlin 程式碼。該程式碼與你在原生 Android 應用中編寫的程式碼完全相同。

首先在檔案頭部新增所需的依賴:

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() 方法下方新增以下新方法:

  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
  }

最後,完成前面新增的 onMethodCall() 方法。你需要處理單個平台方法 getBatteryLevel(),所以在引數 call 中對其進行驗證。該平台方法的實現是呼叫上一步編寫的 Android 程式碼,並使用 result 引數來返回成功和錯誤情況下的響應。如果呼叫了未知方法,則報告該方法。

刪除以下程式碼:

    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // This method is invoked on the main thread.
      // TODO
    }

並替換成以下內容:

    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 客戶端使用的一致。

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 battery API 來檢索電池電量的 Android Java 程式碼。該程式碼與你在原生 Android 應用中編寫的程式碼完全相同。

首先在檔案頭部新增所需的依賴:

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 類中的 onCreate() 方法下方新增以下新方法:

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

最後,完成前面新增的 onMethodCall() 方法,你需要處理單個平台方法 getBatteryLevel(),所以在引數 call 中對其進行驗證。該平台方法的實現是呼叫上一步編寫的 Android 程式碼,並使用 result 引數來返回成功和錯誤情況下的響應。如果呼叫了未知方法,則報告該方法。

移除以下程式碼:

          (call, result) -> {
            // This method is invoked on the main thread.
            // TODO
          }

並替換成以下內容:

          (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

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

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

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

重寫 application:didFinishLaunchingWithOptions: 方法,然後建立一個 FlutterMethodChannel 繫結到名字為 samples.flutter.dev/battery 名稱的 channel:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // This method is invoked on the UI thread.
      // Handle battery messages.
    })

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

然後,新增 iOS Swift 程式碼,使用電池相關的 API 獲取電量。這裡的程式碼和你寫原生 iOS 程式碼別無二致。

AppDelegate.swift 末尾新增以下新的方法:

private func receiveBatteryLevel(result: FlutterResult) {
  let device = UIDevice.current
  device.isBatteryMonitoringEnabled = true
  if device.batteryState == UIDevice.BatteryState.unknown {
    result(FlutterError(code: "UNAVAILABLE",
                        message: "Battery level not available.",
                        details: nil))
  } else {
    result(Int(device.batteryLevel * 100))
  }
}

最後,完成前面新增的 setMethodCallHandler() 方法。你需要處理單個平台方法 getBatteryLevel(),所以在引數 call 中對其進行驗證。該平台方法的實現是呼叫上一步編寫的 iOS 程式碼。如果呼叫了未知方法,則報告該方法。

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 客戶端使用的一致。

#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 battery API 來檢索電池電量的 iOS Objective-C 程式碼。該程式碼與你在原生 iOS 應用中編寫的程式碼完全相同。

AppDelegate 類中的 @end 之前新增以下方法:

- (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 引數來返回成功和錯誤情況下的響應。如果呼叫了未知方法,則報告該方法。

__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 模擬器(注意它並不支援 battery API),應用則會顯示 ‘battery info unavailable’。

第五步:新增 Windows 平台特定實現

首先在 Visual Studio 中開啟你 Flutter 應用 Windows 的 host 部分:

  1. 在你專案的目錄夾下執行一次 flutter build windows 以產生 Visual Studio solution 檔案。

  2. 啟動 Visual Studio。

  3. 選擇 Open a project or solution

  4. 導航至含有你 Flutter 應用的目錄下,然後進入 build 資料夾,然後是 windows 資料夾,然後選擇 batterylevel.sln 檔案,點選 Open

然後新增 platform channel 方法的 c++ 實現:

  1. 在 Solution 瀏覽器中展開 batterylevel > Source Files

  2. 開啟 flutter_window.cpp

首先,在檔案的最頂部新增必要的參考,在 #include "flutter_window.h" 下面寫上就行:

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

#include <memory>

編輯 FlutterWindow::OnCreate 方法,然後建立一個 flutter::MethodChannel 繫結 samples.flutter.dev/battery 名字:

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 battery API 來檢索電池電量的程式碼。該程式碼與你在原生 Windows 應用中編寫程式碼別無二致。

flutter_window.cpp 頂部新增下面的新方法,在 #include 下面新增:

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

最後,完成 setMethodCallHandler() 方法。你可以在這裡處理平台方法,getBatteryLevel(),然後可以在 call 引數中進行測試。這個平台方法呼叫的實現,在之前的步驟中已經完成了。如果呼叫了一個未知的,請報告它。

移除下面的程式碼:

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

然後替換為這個:

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

You should now be able to run the application on Windows. If your device doesn’t have a battery, it displays ‘Battery level not available’.

Step 6: Add a Linux platform-specific implementation

For this example you need to install the upower developer headers. This is likely available from your distribution, for example with:

sudo apt install libupower-glib-dev

Start by opening the Linux host portion of your Flutter app in the editor of your choice. The instructions below are for Visual Studio Code with the “C/C++” and “CMake” extensions installed, but can be adjusted for other IDEs.

  1. Launch Visual Studio Code.

  2. Open the linux directory inside your project.

  3. Choose Yes in the prompt asking: Would you like to configure project "linux"?. This enables C++ autocomplete.

  4. Open the file my_application.cc.

First, add the necessary includes to the top of the file, just after #include <flutter_linux/flutter_linux.h:

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

Add an FlMethodChannel to the _MyApplication struct:

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

Make sure to clean it up in my_application_dispose:

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

Edit the my_application_activate method and initialize battery_channel using the channel name samples.flutter.dev/battery, just after the call to fl_register_plugins:

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

Next, add the C code that uses the Linux battery APIs to retrieve the battery level. This code is exactly the same as you would write in a native Linux application.

Add the following as a new function at the top of my_application.cc just after the G_DEFINE_TYPE line:

static FlMethodResponse* get_battery_level() {
  // Find the first available battery and report that.
  g_autoptr(UpClient) up_client = up_client_new();
  g_autoptr(GPtrArray) devices = up_client_get_devices2(up_client);
  if (devices->len == 0) {
    return FL_METHOD_RESPONSE(fl_method_error_response_new(
        "UNAVAILABLE", "Device does not have a battery.", nullptr));
  }

  UpDevice* device = (UpDevice*)(g_ptr_array_index(devices, 0));
  double percentage = 0;
  g_object_get(device, "percentage", &percentage, nullptr);

  g_autoptr(FlValue) result =
      fl_value_new_int(static_cast<int64_t>(round(percentage)));
  return FL_METHOD_RESPONSE(fl_method_success_response_new(result));
}

Finally, add the battery_method_call_handler function referenced in the earlier call to fl_method_channel_set_method_call_handler. You need to handle a single platform method, getBatteryLevel, so test for that in the method_call argument. The implementation of this function calls the Linux code written in the previous step. If an unknown method is called, report that instead.

Add the following code after the get_battery_level function:

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

你現在應該可以在 Windows 上執行應用了。如果你的裝置沒有電池的話,它會提示 ‘Battery level not available’。

透過 Pigeon 獲得型別安全的通道

在之前的範例中,我們使用 MethodChannel 在 host 和 client 之間進行通訊,然而這並不是型別安全的。為了正確通訊,呼叫/接收訊息取決於 host 和 client 宣告相同的引數和資料型別。 Pigeon 套件可以用作 MethodChannel 的替代品,它將產生以結構化型別安全方式傳送訊息的程式碼。

Pigeon 中,訊息介面在 Dart 中進行定義,然後它將產生對應的 Android 以及 iOS 的程式碼。更復雜的例子以及更多資訊盡在 pigeon

使用 Pigeon 消除了在主機和客戶端之間匹配字串的需要訊息的名稱和資料型別。它支援:巢狀(Nesting)類,訊息轉換為 API,產生非同步包裝程式碼併發送訊息。產生的程式碼具有相當的可讀性並保證在不同版本的多個客戶端之間沒有衝突。支援 Objective-C,Java,Kotlin 和 Swift(透過 Objective-C 互操作)語言。

Pigeon 範例

Pigeon 檔案:

import 'package:pigeon/pigeon.dart';

class SearchRequest {
  String query = '';
}

class SearchReply {
  String result = '';
}

@HostApi()
abstract class Api {
  Future search(SearchRequest request);
}

Dart 用法:

import 'generated_pigeon.dart';

Future<void> onClick() async {
  SearchRequest request = SearchRequest()..query = 'test';
  Api api = SomeApi();
  SearchReply reply = await api.search(request);
  print('reply: ${reply.result}');
}

從 UI 程式碼中分離平台相關程式碼

如果你想要在多個 Flutter 應用中使用你的平台相關程式碼,則將程式碼分離為位於主應用目錄之外的平台外掛會很有用。相關細節檢視 Flutter Packages 的開發和提交

將平台相關程式碼作為 Package 進行提交

與 Flutter 生態中的其他開發者共享你的平台相關程式碼,可檢視 提交 package

自訂通道和編解碼器

除了上面提到的 MethodChannel,你還可以使用更基礎的 BasicMessageChannel,它支援使用自訂的訊息編解碼器進行基本的非同步訊息傳遞。你還可以使用專門的 BinaryCodecStringCodecJSONMessageCodec 類,或建立自己的編解碼器。

您還可以在 cloud_firestore 外掛中檢視自訂編解碼器的範例,該外掛可以序列化和反序列化比預設型別更多的型別。

通道和平台執行緒

目標平台向 Flutter 發起 channel 呼叫的時候,需要在對應平台的主執行緒執行。同樣的,在 Flutter 向目標平台發起 channel 呼叫的時候,需要在根 Isolate 中執行。對應平台側的 handler 既可以在平台的主執行緒執行,也可以透過事件迴圈在後台執行。對應平台側 handler 的返回值可以在任意執行緒非同步執行。

Using plugins and channels from background isolates

Plugins and channels can be used by any Isolate, but that Isolate has to be a root Isolate (the one created by Flutter) or registered as a background Isolate for a root Isolate.

The following example shows how to register a background Isolate in order to use a plugin from a background Isolate.

import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';

void _isolateMain(RootIsolateToken rootIsolateToken) async {
  BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
  SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
  print(sharedPreferences.getBool('isDebug'));
}

void main() {
  RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
  Isolate.spawn(_isolateMain, rootIsolateToken);
}

在後台執行緒中執行 channel 的 handlers

要在 channel 對應的平台側的後臺中執行 handler,需要使用 Task Queue API。當前該功能僅支援在 iOS 和 Android。

對應的 Java 程式碼:

@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
  BinaryMessenger messenger = binding.getBinaryMessenger();
  BinaryMessenger.TaskQueue taskQueue =
      messenger.makeBackgroundTaskQueue();
  channel =
      new MethodChannel(
          messenger,
          "com.example.foo",
          StandardMethodCodec.INSTANCE,
          taskQueue);
  channel.setMethodCallHandler(this);
}

Kotlin 版本:

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
  val taskQueue =
      flutterPluginBinding.binaryMessenger.makeBackgroundTaskQueue()
  channel = MethodChannel(flutterPluginBinding.binaryMessenger,
                          "com.example.foo",
                          StandardMethodCodec.INSTANCE,
                          taskQueue)
  channel.setMethodCallHandler(this)
}

Swift 版本:

public static func register(with registrar: FlutterPluginRegistrar) {
  let taskQueue = registrar.messenger.makeBackgroundTaskQueue()
  let channel = FlutterMethodChannel(name: "com.example.foo",
                                     binaryMessenger: registrar.messenger(),
                                     codec: FlutterStandardMethodCodec.sharedInstance,
                                     taskQueue: taskQueue)
  let instance = MyPlugin()
  registrar.addMethodCallDelegate(instance, channel: channel)
}

Objective-C 版本:

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  NSObject<FlutterTaskQueue>* taskQueue =
      [[registrar messenger] makeBackgroundTaskQueue];
  FlutterMethodChannel* channel =
      [FlutterMethodChannel methodChannelWithName:@"com.example.foo"
                                  binaryMessenger:[registrar messenger]
                                            codec:[FlutterStandardMethodCodec sharedInstance]
                                        taskQueue:taskQueue];
  MyPlugin* instance = [[MyPlugin alloc] init];
  [registrar addMethodCallDelegate:instance channel:channel];
}

跳轉到 Android 中的 UI 執行緒

為了符合通道跳轉到 Android UI 執行緒的要求,你可能需要從後臺執行緒跳轉到 Android 的 UI 執行緒以執行通道的方法。在 Android 中的實現方式是:在一個叫 Looper 的 Android UI 執行緒裡 post() 一個 Runnable。這能使得 Runnable 在下一次機會時在主執行緒上執行。

Java 程式碼:

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

Kotlin 程式碼:

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

跳轉到 iOS 中的主執行緒

為了符合通道跳轉到 iOS 主執行緒的要求,您可能需要從後臺執行緒跳轉到 iOS 的主執行緒來執行通道方法。在iOS中,這是透過在主 dispatch queue上執行 block來實現的:

Objective-C 程式碼:

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

Swift 程式碼:

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