撰寫雙端平台程式碼(外掛編寫實現)
本指南介紹瞭如何編寫自訂的平台相關程式碼,某些平台相關功能可透過已有的軟體包獲得,具體細節可檢視: 在 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)和宿主(平台)之間傳遞,如下圖所示:
訊息和響應以非同步的形式進行傳遞,以確保使用者介面能夠保持響應。
客戶端做方法呼叫的時候 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 宿主部分:
-
啟動 Android Studio
-
選擇選單項 File > Open…
-
導航到包含 Flutter 應用的目錄,然後選擇其中的 android 資料夾。點選 OK。
-
在專案檢視中開啟 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 宿主部分:
-
啟動 Android Studio
-
選擇選單項 File > Open…
-
導航到包含 Flutter 應用的目錄,然後選擇其中的 android 資料夾。點選 OK。
-
在專案檢視中開啟 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 宿主部分:
-
啟動 Xcode
-
選擇選單項 File > Open…
-
導航到包含 Flutter 應用的目錄,然後選擇其中的 ios 資料夾。點選 OK。
在使用 Objective-C 的標準範本設定中新增對 Swift 的支援:
-
在專案導航中展開 Expand Runner > Runner。
-
開啟專案導航
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 宿主部分:
-
啟動 Xcode
-
選擇選單項 File > Open…
-
導航到包含 Flutter 應用的目錄,然後選擇其中的 ios 資料夾。點選 OK。
-
確保 Xcode 專案建構沒有錯誤。
-
開啟專案導航 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 部分:
-
在你專案的目錄夾下執行一次
flutter build windows
以產生 Visual Studio solution 檔案。 -
啟動 Visual Studio。
-
選擇 Open a project or solution
-
導航至含有你 Flutter 應用的目錄下,然後進入 build 資料夾,然後是 windows 資料夾,然後選擇
batterylevel.sln
檔案,點選 Open。
然後新增 platform channel 方法的 c++ 實現:
-
在 Solution 瀏覽器中展開 batterylevel > Source Files
-
開啟
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.
-
Launch Visual Studio Code.
-
Open the linux directory inside your project.
-
Choose Yes in the prompt asking:
Would you like to configure project "linux"?
. This enables C++ autocomplete. -
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
,它支援使用自訂的訊息編解碼器進行基本的非同步訊息傳遞。你還可以使用專門的
BinaryCodec
、StringCodec
和
JSONMessageCodec
類,或建立自己的編解碼器。
您還可以在 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.
}