Futures em Dart


Hoje, vamos abordar uma das APIs mais básicas que o Dart tem para assíncrona: Futures.

.

Futures

A maioria das linguagens modernas tem algum tipo de suporte para programação assíncrona.

Muitas oferecem API Futures, e algumas chamam-nas de Promise.

Na sua maioria, os Futures do Dart são muito semelhantes aos encontrados em outras linguagens.

Gosto de pensar neles como  caixinhas de presente para dados. Alguém te dá uma e ela vem fechada. Então, um pouco depois, ela se abre e lá dentro, tem ou um valor ou um erro. Esses são os três estados em que um Future pode estar.

Primeiro, a caixa está fechada: chamamos de incompleta. Então a caixa se abre e é completa com um valor ou com um erro.

 

A maior parte do código que vocês verão lida com esses três estados.

Uma das suas funções pega um Future e precisa decidir:

- "O que faço se a caixa ainda não estiver aberta?"

- "O que faço quando abrir depois e tiver um valor?"

- "E se for um erro?"

E assim por diante, você verá muito esse padrão um, dois, três. 

Algo dos Futures que vale a pena saber é que eles são, na verdade, só uma API feita para facilitar o uso do Eventos.

O código Dart que você escreve é executado por uma linha simples.

O tempo todo em que seu app estiver rodando, essa linha fica só circulando, recolhendo eventos da Fila de Eventos e processando-os. Futures operam com o Event Loop para simplificar as coisas.

Digamos que você tem código para um botão de download:

RaisedButton(
onPressed: () {
final myFuture = http.get('https://myimge.url');
myfuture.then((resp) {
setImage(resp);
});
},
child: Text('Clique aqui'),
)

O usuário clica e inicia o download de uma imagem de um cupcake ou algo assim.

Bem, primeiro, o evento de clicar ocorre. O "Event Loop" o recebe e o seu gerenciador de cliques é chamado.

Ele usa a biblioteca http para fazer uma solicitação e recebe um Future como resposta.

 

Agora você tem sua caixinha, certo?

Ela começa fechada, então o seu código usa Then para registrar um retorno para quando abrir.

myfuture.then((resp) {
setImage(resp);
});

Então você espera. Talvez alguns outros eventos entrem.

O usuário faz alguma coisa e a sua caixinha fica ali, enquanto o Event Loop fica circulando.

Por fim, os dados da imagem chegam.

E a biblioteca http diz: ótimo, tenho um Future bem aqui. Ele coloca os dados na caixa e a abre, o que aciona o seu retorno.

Agora, o pedacinho de código setImage executa e exibe a imagem.

myfuture.then((resp) {
setImage(resp);
});

Ao longo de todo o processo, seu código não precisou tocar diretamente o Event Loop. Não se importou com o que mais acontecia, quais outros eventos entravam, tudo o que precisava fazer era obter Future da biblioteca https e então dizer o que ele faria quando o Future completasse.

Se eu fosse um programador melhor, incluiria um código em caso de completar com erro.

 

Como obter uma instância de um Future?

A maior parte do tempo, você provavelmente não vai criar Futures diretamente, porque muitas
das tarefas assíncronas comuns já têm bibliotecas que geram Futures para você.

Como a comunicação em rede, que retorna um Future:

myFuture = http.get('http://example.com');

Acessar preferências compartilhadas retorna um Future:

myFuture = SharedPreferences.getInstance

Mas também tem construtores que você pode usar.

O mais simples é o padrão, que pega uma função e retorna um Future do mesmo tipo.

Então, depois, ele roda a função assincronicamente e usa o valor retornado para completar o Future.

import 'dart:async';

void main() {
final myFuture = Future(() {
return 12;
});
}

Vou incluir aqui algumas sentenças impressas para evidenciar a parte assíncrona.

import 'dart:async';

void main() {
final myFuture = Future(() {
print('Criando o futuro.');
return 12;
});

print('Finalizado com main().');
}

Agora, quando eu rodar isto, você verá como todo o método principal encerra antes da função que eu dei ao construtor de Futures.

Isso porque o construtor de Futures só retorna um Future incompleto no início.

Ele diz: "Aqui está esta caixa". Você a segura por agora, e depois vou rodar sua função e colocar alguns dados nela para você.

Se você já sabe o valor do Future, pode usar o construtor chamado Future.value. Mas o Future ainda completa assincronamente. Usei esse ao construir serviços que usam caching. Às vezes, você já tem o valor de que precisa, então pode só inseri-lo aqui.

final myFuture = Future.value(12);

Future.value também tem um equivalente para completar com um erro, aliás, é chamado de Future.error e funciona basicamente do mesmo jeito, mas pega um objeto de erro e um rastreamento opcional de pilha.

final myFuture = Future.error(Exception());

O construtor que, provavelmente, mais uso é Future.delayed, ele funciona exatamente como o padrão, só que espera por um período específico de tempo antes de rodar a função e completar o Future.

final myFuture = Future.delayed(
Duration(seconds: 5),
() = 12,
);

Eu o uso o tempo todo ao criar serviços simulados de rede para testar.

Para assegurar que meu controle giratório de carregamento apareça de forma correta e depois suma, em algum lugar tem um Future adiado me ajudando.

Certo. Então, é daí que os Futures vêm, sim, mas agora, vamos falar sobre como usá-los.

Como mencionei, trata-se de dar conta dos três estados em que um Future pode estar: incompleto, completo com um valor ou completo com um erro..

Aqui está um Future.delayed criando um futuro que completará três segundos depois com um valor de 100.

import 'dart:async';

void main() {
Future‹int›.delayed(
Duration(seconds: 3),
() { return 100; },
);

print('esperando por um valor.');
}

