Corrotinas
Outra boa ferramenta para manter em sua caixa de ferramentas assíncrona são as corrotinas. Corrotinas são Futures
que podem ser interrompidos, iniciados, pausados e retomados manualmente.
Assim como os Futures
regulares, o código em uma corrotina Dioxus será executado até o próximo ponto await
antes do render. Esse controle de baixo nível sobre tarefas assíncronas é bastante poderoso, permitindo tarefas em loop infinito, como pesquisa de WebSocket, temporizadores em segundo plano e outras ações periódicas.
use_coroutine
A configuração básica para corrotinas é o hook use_coroutine
. A maioria das corrotinas que escrevemos serão loops de pesquisa usando async
/await
.
#![allow(unused)] fn main() { fn app(cx: Scope) -> Element { let ws: &UseCoroutine<()> = use_coroutine(cx, |rx| async move { // Connect to some sort of service let mut conn = connect_to_ws_server().await; // Wait for data on the service while let Some(msg) = conn.next().await { // handle messages } }); } }
Para muitos serviços, um loop assíncrono simples lidará com a maioria dos casos de uso.
No entanto, se quisermos desabilitar temporariamente a corrotina, podemos "pausá-la" usando o método pause
e "retomá-la" usando o método resume
:
#![allow(unused)] fn main() { let sync: &UseCoroutine<()> = use_coroutine(cx, |rx| async move { // code for syncing }); if sync.is_running() { cx.render(rsx!{ button { onclick: move |_| sync.pause(), "Disable syncing" } }) } else { cx.render(rsx!{ button { onclick: move |_| sync.resume(), "Enable syncing" } }) } }
Esse padrão é onde as corrotinas são extremamente úteis – em vez de escrever toda a lógica complicada para pausar nossas tarefas assíncronas como faríamos com Promises
de JavaScript, o modelo do Rust nos permite simplesmente não pesquisar nosso Future
.
Enviando valores
Você deve ter notado que o encerramento use_coroutine
recebe um argumento chamado rx
. O que é aquilo? Bem, um padrão comum em aplicativos complexos é lidar com vários códigos assíncronos de uma só vez. Com bibliotecas como o Redux Toolkit, gerenciar várias promessas ao mesmo tempo pode ser um desafio e uma fonte comum de bugs.
Usando corrotinas, temos a oportunidade de centralizar nossa lógica assíncrona. O parâmetro rx
é um canal ilimitado para código externo à corrotina para enviar dados para a corrotina. Em vez de fazer um loop em um serviço externo, podemos fazer um loop no próprio canal, processando mensagens de dentro de nosso aplicativo sem precisar gerar um novo Future
. Para enviar dados para a corrotina, chamaríamos "send" no handle.
#![allow(unused)] fn main() { enum ProfileUpdate { SetUsername(String), SetAge(i32) } let profile = use_coroutine(cx, |mut rx: UnboundedReciver<ProfileUpdate>| async move { let mut server = connect_to_server().await; while let Ok(msg) = rx.next().await { match msg { ProfileUpdate::SetUsername(name) => server.update_username(name).await, ProfileUpdate::SetAge(age) => server.update_age(age).await, } } }); cx.render(rsx!{ button { onclick: move |_| profile.send(ProfileUpdate::SetUsername("Bob".to_string())), "Update username" } }) }
Para aplicativos suficientemente complexos, poderíamos criar vários "serviços" úteis diferentes que fazem um loop nos canais para atualizar o aplicativo.
#![allow(unused)] fn main() { let profile = use_coroutine(cx, profile_service); let editor = use_coroutine(cx, editor_service); let sync = use_coroutine(cx, sync_service); async fn profile_service(rx: UnboundedReceiver<ProfileCommand>) { // do stuff } async fn sync_service(rx: UnboundedReceiver<SyncCommand>) { // do stuff } async fn editor_service(rx: UnboundedReceiver<EditorCommand>) { // do stuff } }
Podemos combinar corrotinas com Fermi
para emular o sistema Thunk
do Redux Toolkit com muito menos dor de cabeça. Isso nos permite armazenar todo o estado do nosso aplicativo dentro de uma tarefa e, em seguida, simplesmente atualizar os valores de "visualização" armazenados em Atoms
. Não pode ser subestimado o quão poderosa é essa técnica: temos todas as vantagens das tarefas nativas do Rust com as otimizações e ergonomia do estado global. Isso significa que seu estado real não precisa estar vinculado a um sistema como Fermi
ou Redux
– os únicos Atoms
que precisam existir são aqueles que são usados para controlar a interface.
#![allow(unused)] fn main() { static USERNAME: Atom<String> = |_| "default".to_string(); fn app(cx: Scope) -> Element { let atoms = use_atom_root(cx); use_coroutine(cx, |rx| sync_service(rx, atoms.clone())); cx.render(rsx!{ Banner {} }) } fn Banner(cx: Scope) -> Element { let username = use_read(cx, USERNAME); cx.render(rsx!{ h1 { "Welcome back, {username}" } }) } }
Agora, em nosso serviço de sincronização, podemos estruturar nosso estado como quisermos. Só precisamos atualizar os valores da view quando estiver pronto.
#![allow(unused)] fn main() { enum SyncAction { SetUsername(String), } async fn sync_service(mut rx: UnboundedReceiver<SyncAction>, atoms: AtomRoot) { let username = atoms.write(USERNAME); let errors = atoms.write(ERRORS); while let Ok(msg) = rx.next().await { match msg { SyncAction::SetUsername(name) => { if set_name_on_server(&name).await.is_ok() { username.set(name); } else { errors.make_mut().push("SetUsernameFailed"); } } } } } }
Valores de Rendimento
Para obter valores de uma corrotina, basta usar um identificador UseState
e definir o valor sempre que sua corrotina concluir seu trabalho.
#![allow(unused)] fn main() { let sync_status = use_state(cx, || Status::Launching); let sync_task = use_coroutine(cx, |rx: UnboundedReceiver<SyncAction>| { to_owned![sync_status]; async move { loop { delay_ms(1000).await; sync_status.set(Status::Working); } } }) }
Injeção Automática na API de Contexto
Os identificadores de corrotina são injetados automaticamente por meio da API de contexto. use_coroutine_handle
com o tipo de mensagem como genérico pode ser usado para buscar um handle.
#![allow(unused)] fn main() { fn Child(cx: Scope) -> Element { let sync_task = use_coroutine_handle::<SyncAction>(cx); sync_task.send(SyncAction::SetUsername); } }