rinf-router is a tiny, ergonomic routing layer that glues Flutter’s
RINF signals to asynchronous Rust handlers.
It takes care of the boring plumbing so you can focus on writing clean,
testable application logic.
- Familiar, Axum-style API
- Zero-boilerplate extraction of data and shared state
- Fully async – powered by
tokioandasync/await - Runs anywhere RINF runs (desktop, mobile, web)
towercompatibility (Basic features, more to come)
- Graceful shutdown support
Add the crate to your existing RINF project.
cargo add rinf-routerA minimal example (run with cargo run):
use {
rinf_router::{Router, State},
rinf::DartSignal,
serde::Deserialize,
std::{
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
},
};
/// Shared state for all handlers
#[derive(Clone)]
struct AppState {
counter: Arc<AtomicUsize>,
}
/// A signal coming from Dart
#[derive(Deserialize, DartSignal)]
struct Increment;
async fn incr(State(state): State<AppState>, _msg: Increment) {
// Atomically increase the counter and print the new value
let new = state.counter.fetch_add(1, Ordering::Relaxed) + 1;
println!("Counter is now: {new}");
}
#[tokio::main]
async fn main() {
let state = AppState {
counter: Arc::new(AtomicUsize::new(0)),
};
Router::new()
.route(incr)
.with_state(state) // 👈 inject shared state
.run()
.await;
}That’s it – incoming Increment signals are automatically deserialized, and the current AppState is dropped right
into your handler!
A router carries exactly one state type.
Trying to register handlers that need different states on the same
router without swapping the state fails to compile:
use rinf_router::{Router, State};
#[derive(Clone)]
struct Foo;
#[derive(Clone)]
struct Bar;
async fn foo(State(_): State<Foo>) { unimplemented!() }
async fn bar(State(_): State<Bar>) { unimplemented!() }
fn main() {
Router::new()
.route(foo) // Router<Foo>
.route(bar) // ❌ Router<Foo> expected, found handler that needs Bar
.run(); // ^^^ mismatched state type
}Fix it by either
Router::new()
.route(foo)
.with_state(state)
.route(bar)
.with_state(other_state)
.run()
.await;or by ensuring both handlers share the same state type.
Handler-level middleware can be applied using the .layer() method for cross-cutting concerns like logging, metrics, or request modification:
use {
rinf_router::{Router, handler::Handler},
rinf::DartSignal,
serde::Deserialize,
tower::{service_fn, ServiceBuilder},
};
#[derive(Deserialize, DartSignal)]
struct MySignal(String);
async fn my_handler(signal: MySignal) {
println!("Handler received: {}", signal.0);
}
#[tokio::main]
async fn main() {
// Create logging middleware using service_fn
let logging_middleware = ServiceBuilder::new()
.map_request(|signal: MySignal| {
println!("Processing signal: {}", signal.0);
signal
});
Router::new()
.route(my_handler.layer(logging_middleware)) // 👈 Apply middleware to handler
.run()
.await;
}Middleware executes in outside-in order: outer layers wrap inner layers, with the handler at the center.
Run cargo doc --open for the full API reference, including:
- Custom extractors
- Error handling
Enjoy – and feel free to open an issue or PR if you spot anything that could be improved!