Skip to main content

Rust GUI library via Flutter, done simple

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

…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;
    }
}

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

…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.