赞
踩
有时,在跨平台开发中,我们需要在特定平台的基础上执行某些代码。对于 Flutter 应用程序,我们可以从丰富的插件库中绘制以使用许多平台的本机功能,而无需编写我们自己的实现,因此这可能不太令人生畏。
但是,随着我们的需求变得越来越小众,我们可能会发现不存在利用某个功能的插件。这时我们需要考虑编写我们自己的特定于平台的代码。
您面向的平台已具有可用于实现该功能的语言。例如,在Android上,您可以使用Kotlin或Java,而在Windows上,您可以使用C++。
所以,我们需要回答的第一个问题是:为什么我们甚至考虑将 Rust 作为我们特定于平台的语言?
假设我们需要获取用户当前使用的设备的当前电池电量。如果没有提供此功能的插件,我们至少需要考虑两件事:
如何在我们的原生代码和 Flutter 之间传输数据
特定于平台的语言(如C++/Kotlin/Swift/等)
现在让我们探讨一下这些挑战中的每一个。
如果我们有大量数据要在 Flutter 应用程序和本机代码之间传输,我们需要创建绑定来回传输数据。此过程涉及相当多的样板文件,当我们的实现发生变化时,必须更新这些绑定可能会令人沮丧且耗时。
幸运的是,有一个名为Pigeon的软件包应该可以为开发人员自动化很多。这对我们的处境有帮助吗?
快速浏览一下Pigeon支持平台的文档,就会发现Pigeon支持生成以下内容:
适用于 iOS 的 Objective-C 代码(可通过 Swift 访问))
适用于 iOS 的实验性 Swift 代码
适用于 Android 的 Java 代码(可通过 Kotlin 访问)
适用于 Android 的实验性 Kotlin 代码
适用于 Windows 的实验性C++代码
尽管 Kotlin 和 Swift 在目标移动平台上得到了采用,但 Pigeon 对这些平台的支持仍处于实验阶段。
对于移动应用程序来说,这没什么大不了的,因为你可以从 Kotlin 调用 Java 代码,从 Swift 调用 Objective-C 代码。这允许您在应用程序中利用生成的 Pigeon 代码。但是,桌面和Web应用程序是另一回事。
Pigeon对Windows的支持是实验性的,对Linux的支持不存在。如果您希望将应用投入生产,那么使用实验性生成器并不是一个好主意。对于 Linux 或 Web,无论如何,您都可以手动编写平台绑定。
如果您正在编写面向许多平台的应用程序,这可能会成为一件苦差事,尤其是当您的应用程序针对 Pigeon 支持处于实验性或不存在的平台时。这不是一个无法管理的工作量,但它仍然是工作量。
Flutter,就其本质而言,是一种跨平台的语言。这意味着一些编写 Flutter 应用程序的人有可能没有遇到真正的平台特定语言,如 Kotlin 或 Swift。
在这些情况下,在StackOverflow中搜索实现并尝试猜测自己的方式并不难。Kotlin 和 Swift 将为您管理内存,在对象不再被访问时处理它们,因此引入内存泄漏更难(尽管并非不可能)。
在Windows和Linux上,这是一个完全不同的主张。
要实现本机功能,您必须使用 C++。由于Pigeon支持在Windows上是实验性的,在Linux上不存在,因此您不仅必须使用您可能不理解的语言编写特定于平台的代码,而且还必须使用绑定代码。
使这个命题更加困难的是,您必须管理自己的记忆并跟踪自己的参考资料。最重要的是,在本机层上发生的任何未捕获的异常都将导致您的应用程序在桌面上崩溃。
简而言之,即使你可以创建绑定代码,你也只会绑定到你的 - 如果你是一个初学者 - 可能非常不安全的代码。这不是很吸引人。
要在 Flutter 项目中使用 Rust,我们必须使用社区生成的 软件包。flutter_rust_bridge
该软件包具有广泛的平台支持,包括Android,iOS,Windows,Linux,macOS和Web。因此,无论您的目标是什么平台,您都可以使用 Rust 和 Flutter Rust Bridge。
快速回顾一下 Rust 和 Flutter Rust Bridge 的好处,它成为一个非常引人注目的案例。
首先,Flutter Rust Bridge 会为您生成所有绑定代码,并支持异步操作,例如发射到 .Stream
接下来,Rust 是一种更容易使用的语言,也比 Windows 上的C++更安全。
此外,您可以在 Rust 项目中使用 Rust crate 来利用本机功能,而不是编写自己的实现。
最后,Rust 代码中未捕获的异常通过 传输到 Flutter,您可以相应地查看和排除故障。与Windows上导致桌面崩溃的本机未捕获异常相比,这是一种更好的体验。panic
为了演示这一点,让我们创建一个简单的 Flutter 应用程序,该应用程序获取运行它的设备的当前电池电量。
首先,让我们为 安装一些依赖项。这些是 Rust 编程语言和 LLVM。flutter_rust_bridge
首先从网站下载并安装 Rust。在命令窗口中,运行以下命令:
winget install -e --id LLVM.LLVM
这将下载并设置 LLVM。
如果你正在使用 Flutter 和 Rust 创建一个新项目,你可以从 Github 克隆这个模板存储库。这个模板是现成的,包含了让 Rust 在你的 Flutter 项目中工作所需的所有零碎的东西。然后,你可以跳到本教程的“编写 Rust 代码”部分。
但是,如果您有一个现有的 Flutter 项目,并且想要添加 Rust,请继续阅读。如果您对 Rust 如何与 Flutter 项目集成感到好奇,这也将很有帮助。
就我而言,我们将 Rust 与我通过运行 创建的全新 Flutter 项目集成。flutter create windows_battery_check
由于我们将对项目进行低级别更改,因此现在是将代码签入源代码管理系统的理想时机。这样,如果我们不小心破坏了您的项目,也很容易撤消。
让我们浏览一下文档,了解如何将 Rust 集成到我们的项目中。flutter_rust_bridge
它本身并不是一个复杂的设置。但是,如果我们弄错了任何步骤,我们的项目将无法构建,并且很难解决原因。我还将提供一些关于我们正在做的事情的解释,以帮助您了解如果这也是您第一次接触 Rust 会发生什么。
首先,导航到您的 Flutter 项目。在项目中,从命令行执行。cargo new native --lib
请注意,这只是 Rust 项目的项目名称。如果需要,可以更改它,但请记住,每次我们在本文的代码示例中引用它时,都必须更新它。native
接下来,在本机目录中,打开 。在标题下,添加以下内容:cargo.toml``[dependencies]
flutter_rust_bridge = "1"
在下面添加以下条目:[package]
[lib] crate-type = ["lib", "cdylib", "staticlib"]
我们的文件现在应如下所示:cargo.toml
对于上下文, 是包含有关 rust 项目信息的文件。这个文件还包含我们的项目所依赖的其他软件包——或者在使用 Rust 时被称为 crates。cargo.toml
让我们继续。在目录中,从命令提示符或终端执行以下命令:native
cargo install flutter_rust_bridge_codegen flutter pub add --dev ffigen && flutter pub add ffi
这会将 Rust 的代码生成工具添加到 Rust 项目中,以及 FFI 生成位添加到 Flutter 项目中。
在我们的目录中,运行以下命令:native
flutter pub add flutter_rust_bridge flutter pub add -d build_runner flutter pub add -d freezed flutter pub add freezed_annotation
这些组件实现以下几点:
flutter_rust_bridge— Flutter Rust Bridge 库的“Flutter-side”部分
build_runner— 用于生成平台绑定中使用的 Dart 代码
freezed— 用于将对象从 Rust 传输到 Flutter
我们已经触及了很多事情,所以让我们花点时间检查一下到目前为止我们的设置是否良好。如果我们不小心跳过了包或犯了错误,则没有任何效果,并且很难解决原因。
我们的文件应如下所示:native/config.toml
[package] name = "native" version = "0.1.0" edition = "2021" # See more keys and their definitions at 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
终于到了将我们的原生 Rust 项目与 Flutter 集成的时候了。趣知笔记为此,请下载 Rust 使用的文件并将其放在项目的目录中。然后,在第 57 行周围,在 之后添加以下行:cmakewindows
include(flutter/generated_plugins.cmake)
include(./rust.cmake)
现在,在您选择的编辑器中,趣知笔记网站地图从目录中打开 Rust 项目。创建一个在目录中调用的新文件。然后,打开文件并将以下内容添加到文件顶部:nativeapi.rs
src``lib.rs
mod api;
现在让我们编写一些非常基本的 Rust 代码,我们可以从我们的 Flutter 应用程序调用它们。在我们的文件中,让我们添加一个非常简单的函数来测试我们的集成:api.rs
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
为了您的理智,您应该将此命令保存到类似 的文件中。在更新 Rust 代码并公开任何新函数后,您需要重新运行它。generate_bindings.bat
打开你的 Flutter 项目。在目录中,添加以下文件:lib``native.dart
// This file initializes the dynamic library and connects it with the stub // generated by flutter_rust_bridge_codegen. import 'dart:ffi'; import 'dart:io' as io; import 'package:windows_battery_check/bridge_generated.dart'; const _base = 'native'; // On MacOS, the dynamic library is not bundled with the binary, // but rather directly **linked** against the binary. final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so'; final api = NativeImpl(io.Platform.isIOS || io.Platform.isMacOS ? DynamicLibrary.executable() : DynamicLibrary.open(_dylib));
至此,我们完成了!我们的 Flutter 项目现在可以调用 Rust 代码了。
在我们的 中,我们将调用非常简单的 Rust 代码。我们执行此操作的小部件如下所示:main.dart
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( // All Rust functions are called as Future's future: api.helloWorld(), // The Rust function we are calling. builder: (context, data) { if (data.hasData) { return Text(data.data!); // The string to display } return Center( child: CircularProgressIndicator(), ); }, ), ), ); } }
运行项目会导致出现以下窗口:
我们的应用程序有效!现在让我们实际获取电池统计数据。
首先,让我们使用在 Windows 上检索电池状态所需的依赖项来更新我们的依赖项。我们需要添加 Windows crate 以利用 Windows API 的功能,并且我们还需要专门从这个 crate 加载某些功能。cargo.toml
我们在其中的依赖项将如下所示: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"]}
现在,是时候实现我们的应用实现的实际功能了。我们的应用程序实现两个功能:
检查系统中是否存在电池
随时间推移发出电池状态更新
现在让我们实现这些功能。
我们从系统中检索当前电池状态的函数如下所示:
pub fn getBatteryStatus() -> Result<bool> { // https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-system_power_status let mut powerStatus: SYSTEM_POWER_STATUS = SYSTEM_POWER_STATUS::default(); unsafe { GetSystemPowerStatus(&mut powerStatus); Ok(powerStatus.BatteryFlag != 128) } }
本质上,我们为 创建一个容器,使用默认值初始化它,然后将其传递给 GetSystemPowerStatus 函数。然后,我们可以使用 API 文档来了解结果。SYSTEM_POWER_STATUS
在这种情况下,表示没有电池存在。只要此返回值不等于 ,就应该存在电池。128``128
为了能够随着时间的推移接收电池更新,我们的应用必须通过 .幸运的是,带有 ,因此通过流发送事件很简单。StreamStreamSink
flutter_rust_bridge
在我们的 中,靠近文件顶部,添加一个定义我们的 :api.rsRwLock
Stream
static BATTERY_REPORT_STREAM: RwLock<Option<StreamSink<BatteryUpdate>>> = RwLock::new(None);
然后,让我们创建一个名为 this 的值的新函数,该函数将 this 的值分配给传递给 Rust 的值:battery_event_streamRwLock
Stream
pub fn battery_event_stream(s: StreamSink<BatteryUpdate>) -> Result<()> { let mut stream = BATTERY_REPORT_STREAM.write().unwrap(); *stream = Some(s); Ok(()) }
由于我们直接与 Windows API 交互,因此我们需要阅读有关电池报告如何在 Windows 上工作的文档。通过阅读这些 API 文档,我们可以了解应该是什么样子以及从中返回的值的含义。BatteryStruct``ChargingState
数据模型如下所示:
#[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, }
随着我们的流被初始化,我们的数据模型被设置,终于到了连接事件生成的时候了。
为此,让我们创建一个函数来设置订阅,并在电池状态更改时将事件发送到流中。当我们这样做时,我们需要小心,因为当设备被拔下时,某些属性(如 )将返回 null。init``ChargeRateInMilliwatts
幸运的是,通过在 Rust 中使用模式匹配来安全地处理这些 null 值非常容易,正如我们在这里看到的:
pub fn init() { Battery::AggregateBattery().unwrap().ReportUpdated(&TypedEventHandler::<Battery, IInspectable>::new(|battery, inspectable| { let agg_battery = Battery::AggregateBattery(); let report = agg_battery.unwrap().GetReport().unwrap(); let battery_outcome = BatteryUpdate { charge_rates_in_milliwatts: match report.ChargeRateInMilliwatts() { Ok(charge_rate) => { Some(charge_rate.GetInt32().unwrap()) } Err(_) => { None } }, design_capacity_in_milliwatt_hours: match report.DesignCapacityInMilliwattHours() { Ok(design_capacity) => { Some(design_capacity.GetInt32().unwrap()) } Err(_) => { None } }, full_charge_capacity_in_milliwatt_hours: match report.FullChargeCapacityInMilliwattHours() { Ok(full_charge) => { Some(full_charge.GetInt32().unwrap()) } Err(_) => { None } }, remaining_capacity_in_milliwatt_hours: match report.RemainingCapacityInMilliwattHours() { Ok(remaining_capacity) => { Some(remaining_capacity.GetInt32().unwrap()) } Err(_) => { None } }, status: match report.Status().unwrap().0 { 3 => Charging, 1 => Discharging, 2 => Idle, 0 => NotPresent, _ => Unknown }, }; println!("Handler Update{:?}", battery_outcome); match BATTERY_REPORT_STREAM.try_read() { Ok(s) => { s.as_ref().unwrap().add(battery_outcome); } Err(_) => { println!("Error when writing battery status."); } } Ok(()) })).expect("Could not subscribe to battery updates"); }
在我们的 中拥有此代码后,是时候从命令行重新运行我们之前保存的命令了: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
因为我们已经将 Rust 项目与我们的 Flutter 项目集成在一起,所以我们所要做的就是更新我们的代码来实现以下目标:
调用函数以开始侦听 Rust 库中的事件init
使用 显示系统是否有电池FutureBuilder
使用 在电池到达时显示电池状态的更新StreamBuilder
我们的小部件现在如下所示,因为它可以直接调用 Rust 库
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。