使用Flutter Rust Bridge进行跨平台开发

有时,在跨平台开发中,我们需要在特定于平台的基础上执行某些代码。 对于 Flutter 应用程序,我们可以利用丰富的插件库来使用许多平台的原生功能,而无需编写自己的实现,因此这可能不会太令人畏惧。

然而,随着我们的需求变得越来越小众,我们可能会发现不存在可以利用某些功能的插件。 这时我们需要考虑编写自己的特定于平台的代码。

您所针对的平台已经具有可用于实现该功能的语言。 例如,在 Android 上,您可以使用 Kotlin 或 Java,而在 Windows 上,您可以使用 C++。

因此,我们需要回答的第一个问题是:为什么我们会考虑使用 Rust 作为我们的平台特定语言?

跳跃前进:

  • 将 Rust 与 Flutter 结合使用的案例

  • 如何使用 Rust 和 Flutter Rust Bridge 解决这些问题

  • 创建我们的 Flutter Rust Bridge 项目

将 Rust 与 Flutter 结合使用的案例

假设我们需要获取用户当前使用的设备的当前电池电量。 如果没有提供此功能的插件,我们至少需要考虑两件事:

  • 我们的原生代码和Flutter之间如何传输数据

  • 特定于平台的语言(如 C++/Kotlin/Swift/等)

现在让我们逐一探讨这些挑战。

原生代码和Flutter之间传输数据

如果我们有大量数据要在 Flutter 应用程序如何在WhatsApp上发送即时视频消息(最新功能)和本机代码之间传输,我们需要创建绑定来来回传输数据。 这个过程涉及相当多的样板文件,当我们的实现发生变化时,必须更新这些绑定可能会令人沮丧且耗时。

幸运的是,有一个 名为 Pigeon 的软件包 可以为开发人员自动完成大部分工作。 这对我们的情况有帮助吗?

快速浏览一下 Pigeon 支持平台的文档,我们会发现 Pigeon 支持生成以下内容:

  • 适用于 iOS 的 Objective-C 代码( 可用于 Swift )

  • 适用于 iOS 的实验性 Swift 代码

  • Android 的 Java 代码(可通过 Kotlin 访问)

  • 适用于 Android 的实验性 Kotlin 代码

  • 适用于 Windows 的实验性 C++ 代码

Despite the uptake of Kotlin and Swift on the targeted mobile platforms, Pigeon support for these platforms is still in an experimental phase.

对于移动应用程序来说,这并不是什么大问题,因为您可以从 Kotlin 调用 Java 代码,从 Swift 调用 Objective-C 代码。 这允许您在应用程序中利用生成的 Pigeon 代码。 然而,桌面和网络应用程序是另一回事。

Pigeon 对 Windows 的支持是实验性的,如何修复Windows 11上的硬盘问题(4种方法)对 Linux 的支持尚不存在。 如果您希望将应用程序投入生产,那么使用实验性生成器并不是一个好主意。 对于 Linux 或 Web,您无论如何都会回到手动编写平台绑定。

如果您正在编写一个针对许多平台的应用程序,这可能会成为一件苦差事,特别是如果您的应用程序针对的是 Pigeon 支持处于实验阶段或不存在的平台。 这不是一个无法管理的工作量,但它仍然是一个工作量。

使用特定于平台的语言的缺点

Flutter 本质上是一种跨平台语言 。 这意味着一些编写 Flutter 应用程序的人可能不会遇到真正的特定于平台的语言,例如 Kotlin 或 Swift。

在这些情况下,在 StackOverflow 中搜索实现并尝试猜测方法并不困难。 Kotlin 和 Swift 将为您管理内存,当对象不再被访问时将其销毁,因此更难(尽管并非不可能)引入内存泄漏。

在 Windows 和 Linux 上,这是一个完全不同的主张。