Agora, quando executo isso, o principal roda de cima para baixo, cria o Future e imprime "esperando por um valor". Todo esse tempo, o Future está incompleto. Não completará por mais três segundos.

Então, se quiser usar esse valor, uso then, um método de instância em cada Future que você pode usar para registrar um retorno para quando o Future completar com um valor.

Você lhe dá uma função, que pega um parâmetro único correspondente ao tipo do Future, e aí, quando o Future completar com um valor, sua função executa com esse valor.

import 'dart:async';

void main() {
Future‹int›.delayed(
Duration(seconds: 3),
() { return 100; },
).then((value) {
print(value);
});

print('esperando por um valor.');
}

Então, se eu rodar isso, ainda recebo primeiro "esperando por um valor" e então, três segundos depois, meu retorno executa e imprime o valor.

Além disso, then retorna um Future próprio, correspondente ao valor de retorno
de qualquer função que você lhe der.

Se você tiver chamadas assíncronas que precisem ser feitas, pode encadeá-las em conjunto, mesmo que tenham diferentes tipos de retorno.

final myFuture = _fetchNameForId(12)
.then((name) = _fetchCountForName(name))
.then((count) = print(count));

 

Mas de volta ao nosso primeiro exemplo, o que acontece se aquele Future inicial não completar com um valor? E se ele completar com um erro?

Then espera por um valor, precisamos de um jeito de registrar outro retorno em caso de erro.

E você poderia fazer isso com catchError que funciona exatamente como Then, só que pega um erro em vez de um valor, e o executa se o Future completa com um erro.

import 'dart:async';

void main() {
Future‹int›.delayed(
Duration(seconds: 3),
() { return 100; },
).then((value) {
print(value);
}).catchError(
(err) {
print('Erro $err');
}
);

print('esperando por um valor.');
}

Assim como Then, ele retorna um futuro próprio e você pode construir uma série inteira de thens e catcherrors e thens e +catcherrors* que esperam um pelo outro.

Pode até lhe dar um método de teste para checar o erro antes de invocar o retorno.

import 'dart:async';

void main() {
Future‹int›.delayed(
Duration(seconds: 3),
() { return 100; },
).then((value) {
print(value);
}).catchError(
(err) {
print('Erro $err');
},
test: (err) = err.runtimeType == String,

);

print('esperando por um valor.');
}

Pode ter múltiplos métodos de detecção de erro assim, cada um deles checando um tipo diferente de erro.

Agora que chegamos aqui, espero que possa ver o que implica dizer que os três estados de um Future, frequentemente, estão refletidos na estrutura do código.

 

whenComplete

 

Mas tenho mais um método para lhe mostrar que é whenComplete.

import 'dart:async';

void main() {
Future‹int›.delayed(
Duration(seconds: 3),
() { return 100; },
).then((value) {
print(value);
}).catchError(
(err) {
print('Erro $err');
},
test: (err) = err.runtimeType == String,
).whenComplete(() {
print('Tudo pronto!');
});


print('esperando por um valor.');
}

Você pode usá-lo para executar um método quando o Future estiver completo, independentemente de ser um valor ou um erro.

É como um bloco finally em um try catch finally.

Tem código sendo executado se tudo der certo, código para um erro e código que roda independente do que aconteça.

É assim que você cria Futures e pode usar seus valores.

 

FutureBuilder

Agora vamos colocá-los para funcionar no Flutter, esta será provavelmente a parte menos complicada deste artigo.

Digamos que tenha um serviço de rede que vai retornar algum JSON e você quer exibir isso.

Você poderia criar um widget stateful, que criará o futuro, checará se completou ou se tem erro, chamará set state e, de um modo geral, lidaria com todo esse esquema manualmente.

Ou você pode usar FutureBuilder que é um widget que vem com o Flutter SDK.

Você lhe dá um Future e um método construtor e ele automaticamente reconstrói os elementos subordinados quando o Future completar.

Faz isso chamando o método builder que pega um contexto e faz um retrato do estado atual do Future.

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
return FutureBuilder‹String›(
future: _fetchNetworkData(),
builder: (context, snapshot) {

},
)
}
}

Você pode checar o retrato para ver se o Future completou com um erro, e então o relata.

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
return FutureBuilder‹String›(
future: _fetchNetworkData(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(
'Há um erro!',
style: Theme.of(context).textTheme.headline,
);
},
},
)
}
}

Ou então, pode checar a propriedade hasData para ver se completou com um valor.

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
return FutureBuilder‹String›(
future: _fetchNetworkData(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(
'Há um erro!',
style: Theme.of(context).textTheme.headline,
);
} else if (snapshot.hasData) {
json.decode(snapshot.data)['delay_was'],
style: Theme.of(context).textTheme.headline,
},

},
)
}
}

Se não, você sabe que ainda está esperando, então pode extrair algo para isso também.

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
return FutureBuilder‹String›(
future: _fetchNetworkData(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(
'Há um erro!',
style: Theme.of(context).textTheme.headline,
);
} else if (snapshot.hasData) {
json.decode(snapshot.data)['delay_was'],
style: Theme.of(context).textTheme.headline,
} else {
return Text(
'Sem valor',
style: Theme.of(context).textTheme.headline,
);
},

},
)
}
}

Até no código do Flutter, dá para ver como esses três estados seguem aparecendo.

Certo. Isso é tudo para este artigo, mas vem mais coisas nesta série...


Este artigo foi feito com base na transcrição do vídeo https://www.youtube.com/watch?v=OTS-ap9_aXc do canal Flutter do Youtube. Artigo transcrico com fins meramente didáticos.

 

157 Visualizações