Talking to a server
frus has one HTTP client for every platform: the browser's fetch on the web, a native client
on the desktop and on phones, behind the same API. It is the net feature, and its typed JSON
half is json:
[dependencies]
frus = { version = "0.5", features = ["net", "json"] }
serde = { version = "1", features = ["derive"] }
A request is awaited. The interface goes on drawing while it waits; see
Work that waits for how async code runs in frus.
One request
fetch(url) gets a page as text. Request is the full form: a method, headers, a timeout, a
body, and an answer decoded into your own types with send_json.
use std::time::Duration;
use frus::{FetchError, Request};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize)]
pub struct Post {
pub id: u32,
pub title: String,
pub body: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct NewPost {
pub title: String,
pub body: String,
}
const BASE: &str = "https://jsonplaceholder.typicode.com";
pub async fn posts() -> Result<Vec<Post>, FetchError> {
Request::get(format!("{BASE}/posts"))
.header("Accept", "application/json")
.timeout(Duration::from_secs(10))
.send_json()
.await
}
pub async fn create(post: NewPost) -> Result<Post, FetchError> {
Request::post(format!("{BASE}/posts"))
.json_body(&post)
.send_json()
.await
}
A failure is a FetchError: Network (no connection, no answer in time), Status(code) (the
server said no), or Decode (the answer was not what the type expects). An application
usually turns it into its own error once, with a message for the person, rather than in every
screen — frus-examples/http/src/common/api.rs does exactly that.
Data a screen needs to show itself
use_future(key, make) starts the work the first time the screen is built, and again whenever
key changes. Until it ends, the snapshot it returns has no data; then it has the result.
use frus::{
component, CircularProgressIndicator, FetchError, ListTile, ListView, Request, Text, Widget,
};
use serde::Deserialize;
#[derive(Clone, Deserialize)]
struct Post {
title: String,
}
async fn posts() -> Result<Vec<Post>, FetchError> {
Request::get("https://jsonplaceholder.typicode.com/posts").send_json().await
}
fn post_list() -> Box<dyn Widget> {
Box::new(component(|cx| {
let snapshot = cx.use_future((), posts);
match snapshot.data {
None => Box::new(CircularProgressIndicator::new()),
Some(Err(error)) => Box::new(Text::new(format!("Could not load the posts: {error}"))),
Some(Ok(posts)) => Box::new(ListView::new(posts.len(), 56.0, move |i| {
ListTile::new().title(posts[i].title.clone())
})),
}
}))
}
To load again — a "Retry" button, a pull to refresh — make the key a counter and add one to it.
FutureBuilder::new(key, make, build) is the same thing as a widget, for a part of a screen
built from data of its own; use_stream and StreamBuilder follow a stream of values, such as
the messages of a WebSocket.
Sending: an async handler
When the person starts the request — a button that sends a form — the handler is async. It
says what happens in order, in one place: lock the form, send, show the answer.
use frus::{
component, Button, Column, FetchError, Request, Text, TextField, Widget,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct NewPost {
title: String,
}
#[derive(Clone, Deserialize)]
struct Post {
id: u32,
}
async fn create(post: NewPost) -> Result<Post, FetchError> {
Request::post("https://jsonplaceholder.typicode.com/posts")
.json_body(&post)
.send_json()
.await
}
fn new_post() -> Box<dyn Widget> {
Box::new(component(|cx| {
let title = cx.use_state(String::new);
let sending = cx.use_state(|| false);
let answer = cx.use_state(|| None::<String>);
let (typed, busy, said) = (title.clone(), sending.clone(), answer.clone());
let send = cx.async_callback({
let title = title.clone();
move || {
let (title, busy, said) = (title.clone(), busy.clone(), said.clone());
async move {
busy.set(true);
let result = create(NewPost { title: title.get() }).await;
busy.set(false);
said.set(Some(match result {
Ok(post) => format!("Created post {}", post.id),
Err(error) => format!("Not sent: {error}"),
}));
}
}
});
let mut button = Button::new(if sending.get() { "Sending…" } else { "Send" });
if !sending.get() {
button = button.on_press(send);
}
Box::new(
Column::new()
.spacing(12.0)
.child(
TextField::new(title.get())
.label("Title")
.on_input(move |text: String| typed.set(text)),
)
.child(button)
.child(Text::new(answer.get().unwrap_or_default())),
)
}))
}
The task belongs to the component: if the person leaves the screen while the request is on
its way, the task ends with it and the answer changes nothing. A StatefulWidget writes the
same thing with cx.async_callback(|state| async move { … }) and changes its state with
state.set_state(..); the getting-started guide shows both, and how to cancel a task that a
newer one replaces.
Work that is not waiting
Some work does not wait for anything: it computes — resizing a picture, parsing a large file.
That work goes to another thread with host::spawn(work, then), so that the interface does not
stall while it runs; then is called on the interface's thread with the result.