要实现本机功能,您必须使用 C++。 由于 Pigeon 支持在 Windows 上是实验性的,并且在 Linux 上不存在,因此您不仅必须使用您可能不理解的语言编写特定于平台的代码,还必须使用绑定代码。

让这个命题变得更加困难的是,趣知笔记 - 分享有价值的教程!你必须管理自己的记忆并跟踪自己的参考资料。 最重要的是,在本机层发生的任何未捕获的异常都会导致您的应用程序在桌面上崩溃。

简而言之,即使您可以创建绑定代码,如果您是初学者,您也只会绑定到可能非常不安全的代码。 这不是很吸引人。

如何使用 Rust 和 Flutter Rust Bridge 解决这些问题

要在 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 crate来利用本机功能,而不是编写自己的实现。 在 Rust 项目中

最后,Rust 代码中未捕获的异常通过 panic,您可以查看并进行相应的故障排除。 与 Windows 上的本机未捕获异常导致桌面崩溃相比,这是一种更好的体验。

为了演示这一点,让我们创建一个简单的 Flutter 应用程序,用于获取运行它的设备的当前电池电量。

创建我们的 Flutter Rust Bridge 项目

首先,让我们安装一些依赖项 flutter_rust_bridge。 它们是 Rust 编程语言和 LLVM。

首先 从网站下载并安装 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 集成到我们的项目中。

它本身并不是一个复杂的设置。 但是,如果我们有任何步骤错误,我们的项目将无法构建,并且很难排查原因。 如果这也是您第一次接触 Rust,我还将对我们正在做的事情提供一些解释,以帮助您了解正在发生的情况。

首先,导航到您的 Flutter 项目。 在项目内,执行 cargo new native --lib从命令行。

注意 native只是 Rust 项目的项目名称。 如果需要,您可以更改它,但请记住,每次我们在本文的代码示例中引用它时,您都必须更新它。

接下来,在本机目录中,打开 cargo.toml。 在下面 [dependencies]标题,添加以下内容:

flutter_rust_bridge = "1"

在下面添加以下条目 [package]:

[lib]
crate-type = ["lib", "cdylib", "staticlib"]

我们的 cargo.toml文件现在应如下所示:

对于上下文, cargo.toml文件 是包含我们 Rust 项目信息的 。 该文件还包含我们的项目所依赖的其他包(或者在使用 Rust 时称为板条箱)。

我们继续吧。 内 native目录中,从命令提示符或终端执行以下命令:

cargo install flutter_rust_bridge_codegen
flutter pub add --dev ffigen && flutter pub add ffi

这会将 Rust 的代码生成工具添加到 Rust 项目中,并将 FFI 生成位添加到 Flutter 项目中。

配置我们的 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 端”部分

  • 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

设置我们的 Windows 项目集成

终于到了将我们的原生 Rust 项目与 Flutter 集成的时候了。 为此,请下载 cmakeRust 使用的文件 并将其放入您的项目中 windows目录。 然后,在第 57 行左右,之后 include(flutter/generated_plugins.cmake),添加以下行:

include(./rust.cmake)

回到我们的 Rust 项目配置

现在,在您选择的编辑器中,从以下位置打开 Rust 项目 native目录。 创建一个名为的新文件 api.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

为了您的理智,您应该将此命令保存到类似的文件中 generate_bindings.bat。 更新 Rust 代码并公开任何新函数后,您需要重新运行它。

打开您的 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 代码。

从 Flutter 调用 Rust

在我们的 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( // 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(),
            );
          },
        ),
      ),
    );
  }
}

运行项目会出现以下窗口:

我们的应用程序有效! 现在让我们实际获取电池统计数据。

首先,我们来更新一下我们的 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"]}

现在,是时候实现我们的应用程序实现的实际功能了。 我们的应用程序实现两个功能:

  1. 检查系统中是否有电池

  2. 随着时间的推移发出电池状态更新

现在我们来实现这些功能。

检查是否有电池

我们从系统检索当前电池存在状态的函数如下所示:

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

