通过 Flutter 显示 HN:Rust GUI 库
Show HN: Rust GUI Library via Flutter

原始链接: https://cjycode.com/posts/rust-ui-flutter/

这篇博文演示了如何使用作者开发的 Flutter 框架和开源“flutter_rust_bridge”库以 Rust 编程语言创建图形用户界面(GUI)。 flutter_rust_bridge 允许 Rust 和 Flutter 之间进行通信,允许使用 Rust 定义应用程序状态和逻辑,同时利用 Flutter 广泛的生态系统和强大的 GUI 工具来构建交互式 UI。 这种方法的好处包括 Flutter 的流行、成熟、庞大的生态系统以及由于可用的丰富的第三方软件包而易于实现。 此外,“热重载”功能通过在编码更改期间即时视觉更新来加快开发时间,而不会丢失状态或重新编译延迟。 此外,相同的代码库可以跨多个平台运行,包括 Android、iOS、Linux、MacOS、Windows 和 Web 浏览器。 然而,应该指出的是,该方法涉及混合解决方案,而不是纯粹的 Rust,因为它集成了 Rust 和 Flutter 代码库。 尽管如此,与其他语言相比,学习 Flutter 相对简单,尤其是考虑到对 Rust 的熟悉程度。 对于网络平台的一些批评是适用的,这使得它更适合应用程序而不是传统的网页。 最后,Flutter 包含大量样板代码,尽管这些部分在较小的项目中可能保持不变。 帖子中提供的两个示例展示了使用“flutter_rust_bridge”对计数器和待办事项列表应用程序进行 Rust 状态管理。 这两个示例都提供了各种方法将 Rust 状态和逻辑与相应的 Flutter UI 元素连接起来。 本教程还提到了组织和配置项目组件的其他技术,引用了 functionnal_widget 和个性化配置的使用。 最后,读者可以从给定的 GitHub 存储库或博客文章中提到的各个示例文件夹中访问完整的代码、说明和更多详细信息。

