Flutter 與 Rust 跨界聯手:打造跨平臺開發新紀元
使用 Rust 與 Flutter 的理由
假設我們需要獲取當前設備的電池電量。如果沒有任何插件提供這種功能,那就必須解決兩個問題:如何在本地代碼和 Flutter 之間傳輸數據,以及如何處理不同平臺的特定語言(如 C++/Kotlin/Swift 等)。
數據傳輸挑戰
在 Flutter 應用和本地代碼之間傳輸大量數據時,創建綁定以實現在兩者間的數據交換是必要的。這個過程涉及到大量的樣板代碼,並且當實現發生變化時更新這些綁定可能既耗時又令人沮喪。幸運的是,有一些工具可以幫助自動化這一過程,例如 Pigeon。
儘管 Pigeon 支持多種平臺,但對於桌面和 Web 應用的支持仍在實驗階段。這意味着如果你的應用目標包括 Linux 或 Web,你可能還得手動編寫平臺綁定,這是一項不小的挑戰。
平臺特定語言的缺點
對一些 Flutter 開發者來說,他們可能從未接觸過 Kotlin 或 Swift 這樣的平臺特定語言。雖然 Kotlin 和 Swift 管理內存相對簡單,但在 Windows 和 Linux 上使用 C++ 則完全不同——你需要自己管理內存,任何未捕獲的異常都可能導致桌面應用程序崩潰。
Rust 與 Flutter Rust Bridge 的解決方案
使用 Rust 和 Flutter Rust Bridge 可以解決上述許多問題。Flutter Rust Bridge 支持 Android、iOS、Windows、Linux、macOS 和 Web,幾乎涵蓋了所有主流平臺。它不僅自動生成所有的綁定代碼,還支持異步操作,而且相比 C++,Rust 更加安全。此外,任何在 Rust 代碼中的未捕獲異常都會通過 panic 機制傳遞給 Flutter,而不是直接導致應用崩潰。
創建 Flutter Rust Bridge 項目
首先,安裝 flutter_rust_bridge 所需的依賴項,包括 Rust 編程語言和 LLVM。可以通過運行winget install -e --id LLVM.LLVM
來設置 LLVM。接下來,根據是否已有 Flutter 項目,可以選擇從模板開始或者向現有項目添加 Rust 支持。
配置 Rust 項目
進入 Flutter 項目目錄後,執行cargo new native --lib
來創建 Rust 項目。然後,在native/Cargo.toml
文件中添加 flutter_rust_bridge 依賴,並配置 crate 類型爲["lib", "cdylib", "staticlib"]
。接着,安裝 flutter_rust_bridge_codegen 到 Rust 項目,並在 Flutter 項目中添加 ffigen 和 ffi 依賴。
配置 Flutter 項目
最後,在 Flutter 項目的 native 目錄下,添加 flutter_rust_bridge、build_runner、freezed 及 freezed_annotation 依賴。這樣就完成了基本配置,準備好開始用 Rust 增強 Flutter 應用的功能了。
這些組件實現了以下要點:
-
flutter_rust_bridge — Flutter Rust Bridge 庫的 “Flutter 端” 部分
-
build_runner — 用於生成平臺綁定中使用的 Dart 代碼
-
freezed — 用於將對象從 Rust 傳遞到 Flutter
現在來檢查一下配置是否正確。如果意外跳過了某個包或者犯了錯誤,整個系統都無法正常工作,而且排查問題會非常困難。
native/config.toml
文件應如下所示:
[package]
name = "native"
version = "0.1.0"
edition = "2021"
# 更多鍵及其定義請參閱 https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
flutter_rust_bridge = "1"
[package]
name = "native"
version = "0.1.0"
edition = "2021"
# 更多鍵及其定義請參閱 https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
flutter_rust_bridge = "1"
同時,pubspec.yaml
應包含以下依賴項:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
ffi: ^2.0.1
flutter_rust_bridge: ^1.49.1
freezed_annotation: ^2.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
ffigen: ^7.2.0
build_runner: ^2.3.2
freezed: ^2.2.1
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
ffi: ^2.0.1
flutter_rust_bridge: ^1.49.1
freezed_annotation: ^2.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
ffigen: ^7.2.0
build_runner: ^2.3.2
freezed: ^2.2.1
設置 Windows 項目的集成。終於到了將原生 Rust 項目與 Flutter 整合的時候了。爲此,請下載相關文件並將其放置在項目中的 windows 目錄內。然後,在大約第 57 行,在include(flutter/generated_plugins.cmake)
之後添加以下行:
include(./rust.cmake)
include(./rust.cmake)
回到 Rust 項目的配置。現在,在所選編輯器中打開位於 native 目錄下的 Rust 項目。在 src 目錄下創建一個名爲 api.rs 的新文件。接着,打開 lib.rs 文件,並在文件頂部添加以下內容:
mod api;
mod api;
現在編寫一些基礎的 Rust 代碼,以便 Flutter 應用調用。在 api.rs 文件中,添加一個簡單的函數測試集成情況:
pub fn helloWorld() -> String {
String::from("Hello from Rust! 🦀")
}
pub fn helloWorld() -> String {
String::from("Hello from Rust! 🦀")
}
生成平臺綁定代碼。現在是時候生成 Flutter 用來調用 Rust 功能的代碼了。在項目根目錄運行以下命令:
flutter_rust_bridge_codegen --rust-input native/src/api.rs --dart-output lib/bridge_generated.dart --dart-decl-output lib/bridge_definitions.dart
flutter_rust_bridge_codegen --rust-input native/src/api.rs --dart-output lib/bridge_generated.dart --dart-decl-output lib/bridge_definitions.dart
爲了方便起見,可以將此命令保存爲 generate_bindings.bat 文件。每次更新 Rust 代碼或暴露新函數後都需要重新運行它。
打開 Flutter 項目,在 lib 目錄下添加以下 native.dart 文件:
import 'dart:ffi';
import 'dart:io' as io;
import 'package:windows_battery_check/bridge_generated.dart';
const _base = 'native';
final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so';
final api = NativeImpl(io.Platform.isIOS || io.Platform.isMacOS
? DynamicLibrary.executable()
: DynamicLibrary.open(_dylib));
import 'dart:ffi';
import 'dart:io' as io;
import 'package:windows_battery_check/bridge_generated.dart';
const _base = 'native';
final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so';
final api = NativeImpl(io.Platform.isIOS || io.Platform.isMacOS
? DynamicLibrary.executable()
: DynamicLibrary.open(_dylib));
在 main.dart 中調用 Rust 代碼。我們的小部件看起來像這樣:
import 'package:windows_battery_check/native.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter Battery Windows"),
),
body: Center(
child: FutureBuilder(
future: api.helloWorld(),
builder: (context, data) {
if (data.hasData) {
return Text(data.data!);
}
return Center(
child: CircularProgressIndicator(),
);
},
),
),
);
}
}
import 'package:windows_battery_check/native.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter Battery Windows"),
),
body: Center(
child: FutureBuilder(
future: api.helloWorld(),
builder: (context, data) {
if (data.hasData) {
return Text(data.data!);
}
return Center(
child: CircularProgressIndicator(),
);
},
),
),
);
}
}
應用程序成功運行!現在讓我們真正獲取電池統計數據。首先,需要更新 cargo.toml 以添加檢索 Windows 上電池狀態所需的依賴項。需要添加 Windows crate 以利用 Windows API 的功能,並且還需要從這個 crate 中加載某些特定特性。
cargo.toml
中的依賴關係如下所示:
[dependencies]
anyhow = "1.0.66"
flutter_rust_bridge = "1"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.43.0", features =["Devices_Power", "Win32_Foundation", "Win32_System_Power", "Win32_System_Com", "Foundation", "System_Power"]}
[dependencies]
anyhow = "1.0.66"
flutter_rust_bridge = "1"
[target.'cfg(target_os = "windows")'.dependencies]
windows = {version = "0.43.0", features =["Devices_Power", "Win32_Foundation", "Win32_System_Power", "Win32_System_Com", "Foundation", "System_Power"]}
接下來實現應用程序的功能:檢查系統中是否存在電池;隨着時間推移發出電池狀態更新。
檢查電池是否存在
獲取當前系統電池存在狀態的函數如下所示:
pub fn getBatteryStatus() -> Result<bool> {
let mut powerStatus: SYSTEM_POWER_STATUS = SYSTEM_POWER_STATUS::default();
unsafe {
GetSystemPowerStatus(&mut powerStatus);
Ok(powerStatus.BatteryFlag != 128)
}
}
pub fn getBatteryStatus() -> Result<bool> {
let mut powerStatus: SYSTEM_POWER_STATUS = SYSTEM_POWER_STATUS::default();
unsafe {
GetSystemPowerStatus(&mut powerStatus);
Ok(powerStatus.BatteryFlag != 128)
}
}
這裏,128 意味着沒有電池存在。只要返回值不等於 128,就表示系統中有電池。
接收隨時間變化的電池更新
爲了讓應用程序能夠隨時間接收到電池更新,必須通過 Stream 發送結果。幸運的是,flutter_rust_bridge 提供了 StreamSink,因此通過流發送事件變得簡單直接。在 api.rs 文件頂部附近添加一個 RwLock 來定義 Stream:
static BATTERY_REPORT_STREAM: RwLock<Option<StreamSink<BatteryUpdate>>> = RwLock::new(None);
static BATTERY_REPORT_STREAM: RwLock<Option<StreamSink<BatteryUpdate>>> = RwLock::new(None);
然後創建一個名爲 battery_event_stream 的函數,該函數將這個 RwLock 的值分配給傳遞給 Rust 的 Stream:
pub fn battery_event_stream(s: StreamSink<BatteryUpdate>) -> Result<()> {
let mut stream = BATTERY_REPORT_STREAM.write().unwrap();
*stream = Some(s);
Ok(())
}
pub fn battery_event_stream(s: StreamSink<BatteryUpdate>) -> Result<()> {
let mut stream = BATTERY_REPORT_STREAM.write().unwrap();
*stream = Some(s);
Ok(())
}
數據模型如下所示:
#[derive(Debug)]
pub struct BatteryUpdate {
pub charge_rates_in_milliwatts: Option<i32>,
pub design_capacity_in_milliwatt_hours: Option<i32>,
pub full_charge_capacity_in_milliwatt_hours: Option<i32>,
pub remaining_capacity_in_milliwatt_hours: Option<i32>,
pub status: ChargingState,
}
#[derive(Debug)]
pub enum ChargingState {
Charging = 3,
Discharging = 1,
Idle = 2,
NotPresent = 0,
Unknown = 255,
}
#[derive(Debug)]
pub struct BatteryUpdate {
pub charge_rates_in_milliwatts: Option<i32>,
pub design_capacity_in_milliwatt_hours: Option<i32>,
pub full_charge_capacity_in_milliwatt_hours: Option<i32>,
pub remaining_capacity_in_milliwatt_hours: Option<i32>,
pub status: ChargingState,
}
#[derive(Debug)]
pub enum ChargingState {
Charging = 3,
Discharging = 1,
Idle = 2,
NotPresent = 0,
Unknown = 255,
}
初始化流和數據模型後,終於可以連接事件生成了。創建一個 init 函數來設置訂閱,並隨着電池狀態的變化向流中發出事件。需要注意處理設備拔出時某些屬性(如 ChargeRateInMilliwatts)可能返回 null 的情況。使用 Rust 中的模式匹配安全地處理這些 null 值非常容易。
在 api.rs 中加入這段代碼後,是時候回到命令行執行之前保存的命令了:
flutter_rust_bridge_codegen --rust-input native/src/api.rs --dart-output lib/bridge_generated.dart --dart-decl-output lib/bridge_definitions.dart
這樣就能在 Flutter 應用中展示電池狀態了。由於 Rust 項目已經與 Flutter 項目整合在一起,現在只需更新代碼以實現以下目標:
-
調用 init 函數開始監聽來自 Rust 庫的事件。
-
使用 FutureBuilder 顯示系統是否含有電池。
-
使用 StreamBuilder 實時顯示電池狀態更新。
現在 HomePage 小部件看起來像下面這樣,因爲它可以直接調用 Rust 庫:
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
void initState() {
api.init();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter Battery Windows"),
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FutureBuilder( // 適用於結果只發射一次的情況
future: api.getBatteryStatus(),
builder: (context, data) {
return Text(
'系統是否包含電池:${data.data}',
style: TextStyle(
color: (data.data ?? false) ? Colors.green : Colors.red),
);
},
),
StreamBuilder( // 適用於隨時間推移有結果的情況
stream: api.batteryEventStream(),
builder: (context, data) {
if (data.hasData) {
return Column(
children: [
Text(
"充電速率(毫瓦):${data.data!.chargeRatesInMilliwatts.toString()}"),
Text(
"設計容量(毫瓦時):${data.data!.designCapacityInMilliwattHours.toString()}"),
Text(
"完全充電容量(毫瓦時):${data.data!.fullChargeCapacityInMilliwattHours.toString()}"),
Text(
"剩餘容量(毫瓦時):${data.data!.remainingCapacityInMilliwattHours}"),
Text("電池狀態爲 ${data.data!.status}")
],
);
}
return Column(
children: [
Text("等待電池事件..."),
Text(
"如果你使用的是一臺沒有電池的臺式機,這個事件可能永遠不會發生..."),
CircularProgressIndicator(),
],
);
},
),
],
),
),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
void initState() {
api.init();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter Battery Windows"),
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FutureBuilder( // 適用於結果只發射一次的情況
future: api.getBatteryStatus(),
builder: (context, data) {
return Text(
'系統是否包含電池:${data.data}',
style: TextStyle(
color: (data.data ?? false) ? Colors.green : Colors.red),
);
},
),
StreamBuilder( // 適用於隨時間推移有結果的情況
stream: api.batteryEventStream(),
builder: (context, data) {
if (data.hasData) {
return Column(
children: [
Text(
"充電速率(毫瓦):${data.data!.chargeRatesInMilliwatts.toString()}"),
Text(
"設計容量(毫瓦時):${data.data!.designCapacityInMilliwattHours.toString()}"),
Text(
"完全充電容量(毫瓦時):${data.data!.fullChargeCapacityInMilliwattHours.toString()}"),
Text(
"剩餘容量(毫瓦時):${data.data!.remainingCapacityInMilliwattHours}"),
Text("電池狀態爲 ${data.data!.status}")
],
);
}
return Column(
children: [
Text("等待電池事件..."),
Text(
"如果你使用的是一臺沒有電池的臺式機,這個事件可能永遠不會發生..."),
CircularProgressIndicator(),
],
);
},
),
],
),
),
);
}
}
更新完代碼後,可以嘗試運行 Windows 上的 Flutter 應用程序。幾秒鐘後(或者拔掉筆記本電源),你將看到如下界面:
隨着時間的推移,當電池電量有所更新時,這些值會通過流發送出來,並且 UI 也會自動更新。
使用 Rust 來實現原生平臺功能,尤其是在 Windows 上,可以讓編寫原生代碼變得更加簡單和安全。能夠通過流接收事件非常適合處理異步事件。
此外,本文所使用的代碼示例可以在 [此處] 找到。倉庫中有兩個文件夾:
-
batterytest
文件夾是一個獨立的 Rust 控制檯應用程序,作爲測試 Windows API 調用的沙箱環境。這讓我能夠在添加 Flutter 解決方案前驗證我的 API 調用是否正常工作,這一點本身就很有價值。 -
windows_battery_check
文件夾則包含了完整的 Flutter 項目,包括 Rust 庫和相關代碼。
參考鏈接: https://blog.logrocket.com/using-flutter-rust-bridge-cross-platform-development/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/SSDLKj68OdacpTR_12E78A