本质上,我们创建一个容器 SYSTEM_POWER_STATUS,使用默认值对其进行初始化,然后将其传递给 GetSystemPowerStatus 函数。 然后, 我们可以使用API文档来了解结果 。

在这种情况下, 128意味着没有电池。 只要这个返回值不等于 128,应该有电池。

随着时间的推移接收电池更新

为了能够随着时间的推移接收电池更新,我们的应用程序必须通过 Stream。 幸运的是, StreamSink附带 flutter_rust_bridge,因此通过流发送事件很简单。

在我们的 api.rs,在文件顶部附近添加 RwLock这定义了我们的 Stream:

static BATTERY_REPORT_STREAM: RwLock<Option<StreamSink<BatteryUpdate>>> = RwLock::new(None);

然后,我们创建一个新函数,名为 battery_event_stream分配 this 的值 RwLock到 Stream正在传递给 Rust:

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

初始化流并设置数据模型后,终于可以连接事件生成了。

为此,我们创建一个 init设置订阅并在电池状态发生变化时将事件发送到流中的函数。 执行此操作时我们需要小心,因为当拔掉设备插头时,某些属性(例如 ChargeRateInMilliwatts) 将返回 null。

幸运的是,通过在 Rust 中使用模式匹配来安全地处理这些空值非常容易,正如我们在这里看到的:

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

在 Flutter 应用程序中显示电池结果

因为我们已经将 Rust 项目与 Flutter 项目集成,所以我们所要做的就是更新我们的代码以实现以下目标:

  1. 致电 init函数开始监听 Rust 库中的事件

  2. 用一个 FutureBuilder显示系统是否有电池

  3. 用一个 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( // For when results emit once
              future: api.getBatteryStatus(),
              builder: (context, data) {
                return Text(
                  'System has battery present: ${data.data}',
                  style: TextStyle(
                      color: (data.data ?? false) ? Colors.green : Colors.red),
                );
              },
            ),
            StreamBuilder( // For when there are results over time
              stream: api.batteryEventStream(),
              builder: (context, data) {
                if (data.hasData) {
                  return Column(
                    children: [
                      Text(
                          "Charge rate in milliwatts: ${data.data!.chargeRatesInMilliwatts.toString()}"),
                      Text(
                          "Design capacity in milliwatts: ${data.data!.designCapacityInMilliwattHours.toString()}"),
                      Text(
                          "Full charge in milliwatt hours: ${data.data!.fullChargeCapacityInMilliwattHours.toString()}"),
                      Text(
                          "Remaining capacity in milliwatts: ${data.data!.remainingCapacityInMilliwattHours}"),
                      Text("Battery status is ${data.data!.status}")
                    ],
                  );
                }
                return Column(
                  children: [
                    Text("Waiting for a battery event."),
                    Text(
                        "If you have a desktop computer with no battery, this event will never come..."),
                    CircularProgressIndicator(),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

更新代码后,我们可以继续在 Windows 上运行 Flutter 应用程序。 几秒钟后(或者,如果拔掉笔记本电脑的插头),应显示以下内容:

随着时间的推移,当电池电量更新时,这些值将通过流发出,并且 UI 将自动更新。

结论

使用 Rust 实现本机平台功能,尤其是在 Windows 上,可以使编写本机代码变得更容易、更安全。 能够通过流接收事件也非常适合异步事件。

与往常一样,本指南中使用的代码示例可 在 Github 上找到 。 该存储库中有两个文件夹。

这 batterytest文件夹是一个独立的 Rust 控制台应用程序,它充当沙箱,供我自行测试 Windows API 调用。 在添加 Flutter 解决方案之前能够检查我的调用是否正常运行,这本身就很有价值。

这 windows_battery_check文件夹包含完整的 Flutter 项目,包括 Rust 库和代码。

猜你喜欢

转载自blog.csdn.net/weixin_47967031/article/details/132817015