Programação assíncrona em Dart: Futures
Uma das APIs mais básicas que o Dart tem para programação assíncrona são os futures, objetos do tipo Future. Na maior parte, os futures do Dart são…
Uma das APIs mais básicas que o Dart tem para programação assíncrona são os futures — objetos do tipo
Future. Na maior parte, os futures do Dart são muito parecidos com as APIs de
future ou promise encontradas em outras linguagens.
Este artigo discute os conceitos por trás dos futures do Dart e mostra como usar a API Future. Ele também fala sobre o widget
FutureBuilder do Flutter, que ajuda a atualizar uma UI do Flutter de forma assíncrona, com base no estado de um future.
Graças a recursos da linguagem Dart, como async-await, você talvez nunca precise usar a API
Future diretamente. Mas é quase certo que você vai encontrar futures no seu código Dart. E você pode querer
criar futures ou ler código que usa a API Future.
Este artigo é o segundo baseado na série de vídeos Flutter in Focus chamada Asynchronous Programming in Dart. O primeiro artigo, Isolates e event loops, tratou dos fundamentos do suporte do Dart a trabalho em segundo plano. Se você prefere aprender assistindo ou ouvindo, tudo o que este artigo cobre está no vídeo a seguir*.*
Você pode pensar nos futures como pequenas caixas de presente com dados. Alguém entrega uma dessas caixas para você, e ela começa fechada. Um pouco depois, a caixa se abre, e dentro dela há um valor ou um erro.
Então, um future pode estar em um de 3 estados:
Incompleto: a caixa de presente está fechada*.*
-
Concluído com um valor: a caixa está aberta, e o seu presente (dados) está pronto.
Concluído com um erro: a caixa está aberta, mas algo deu errado.
A maior parte do código que você verá a seguir gira em torno de lidar com esses três estados. Você recebe um future e precisa decidir o que fazer até a caixa abrir, o que fazer quando ela abrir com um valor e o que fazer se houver um erro. Você vai ver muito esse padrão 1–2–3.
Você pode se lembrar do event loop (ilustrado abaixo) do nosso artigo sobre o event loop do Dart. Uma coisa boa de saber sobre os futures é que eles são, na prática, apenas uma API criada para facilitar o uso do event loop.
O código Dart que você escreve é executado por uma única thread. Durante todo o tempo em que o seu app está rodando, essa pequena thread fica dando voltas sem parar, pegando eventos da fila de eventos e processando-os.
Digamos que você tenha um código para um botão de download (implementado abaixo como um RaisedButton). O usuário toca, e o seu botão começa a baixar uma imagem.
RaisedButton(
onPressed: () {
final myFuture = http.get('https://my.image.url');
myFuture.then((resp) {
setImage(resp);
});
},
child: Text('Click me!'),
)
Primeiro, ocorre o evento de toque. O event loop recebe o evento e chama o seu handler de toque (que você definiu usando o parâmetro
onPressed do construtor de RaisedButton). O seu handler usa a biblioteca http para fazer uma requisição (http.get()) e recebe um future em troca (myFuture).
Então agora você tem a sua caixinha, myFuture. Ela começa fechada. Para registrar um callback para quando ela abrir, você usa
then().
Depois de ter a sua caixa de presente, você espera. Talvez outros eventos cheguem, o usuário faça outras coisas, e a sua caixinha simplesmente fica ali enquanto o event loop continua girando.
Eventualmente, os dados da imagem são baixados, e a biblioteca http diz: “Ótimo! Eu tenho este future bem aqui.” Ela coloca os dados na caixa e a abre, o que dispara o seu callback.
Agora, aquele pedacinho de código que você passou para then() é executado, e ele exibe a imagem.
Durante todo esse processo, o seu código nunca precisou tocar diretamente no event loop. Não importava o que mais estava acontecendo, nem quais outros eventos chegavam. Tudo o que você precisava fazer era obter o future da biblioteca http e, depois, dizer o que fazer quando o future fosse concluído.
Em código real, você também cuidaria dos erros. Vamos mostrar como fazer isso um pouco mais adiante.
Vamos dar uma olhada mais de perto na API Future, parte da qual você acabou de ver em uso.
Ok, primeira pergunta: como você obtém uma instância de Future? Na maioria das vezes, você não cria futures diretamente. Isso porque muitas das tarefas comuns de programação assíncrona já têm bibliotecas que geram futures para você.
Por exemplo, a comunicação de rede retorna um future:
final myFuture = http.get('http://example.com');
Acessar as preferências compartilhadas também retorna um future:
final myFuture = SharedPreferences.getInstance();
Mas você também pode usar construtores de Future para criar futures.
Construtores de Future
#
O construtor mais simples é o Future(), que recebe uma função e retorna um future que corresponde ao tipo de retorno da função. Depois, a função é executada de forma assíncrona, e o future é concluído com o valor de retorno da função. Aqui está um exemplo de uso de
Future():
void main() {
final myFuture = Future(() {
return 12;
});
}
Vamos adicionar alguns comandos print para deixar a parte assíncrona clara:
void main() {
final myFuture = Future(() {
print('Creating the future.'); // Prints second.
return 12;
});
print('Done with main().'); // Prints first.
}
Se você executar esse código no DartPad (dartpad.dev), toda a função
main termina antes da função passada para o construtor Future(). Isso acontece porque o construtor
Future() apenas retorna, no início, um future incompleto. Ele diz: “Aqui está esta caixa. Segure-a por enquanto, e depois eu vou executar a sua função e colocar alguns dados dentro dela para você.” Esta é a saída do código anterior:
Done with main().
Creating the future.
Outro construtor, Future.value(), é útil quando você já conhece o valor do future. Esse construtor é útil quando você está criando serviços que usam cache. Às vezes você já tem o valor de que precisa, então pode colocá-lo ali diretamente:
final myFuture = Future.value(12);
O construtor Future.value() tem um equivalente para concluir com um erro. Ele se chama Future.error(), funciona essencialmente da mesma forma, mas recebe um objeto de erro e um stacktrace opcional:
final myFuture = Future.error(ArgumentError.notNull('input'));
O construtor de future mais prático é provavelmente o Future.delayed(). Ele funciona como o
Future(), exceto que espera um tempo especificado antes de executar a função e concluir o future.
Uma forma de usar Future.delayed() é ao criar serviços de rede simulados para testes. Se você precisa garantir que o seu spinner de carregamento seja exibido corretamente, um future com atraso é um ótimo aliado.
final myFuture = Future.delayed(
const Duration(seconds: 5),
() => 12,
);
Usando futures
#Agora que você sabe de onde vêm os futures, vamos falar sobre como usá-los. Como mencionamos antes, usar um future é principalmente lidar com os três estados em que ele pode estar: incompleto, concluído com um valor ou concluído com um erro.
O código a seguir usa Future.delayed() para criar um future que é concluído após 3 segundos com o valor 100.
void main() {
Future.delayed(
const Duration(seconds: 3),
() => 100,
);
print('Waiting for a value...');
}
Quando esse código é executado, main() roda de cima para baixo, criando o future e imprimindo “Waiting for a value…”. Durante todo esse tempo, o future está incompleto. Ele só será concluído daqui a 3 segundos.
Para usar o valor concluído, você pode usar o then(). Esse é um método de instância de cada future, que você pode usar para registrar um callback para quando o future for concluído com um valor. Você passa uma função que recebe um único parâmetro do mesmo tipo do future. Assim que o future for concluído com um valor, a sua função é chamada com esse valor.
void main() {
Future.delayed(
const Duration(seconds: 3),
() => 100,
).then((value) {
print('The value is $value.'); // Prints later, after 3 seconds.
});
print('Waiting for a value...'); // Prints first.
}
Esta é a saída do código anterior:
Waiting for a value... *(3 seconds pass until callback executes)*
The value is 100.
Além de executar o seu código, o then() retorna um future próprio, que corresponde ao valor de retorno da função que você passou. Então, se você precisar fazer algumas chamadas assíncronas, pode encadeá-las, mesmo que tenham tipos de retorno diferentes.
_fetchNameForId(12)
.then((name) => _fetchCountForName(name))
.then((count) => print('The count is $count.'));
Voltando ao nosso primeiro exemplo: o que acontece se esse future inicial não for concluído com um valor, e sim com um erro? O método
then() espera um valor. Você precisa de uma forma de registrar outro callback para o caso de erro.
A resposta é usar o catchError(). Ele funciona como o
then(), exceto que recebe um erro em vez de um valor, e é executado se o future for concluído com um erro. Assim como o
then(), o método catchError() retorna um future próprio, então você pode montar uma cadeia inteira de métodos
then() e catchError() que esperam uns pelos outros.
Observação: você não precisa chamar
then()oucatchError()se usar o recurso de linguagem async-await. Em vez disso, você espera (await) o valor concluído e usa try-catch-finally para tratar erros. Para detalhes, veja a seção suporte a assincronia do tour pela linguagem Dart.
Aqui está um exemplo de uso de catchError() para tratar o caso em que um future é concluído com um erro:
void main() {
Future.delayed(
Duration(seconds: 3),
() => throw 'Error!', // Complete with an error.
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err'); // Handle the error.
});
print('Waiting for a value...');
}
Você pode até passar para o catchError() uma função de teste, para verificar o erro antes de invocar o callback. Assim, você pode ter várias funções
catchError(), cada uma verificando um tipo diferente de erro. Aqui está um exemplo de como especificar uma função de teste, usando o parâmetro opcional
test do catchError():
void main() {
Future.delayed(
Duration(seconds: 3),
() => throw 'Error!',
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err');
}, test: (err) { // Optional test parameter.
return err is String;
});
print('Waiting for a value...');
}
Agora que você chegou até aqui, espero que possa perceber como os três estados de um future costumam se refletir na estrutura do código. Há três blocos no exemplo anterior:
O primeiro bloco cria um future incompleto.
Depois, há uma função a ser chamada se o future for concluído com um valor.
Em seguida, há outra função a ser chamada se o future for concluído com um erro.
Há mais um método que você pode querer usar: o whenComplete(). Você pode usá-lo para executar uma função quando o future for concluído, não importa se com um valor ou com um erro.
É meio como o bloco finally de um try-catch-finally. Há código executado se tudo der certo, código para um erro e código que roda independentemente do que aconteça.
Usando futures no Flutter
#Então, é assim que você cria futures, e um pouco sobre como usar os valores deles. Agora vamos falar sobre como colocá-los para trabalhar no Flutter.
Digamos que você tenha um serviço de rede que vai retornar alguns dados JSON, e você quer exibir esses dados. Você poderia criar um
StatefulWidget
que cria o future, verifica se foi concluído ou se houve erro, chama setState() e, de modo geral, faz toda a ligação manualmente.
Ou você pode usar o FutureBuilder. É um widget que vem com o SDK do Flutter. Você passa para ele um future e uma função builder, e ele reconstrói automaticamente os seus filhos quando o future for concluído.
O widget FutureBuilder funciona chamando a sua função builder, que recebe um context e um snapshot do estado atual do future.
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Use a FutureBuilder.
return FutureBuilder<String>(
future: _fetchNetworkData(),
builder: (context, snapshot) {},
);
}
}
Você pode verificar o snapshot para ver se o future foi concluído com um erro:
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',
);
}
throw UnimplementedError("Case not handled yet");
},
);
Caso contrário, você pode verificar a propriedade hasData para ver se ele foi concluído com um valor:
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',
);
} else if (snapshot.hasData) {
// Future completed with a value.
return Text(
json.decode(snapshot.data)['field'],
);
}
throw UnimplementedError("Case not handled yet");
},
);
Se nem hasError nem hasData forem verdadeiros, você sabe que ainda está esperando, e pode exibir algo para esse caso também.
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',
);
} else if (snapshot.hasData) {
// Future completed with a value.
return Text(
json.decode(snapshot.data)['field'],
);
} else {
// Uncompleted.
return Text(
'No value yet!',
);
}
},
);
Mesmo no código Flutter, dá para ver como esses três estados continuam aparecendo: incompleto, concluído com valor e concluído com erro.
Resumo
#
Este artigo falou sobre o que os futures representam e como você pode usar as APIs Future
e FutureBuilder para criar futures e usar os valores concluídos.
Se você quiser aprender mais sobre o uso de futures, com a opção de exemplos executáveis e exercícios interativos para testar o seu entendimento, confira o codelab de programação assíncrona sobre futures, async e await.
Ou siga para o próximo vídeo da série Asynchronous Programming in Dart. Ele fala sobre streams, que são muito parecidos com os futures, pois também podem fornecer valores ou erros. Mas, enquanto os futures entregam apenas um resultado e param, os streams continuam sem parar.
Muito obrigado a Andrew Brogdon, que criou o vídeo no qual este artigo é baseado.
Mais do Dart
Trazendo construtores primários para o Dart
Um olhar por dentro das decisões de design, dos trade-offs e do açúcar sintático por trás da adição de construtores primários no Dart 3.13.
A história da interop com JS no Dart
O suporte ao Wasm acaba de chegar ao Flutter beta atual, graças a um marco empolgante de interop com JavaScript alcançado no Dart 3.3. Para celebrar…