Async e Await


Iremos neste artigo aprender a como usar o async e await no Dart para simplificar o seu código.

Programação Assíncrona Com Dart

Olá a todos e bem-vindos ao artigo sobre programação assíncrona em Dart.

Iremos neste artigo aprender a como usar o async e await no Dart para simplificar o seu código.

Muitas linguagens usam async e await em sua sintaxe e me lembro de achar estranho quando as vi pela primeira vez.

Eu sabia que era possível usar async como tag de uma função e alterar o tipo de retorno.

E em algum lugar no caminho havia uma pausa com uma espera.

Afinal das contas, elas são apenas uma sintaxe alternativa para o uso de futures e streams que podem nos ajudar a ter um código mais limpo e claro.

Vamos começar com um exemplo simples:

class ProcessedData {
ProcessedData(this.data);
final String data;
...
}

Digamos que eu tenho uma classe que represente dados processados.

Eu posso fornecer uma string e ela fará a lógica de negócio que preciso.

Também tenho um método que carregará um valor de ID do disco e outro que buscará algum dado de rede que eu possa usar em minha classe.

Future‹int› _loadFromDisk() {}

Future‹String› _fetchNetworkData(int id) {}

class ProcessedData {
ProcessedData(this.data);
final String data;
...
}

Network e File I/O são operações assíncronas, então elas retornam futures.

Eu gostaria de escrever um método único para juntar todas essas peças.

Primeiro, devemos carregar uma ID do disco, usar esta ID para fazer uma chamada de rede, e então fazer um objeto ProcessedData com o resultado.

Com a API Futures do Dart, podemos usar then para conectar callbacks para que o valor completo do primeiro future se torne o parâmetro para a próxima callback e assim por diante.

Future‹ProcessedData› createData() {
return _loadFromDisk().then((id) {
return _fetchNetworkData(id);
}).then((data) {
return ProcessedData(data);
});
}

Em breve, iremos postar um novo artigo explicando esta técnica e também funciona muito bem.

Contudo, o código não é tão legível como seria se tudo fosse síncrono.

Se não fossem pelos futures, o código poderia ser escrito desta forma, certo? Fazendo algumas ligações, em ordem, sem nada de mais.

ProcessedData createData() {
final id = _loadFromDisk();
final data = _fetchNetworkData(id);
return ProcessedData(data);
}

Bem, a grande diferença com async e await é que podemos ter este código usando futures.

Primeiro, adicionamos a palavra-chave async logo antes de abrir as chaves.

ProcessedData createData() async {
final id = _loadFromDisk();
final data = _fetchNetworkData(id);
return ProcessedData(data);
}

Este é apenas um modo de dizer ao Dart: "Ei! Eu planejo usar a palavra-chave await aqui."

Falando nisso, em seguida colocamos a palavra-chave await após cada future que a função precisa aguardar.

Não pode chamar _fetchNetworkData sem a ID de _loadFromDisk, então precisamos de await ali. E não pode criar um objeto ProcessedData sem os dados da rede. Então, precisamos de um await ali.

ProcessedData createData() async {
final id = await _loadFromDisk();
final data = await _fetchNetworkData(id);
return ProcessedData(data);
}

A última alteração é fazer do tipo de retorno um future.

Future‹ProcessedData› createData() async {
final id = await _loadFromDisk();
final data = await _fetchNetworkData(id);
return ProcessedData(data);
}

Isso pode parecer estranho no começo, porque a declaração de retorno nesta função usa um objeto regular ProcessedData.

Mas antes que createData seja completo, precisa aguardar por dois futures. Isso significa que vai começar a executar, parar e esperar um evento de disco.

Então vai seguir adiante, parar e esperar pelo evento de rede. E somente depois disso pode fornecer um valor.

Quando createData começa a rodar e encontra o primeiro await, no mesmo instante retorna um future para a função que está chamando.

Diz: "Ei! Parece que precisarei esperar algumas coisas. Aqui está uma caixa vazia, se atenha a isso e quando eu terminar de esperar por este disco na rede chamarei a declaração de retorno e colocarei os dados aqui para você. Coloque em um FutureBuilder ou algo assim."

E é assim que funciona.

Antes de seguirmos, vamos dar uma olhada rápida ao loop de eventos e como isso funciona em ambas as versões do código que você acabou de ver.

Começamos com esta aqui que usa a API Futures.

Future‹ProcessedData› createData() {
return _loadFromDisk().then((id) {
return _fetchNetworkData(id);
}).then((data) {
return ProcessedData(data);
});
}

E uma das coisas legais sobre a API Futures é que podemos facilmente ver como o código está construído para os eventos envolvidos.

Primeiro, a função começa a rodar e chama _loadFromDisk. E então espera. Espera por algum dado do disco chegar até o loop de eventos.

Isso completa o future retornado por loadFromDisk. Então, a callback da primeira declaração de evento é invocada e o pedido de rede é expedido.

Em seguida, createData espera novamente. Espera por algum dado de rede chegar ao loop de eventos.

