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 應用的功能了。
這些組件實現了以下要點:

現在來檢查一下配置是否正確。如果意外跳過了某個包或者犯了錯誤,整個系統都無法正常工作,而且排查問題會非常困難。

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 項目整合在一起,現在只需更新代碼以實現以下目標:

現在 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 上,可以讓編寫原生代碼變得更加簡單和安全。能夠通過流接收事件非常適合處理異步事件。

此外,本文所使用的代碼示例可以在 [此處] 找到。倉庫中有兩個文件夾:

參考鏈接: https://blog.logrocket.com/using-flutter-rust-bridge-cross-platform-development/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SSDLKj68OdacpTR_12E78A