Hi, I made a bridge (https://github.com/fzyzcjy/flutter_rust_bridge v2.0.0) between Flutter and Rust, which auto translates syntaxes like arbitrary types, &mut, async, traits, results, closure (callback), lifetimes, 目标是在两者之间架起一座桥梁,就像使用一种语言一样无缝。然后,作为示例,我展示了如何利用 Flutter 使用 GUI 编写 Rust 应用程序。 链接中对此进行了详细讨论。要使用它,请访问 GitHub 存储库,或参阅文章末尾了解详细的文件夹和命令。当我几年前首次发布 1.0.0 时,它只包含很少的功能 到今天。 这是我和贡献者们共同努力的结果,非常感谢所有贡献者!
相关文章

原文

Background #

Rust has been “the most desired programming language” for 8 years (by StackOverflow and GitHub 1 2), and many people want to write programs with a GUI in Rust.

Therefore, in this blog, I will share an approach by utilizing Flutter and the https://github.com/fzyzcjy/flutter_rust_bridge I made.

To have a try, please visit the GitHub repo or the demo folder at the end of this article.

Pros of the approach #

Firstly, Flutter is popular and mature. It is “the most popular cross-platform mobile SDK” (by StackOverflow 1 2). In addition, many developers and well-known brands (e.g. see this long list) are using it. It is a lot of work to make an engine feature-rich and mature like that.

Secondly, it also has a large ecosystem, making it easy to implement what we want. For example, even if we want to add some beautiful confetti 🎉 animations, there exists a package for us. Let alone other beautiful widgets and functionalities, and its intrinsic flexibility to control every pixel.

The “hot-reload” feature makes developing UI much faster, since it happens frequently to tweak the UI. When changing code, as is shown in the gif below, the updated UI can be seen almost instantly, without losing state or wait for a recompilation.

Flutter is also cross-platform. The same codebase can not only be run on Android and iOS, but also on Linux, MacOS, Windows and Web.

Hot-reload to add a confetti to UI

Cons of the approach #

Firstly, this approach is not 100% pure Rust (e.g. Rust state/logic, Flutter UI). However, this seems in analogy to many other Rust UIs - write a custom DSL using macros, or another language like HTML/CSS/Slint. Such split also follows separation-of-concerns and is adopted (e.g. link). In addition, Flutter is easy to learn, especially if understanding Rust.

Secondly, honestly speaking, I heard some criticism about web platform. It seems more suitable for “apps” on web and other platforms (real-world e.g. Google Earth, Rive’s animation editor, …) than static webpages.

Last but not least, Flutter has a bunch of boilerplate/scaffold code. My humble understanding is that, for small projects, those files are usually not changed, thus similar to not existing. For large projects, modifiability is indeed customizability.

What’s flutter_rust_bridge? #

The goal is to make a bridge between the two, seamlessly as if working in one single language. It translates many things automatically, such as arbitrary types,&mut, async, traits, results, closure (callback), lifetimes, etc.

Therefore, it is quite general-purpose, and “Rust GUI via Flutter” is just one of the many scenarios. Other typical usages include using arbitrary Rust libraries for Flutter, and writing code such as algorithms in Rust while others in Flutter.

Example: A counter app #

Here, I demonstrate one of the many possible ways to integrate Rust with Flutter. Since flutter_rust_bridge is unopinionated and general-purpose, there can be many other approaches, such as a Redux-like or Elm-like one.

flutter_rust_bridge supports quite rich Rust syntax, such as arbitrary types, results, traits, async, streams, etc. But let us keep it simple and define Rust state and logic as:

#[frb(ui_state)]
pub struct RustState {
    pub count: i32,
}

impl RustState {
	pub fn new() -> Self {
		Self { count: 100 }
	}

	#[frb(ui_mutation)]
    pub fn increment(&mut self) {
	    self.count += 1;
    }
}

// Indeed flutter_rust_bridge can support something complex such as:
// impl MyTrait for MyType {
//     pub fn f(&self, callback: impl Fn(String) -> FancyEnum,
//              stream: StreamSink<Whatever>) -> Result<(FancyStruct, Hello)> { .. }
// }

Remark: The #[frb(ui_state)] and #[frb(ui_mutation)] are very lightweight (only a dozen line of code), and there is no magic hidden.

Then, the UI is like the following. Flutter is declarative, thus we can naturally translate the sentence “a column with padding, containing a text showing current count, and a button for increment” into:

Widget body(RustState state) => [
  Text('Count: ${state.count}'),
  TextButton(onPressed: state.increment, child: Text('+1')),
].toColumn().padding(all: 16);

Remark: Similarly, there are many ways to write a Flutter UI, but here we choose one with simplicity. For larger projects, functional_widget (which adds one-line annotation for widget functions) and many tunable things can be configured.

Now we can run app and play with it in a command (for full code directory, please refer to the end of article). As a bonus, we can modify the UI and see it immediately shown, thanks to hot-reload.

(Optional) A todo-list app #

Feel free to skip this section, since it mainly serves for completeness.

Todo-list app seems to be quite common when it comes to examples, so let’s also make one, and again it is only one of the many possible approaches that flutter_rust_bridge can support.

Define the states:

#[frb(ui_state)]
pub struct RustState {
    items: Vec<Item>,
    pub input_text: String,
    pub filter: Filter,
    next_id: i32,
}

#[derive(Clone)]
pub struct Item {
    pub id: i32,
    pub content: String,
    pub completed: bool,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Filter {
    All,
    Active,
    Completed,
}

&mldr;some actions to update it:

#[frb(ui_mutation)]
impl RustState {
    pub fn add(&mut self) {
        let id = self.next_id;
        self.next_id += 1;
        self.items.push(Item { id, content: self.input_text.clone(), completed: false });
        self.input_text.clear();
    }

    pub fn remove(&mut self, id: i32) {
        self.items.retain(|x| x.id != id);
    }

    pub fn toggle(&mut self, id: i32) {
        let entry = self.items.iter_mut().find(|x| x.id == id).unwrap();
        entry.completed = !entry.completed;
    }
}

&mldr;some more business logic:

impl RustState {
    pub fn new() -> Self {
        Self {
            items: vec![],
            input_text: "".to_string(),
            filter: Filter::All,
            next_id: 0,
            base_state: Default::default(),
        }
    }

    pub fn filtered_items(&self) -> Vec<Item> {
        self.items.iter().filter(|x| self.filter.check(x)).cloned().collect()
    }
}

impl Filter {
    fn check(&self, item: &Item) -> bool {
        match self {
            Self::All => true,
            Self::Active => !item.completed,
            Self::Completed => item.completed,
        }
    }
}

&mldr;and the UI. It is a plain translation of “I want a column of things, the first one is a text field, second one is a list view, etc”, and looks similar to other UI DSLs:

Widget body(RustState state) => [
  SyncTextField(
    decoration: InputDecoration(hintText: 'Input text and enter to add a todo'),
    text: state.inputText,
    onChanged: (text) => state.inputText = text,
    onSubmitted: (_) => state.add(),
  ),
  ListView(children: [
    for (final item in state.filteredItems()) todoItem(state, item)
  ]).expanded(),
  [
    for (final filter in Filter.values)
      TextButton(
        onPressed: () => state.filter = filter,
        child: Text(filter.name).textColor(state.filter == filter ? Colors.blue : Colors.black87),
      ),
  ].toRow(),
].toColumn().padding(all: 16);

Widget todoItem(RustState state, Item item) => [
  Checkbox(value: item.completed, onChanged: (_) => state.toggle(id: item.id)),
  Text(item.content).expanded(),
  IconButton(icon: Icon(Icons.close), onPressed: () => state.remove(id: item.id)),
].toRow();

(For full code directory, please refer to the end of article.)

Conclusion #

In summary, we see how Flutter can be used when we want to write Rust program that needs a GUI. Feel free to ping me (I check GitHub inbox most frequently) if there are any questions!

Appendix: Full code and detailed commands #

Full code is in frb_example/rust_ui_counter and frb_example/rust_ui_todo_list of https://github.com/fzyzcjy/flutter_rust_bridge. Most are auto-generated boilerplate files (since Flutter has a lot of features), and the interesting files are merely src/app.rs and ui/lib/main.dart. To run the demo, enter ui directory and execute flutter_rust_bridge_codegen generate && flutter run.

联系我们 contact @ memedata.com