Skip to main content

Module tokio_integration

Module tokio_integration 

Source
Expand description

§Tokio Integration

Freya has its own async runtime, but many Rust ecosystem crates depend on Tokio for things like HTTP clients, database drivers, etc.

§Setting up the Runtime

#[tokio::main] is completely discouraged because it messes the event loop, instead create a Tokio runtime in main() and enter it before launching Freya. The enter() guard makes Tokio APIs available on the current thread.

use tokio::runtime::Builder;

fn main() {
    let rt = Builder::new_multi_thread().enable_all().build().unwrap();
    // Enter the Tokio runtime so its APIs (channels, timers, etc.) work.
    let _rt = rt.enter();

    launch(LaunchConfig::new().with_window(WindowConfig::new(app)))
}

The _rt guard must remain alive for the duration of the program. Binding it to a variable in main() achieves this.

§What you can use

With the Tokio context active, you can use:

  • tokio::sync channels (mpsc, watch, broadcast, oneshot, etc.)
  • tokio::time utilities (sleep, interval, timeout)
  • Any ecosystem crate that requires a Tokio runtime (e.g. reqwest, sqlx, tonic)

These work well with Freya’s built-in spawn():

let mut count = use_state(|| 0);

use_future(move || {
    async move {
        // tokio::time::sleep works because the Tokio context is active
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
        count.set(42);
    }
});

§Caveat about tokio::spawn

Freya’s reactivity model is single-threaded, which means you cannot update e.g state from another thread, and thus using tokio::spawn is simply not possible due to the Sync and Send bounds.

You may tokio::spawn for background work that does not interact with components. For anything that updates the UI, use Freya’s spawn() instead.

§Watch Channels with use_track_watcher

You can easily subscribe to tokio watch channels using use_track_watcher hook from the sdk feature:

[dependencies]
freya = { version = "...", features = ["sdk"] }
tokio = { version = "1", features = ["sync"] }

The sender can live on any thread/task. use_track_watcher re-renders the component whenever the watch value changes:

fn main() {
    let rt = Builder::new_multi_thread().enable_all().build().unwrap();
    let _rt = rt.enter();

    let (tx, rx) = watch::channel(0);

    launch(
        LaunchConfig::new()
            .with_future(move |_| async move {
                let mut interval = tokio::time::interval(Duration::from_secs(1));
                interval.tick().await;
                let mut i = 0;
                loop {
                    interval.tick().await;
                    i += 1;
                    let _ = tx.send(i);
                }
            })
            .with_window(WindowConfig::new_app(MyApp { rx })),
    )
}

struct MyApp {
    rx: watch::Receiver<i32>,
}

impl App for MyApp {
    fn render(&self) -> impl IntoElement {
        // Re-renders this component whenever the watched value changes.
        use_track_watcher(&self.rx);

        rect()
            .expanded()
            .center()
            .child(format!("Latest value is {}", *self.rx.borrow()))
    }
}

§Examples