Async components and factories

Asynchronous components and factories are almost identical compared to regular components and factories. The only major difference is that they have asynchronous init, update and update_cmd methods. This allows you to await almost everywhere from within the component.

The app we will write in this chapter is also available here. Run cargo run --example simple_async from the example directory if you want to see the code in action.

Because Rust doesn't support async traits yet, we need macros to add support for this feature. To tell the component macro that we're using an async trait, we pass the async parameter to it. The component macro will then utilize the async_trait crate behind the scenes to make everything work. Also, we need to use AsyncComponent instead of Component as trait. Apart from that, the first section is identical.

Similarly, the factory macro needs the async parameter for async factories and the trait changes from FactoryComponent to AsyncFactoryComponent.

#[relm4::component(async)]
impl AsyncComponent for App {
    type Init = u8;
    type Input = Msg;
    type Output = ();
    type CommandOutput = ();

Most functions of async component and factory traits are asynchronous, which allows us to await on futures within those functions. Apart from that, only a couple of types need to be adjusted for the async versions of the traits, for example AsyncComponentSender and AsyncComponentParts.

    async fn init(
        counter: Self::Init,
        root: Self::Root,
        sender: AsyncComponentSender<Self>,
    ) -> AsyncComponentParts<Self> {
        tokio::time::sleep(Duration::from_secs(1)).await;

        let model = App { counter };

        // Insert the code generation of the view! macro here
        let widgets = view_output!();

        AsyncComponentParts { model, widgets }
    }

Awaiting in the init function allows us to perform a late initialization. Depending on how you implement the init function, it might take a long time to complete. Not showing anything in this case can look very odd. Therefore, Relm4 allows you to specify widgets that will be displayed while your async component is initialized.

If your init function doesn't await or completes quickly, you don't need to implement init_loading_widgets.

    fn init_loading_widgets(root: Self::Root) -> Option<LoadingWidgets> {
        view! {
            #[local]
            root {
                set_title: Some("Simple app"),
                set_default_size: (300, 100),

                // This will be removed automatically by
                // LoadingWidgets when the full view has loaded
                #[name(spinner)]
                gtk::Spinner {
                    start: (),
                    set_halign: gtk::Align::Center,
                }
            }
        }
        Some(LoadingWidgets::new(root, spinner))
    }

In this case, we do some basic initialization of our root widget upfront and also add a Spinner for a nice loading animation. As soon as the init function returns, the temporary spinner will be removed automatically and the widgets from the view! macro will be inserted instead.

Finally, the update function completes the trait implementation. Notably, awaiting slow futures will block the processing of further messages. In other words, the update function can only process one message afters the other. Because we use async however, this only affects each async component individually and all other components won't be affected. If you want to process multiple messages at the same time, you should consider using commands.

    async fn update(
        &mut self,
        msg: Self::Input,
        _sender: AsyncComponentSender<Self>,
        _root: &Self::Root,
    ) {
        tokio::time::sleep(Duration::from_secs(1)).await;
        match msg {
            Msg::Increment => {
                self.counter = self.counter.wrapping_add(1);
            }
            Msg::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
    }

The complete code

use std::time::Duration;

use gtk::prelude::*;
use relm4::{
    component::{AsyncComponent, AsyncComponentParts, AsyncComponentSender},
    gtk,
    loading_widgets::LoadingWidgets,
    view, RelmApp, RelmWidgetExt,
};

struct App {
    counter: u8,
}

#[derive(Debug)]
enum Msg {
    Increment,
    Decrement,
}

#[relm4::component(async)]
impl AsyncComponent for App {
    type Init = u8;
    type Input = Msg;
    type Output = ();
    type CommandOutput = ();

    view! {
        gtk::Window {
            gtk::Box {
                set_orientation: gtk::Orientation::Vertical,
                set_spacing: 5,
                set_margin_all: 5,

                gtk::Button {
                    set_label: "Increment",
                    connect_clicked => Msg::Increment,
                },

                gtk::Button {
                    set_label: "Decrement",
                    connect_clicked => Msg::Decrement,
                },

                gtk::Label {
                    #[watch]
                    set_label: &format!("Counter: {}", model.counter),
                    set_margin_all: 5,
                }
            }
        }
    }

    fn init_loading_widgets(root: Self::Root) -> Option<LoadingWidgets> {
        view! {
            #[local]
            root {
                set_title: Some("Simple app"),
                set_default_size: (300, 100),

                // This will be removed automatically by
                // LoadingWidgets when the full view has loaded
                #[name(spinner)]
                gtk::Spinner {
                    start: (),
                    set_halign: gtk::Align::Center,
                }
            }
        }
        Some(LoadingWidgets::new(root, spinner))
    }

    async fn init(
        counter: Self::Init,
        root: Self::Root,
        sender: AsyncComponentSender<Self>,
    ) -> AsyncComponentParts<Self> {
        tokio::time::sleep(Duration::from_secs(1)).await;

        let model = App { counter };

        // Insert the code generation of the view! macro here
        let widgets = view_output!();

        AsyncComponentParts { model, widgets }
    }

    async fn update(
        &mut self,
        msg: Self::Input,
        _sender: AsyncComponentSender<Self>,
        _root: &Self::Root,
    ) {
        tokio::time::sleep(Duration::from_secs(1)).await;
        match msg {
            Msg::Increment => {
                self.counter = self.counter.wrapping_add(1);
            }
            Msg::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
    }
}

fn main() {
    let app = RelmApp::new("relm4.example.simple_async");
    app.run_async::<App>(0);
}