Isso completa o future retornado por _fetchNetworkData, então a segunda callback é invocada, algum dado de processo é criado e agora o future retornado por este createData está completo com este valor.

Agora, vamos fazer a mesma coisa novamente, usando a versão async/await do código. O processo é exatamente o mesmo.

Future‹ProcessedData› createData() async {
final id = await _loadFromDisk();
final data = await _fetchNetworkData(id);
return ProcessedData(data);
}

Antes, podíamos usar as chamadas para then para imaginar como o código é construído, evento a evento.

Aqui, podemos fazer a mesma coisa quebrando o código após cada expressão await.

Então conseguimos essa progressão linha por linha.

Como funciona? A execução de createData é iniciada e chega no primeiro await. Neste ponto, retorna o seu próprio future para a função que está chamando e invoca _loadFromDisk.

Então, assim como antes, aguarda pelo evento File I/O do disco. Isso completa o future retornado por _loadFromDisk, o que significa que createData parou de aguardar e já pode seguir para o resto do código.

Em seguida, chama _fetchNetworkData e aguarda novamente. Eventualmente, os dados de rede chegam até o loop de eventos, que completa o retorno do future com _fetchNetworkData e assim createData fica livre para continuar a se mover.

Ele cria e retorna uma instância de ProcessedData, que completa o future que o createData forneceu a quem o chamou lá no começo.

Como podem ver, em ambos os casos, o mesmo loop de eventos controla a ação e os mesmos futures estão envolvidos.

A única mudança real é que as funções async/await são menores e parecem mais com um código síncrono.

Qual é a relação de async/await com erros?

A resposta é que async e await também nos ajudam a lidar com os erros de um modo mais parecido a um código síncrono.

Se voltarmos ao primeiro exemplo baseado na API Futures, a tela para lidar com erros pode ser assim.

Future‹ProcessedData› createData() {
return _loadFromDisk().then((id) {
return _fetchNetworkData(id);
}).then((data) {
return ProcessedData(data);
})catchError(
(err) {
print("Erro de rede: $err");
return ProcessedData.empty();
},
test: (err) = err is HttpException,
).whenComplete(() {
print("Pronto!);
});
}

Usa catchError para testar e responder a erros, e, quando completo, executa uma callback no final, o erro existindo ou não.

Já com async e await, em vez de usar callbacks adicionais, podemos usar try-catch.

Future‹ProcessedData› createData() async {
try {
final id = await _loadFromDisk();
final data = await _fetchNetworkData(id);
return ProcessedData(data);
} on HttpException catch (err) {
print("Erro de rede: $err");
return ProcessedData.empty();
} finally {
print("Pronto");
}
}

Dentro de uma função com a tag async, blocos try-catch vão lidar com erros assíncronos do mesmo modo que lidam com erros síncronos em funções normais.

Podemos usar as palavras-chave on e catch para reter tipos específicos de exceções e finally vai executar o seu bloco de códigos no final como já era esperado.

Certo, temos mais uma coisa para cobrir e isso é como usar await com um loop for para processar os dados de um stream.

Este é um caso de uso muito menos comum para a palavra-chave await, mas é algo que pode ser feito.

Digamos que eu tenha uma função que pode pegar uma lista de números e os somar.

Isso é bem direto, né? Eu posso apenas usar um loop for para iterar os valores.

int getTotal(List‹int› numbers) {
int total = 0;

for (final value in numbers) {
total += value;
}

return total;
}

E se eu quisesse que essa função, em vez disso, pegasse um stream de números e os somasse de forma assíncrona conforme chegassem, e quando o stream terminasse retornasse o resultado da soma?

Assim como com futures, async/await ajuda a fazer isso acontecer sem a necessidade de alterar a estrutura básica do código.

Primeiro, eu coloco a tag async na função, depois eu altero o tipo de retorno para future e então adiciono a palavra-chave await na frente do for e é isso!

Future‹int› getTotal(Stream‹int› numbers) {
int total = 0;

await for (final value in numbers) {
total += value;
}

return total;
}

Assim como com futures a palavra-chave await está separando a minha função nas partes que são executadas antes e depois de aguardar pelos eventos.

Primeiro, começa a execução e percorre tudo até o await. Então retorna o seu future à função que está chamando e espera por um pedaço de dado chegar.

Quando ele chega, o loop executa uma vez para processar aquele pedaço de dado e então para, e espera pelo próximo.

Talvez o app continue a rodar e faça outras coisas, como coletar lixos, qualquer coisa.

Mas, eventualmente, outro pedaço de dado chega e o loop começa novamente.

Isso continua acontecendo até que o stream termine e finalize.

E quando acontece, a função sai do loop e executa a sua declaração de retorno.

Isso completa o future que o getTotal aqui deu para quem o chamou lá no começo.

Uma coisa importante para ter em mente ao usar await for é que só deve ser usada com streams que serão completados.

Se, por exemplo, tentar usar com um stream de cliques vindo de um botão HTML, o stream vai durar pelo tempo que o botão durar, o que significa que o seu loop poderia ficar lá esperando.

Espero que tenham gostado!

 

428693 Visualizações
Awesome Flutter