Objeto: criação de um pequeno app com gerência de estado, comunicação entre componentes gráficos e acesso a api externa para carregar dados.
Principais termos técnicos abordados: programação assíncrona, async, await, Future, api, end-point
Requisitos para as instruções funcionarem: navegador ou ide (VS Code, Android Studio etc.)
Requisitos para compreensão das instruções: noções básicas de programação e ter realizado receita anterior, com exercícios
Como ler essa receita: instruções, dentro dos passos da receita e que requerem ação sua no computador, estarão escritas em cor azul. Comentários sobre os passos estarão em fonte normal, cor preta. Comandos, código-fonte, termos técnicos ou configuração explícita, estarão com fonte diferenciada.
Observação: eventuais instruções de terminal serão comandos Linux. Adapte caso esteja usando outro sistema operacional
- Na verdade, Major, Padre João não chamou a sua filha de cachorra.
- Chamou, Seu Bispo...
- Não chamou, Major...
- Chamou, Seu Bispo...
- Não chamou, Major...
- CHAMOU, SEU BISPO!!!
- Chamou, Major.
- Peraí, chamou ou não chamou, Seu Bispo???
- Chamou. E não chamou. O senhor estava vindo encomendar uma bênção pra sua filha, e o Padre João pensou que fosse uma cachorra.
(Trecho do Auto da Compadecida)
Bem, crianças, chegamos ao ponto em que não há mais desculpas para não irmos buscar em algum lugar mais adequado os dados que servem à contrução de nossa interface com o usuário.
Porque até agora estamos simplesmente inventando esses dados, eles estão hardcoded em alguma parte do código-fonte.
E qual o lugar ideal para irmos buscar esses dados?
No nosso caso, a gente vai buscar esses dados numa API publicada na internet. A gente vai fazer consultas a ela e ela vai nos dar de volta textos no formato JSON.
E a gente já sabe manipular objetos JSON para construir interfaces gráficas.
E a gente já sabe por componentes para se redesenharem em mudanças de estado.
Basta-nos, essencialmente, fechar o cerco. Fazer uma chamada a uma API e fazer com que a chegada da resposta a essa chamada acarrete uma mudança de estado.
Os componentes que estiverem conectados com esse estado vão ser redesenhados com os dados atualizados.
Assim, nosso conhecimento novo é "apenas" o de saber fazer essa chamada a uma API e saber processar sua resposta, e não botei o apenas entre aspas por acaso.
Botei porque isso envolve uma tal programação assíncrona, que não é lá a coisa mais intuitiva do mundo.
Mas, se você for chamar uma base de dados local ou na internet, você vai precisar de programação assíncrona. Se você for ler um arquivo local para obter seus dados, você vai precisar de programação assíncrona. E se você for chamar uma API, claro, também precisará.
- E por quê?
Porque em todos esses casos o seu programa vai chegar a um ponto em que a resposta a um comando não é imediata. A gente não sabe quanto tempo vai levar para a API dar a resposta, essa requisição sequer é processada no dispositivo que roda nosso app. Esse ponto em que alguma espera é necessária é o que a gente chama de end point - uma API, uma chamada a uma API, é um end point.
Sendo que nosso app não pode ficar esperando o bom humor do end point em disponibilizar sua resposta a nossa requisição. Para ficar apenas num problema, isso travaria o app (interações do usuário, inclusive), em especial em linguagens cujo código normalmente roda numa thread só, como dart e JS.
Cole o código inicial. Ou faça um fork nesse projeto.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class DataService{
final ValueNotifier<List> tableStateNotifier = new ValueNotifier([]);
void carregar(index){
if (index == 1) carregarCervejas();
}
void carregarCervejas(){
tableStateNotifier.value = [{
"name": "La Fin Du Monde",
"style": "Bock",
"ibu": "65"
},
{
"name": "Sapporo Premiume",
"style": "Sour Ale",
"ibu": "54"
},
{
"name": "Duvel",
"style": "Pilsner",
"ibu": "82"
}
];
}
}
final dataService = DataService();
void main() {
MyApp app = MyApp();
runApp(app);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepPurple),
debugShowCheckedModeBanner:false,
home: Scaffold(
appBar: AppBar(
title: const Text("Dicas"),
),
body: ValueListenableBuilder(
valueListenable: dataService.tableStateNotifier,
builder:(_, value, __){
return DataTableWidget(
jsonObjects:value,
propertyNames: ["name","style","ibu"],
columnNames: ["Nome", "Estilo", "IBU"]
);
}
),
bottomNavigationBar: NewNavBar(itemSelectedCallback: dataService.carregar),
));
}
}
class NewNavBar extends HookWidget {
final _itemSelectedCallback;
NewNavBar({itemSelectedCallback}):
_itemSelectedCallback = itemSelectedCallback ?? (int){}
@override
Widget build(BuildContext context) {
var state = useState(1);
return BottomNavigationBar(
onTap: (index){
state.value = index;
_itemSelectedCallback(index);
},
currentIndex: state.value,
items: const [
BottomNavigationBarItem(
label: "Cafés",
icon: Icon(Icons.coffee_outlined),
),
BottomNavigationBarItem(
label: "Cervejas", icon: Icon(Icons.local_drink_outlined)),
BottomNavigationBarItem(
label: "Nações", icon: Icon(Icons.flag_outlined))
]);
}
}
class DataTableWidget extends StatelessWidget {
final List jsonObjects;
final List<String> columnNames;
final List<String> propertyNames;
DataTableWidget( {this.jsonObjects = const [], this.columnNames = const ["Nome","Estilo","IBU"], this.propertyNames= const ["name", "style", "ibu"]});
@override
Widget build(BuildContext context) {
return DataTable(
columns: columnNames.map(
(name) => DataColumn(
label: Expanded(
child: Text(name, style: TextStyle(fontStyle: FontStyle.italic))
)
)
).toList()
,
rows: jsonObjects.map(
(obj) => DataRow(
cells: propertyNames.map(
(propName) => DataCell(Text(obj[propName]))
).toList()
)
).toList());
}
}
Instale o pacote http, acrescentando a seguinte linha ao pubspec do seu projeto.
dependencies:
http: ^0.13.6
Você pode verificar aqui a versão mais recente do pacote.
Alternativamente, você pode ir no terminal e escrever.
flutter pub add http
Isso deve baixar o pacote e acrescentar a dependência no lugar correto, caso você não se sinta seguro para fazer isso sozinho.
Agora, importe o pacote http no main.dart. Para tal, acrescente a seguinte linha ao início do arquivo.
import 'package:http/http.dart' as http;
Bem, essa criança vai ajudar a gente e ir buscar dados na internet, a chamar API's. Esse as http indica que todas as funções e objetos dentro do pacote serão encapsuladas num objeto identificado por http. Se houver uma função f solta pelo pacote, a gente deve chamá-la usando a expressão http.f(), por exemplo. Se, igualmente, houver um objeto x solto pelo pacote, podemos acessá-lo pela expressão http.x.
Parece um pouco lógico ir ali no método carregarCervejas e, em vez de empurrar uma lista hardcoded no estado do app, ir buscar essa lista na internet com a ajuda do pacote http.
E é isso mesmo.
Primeiramente, vamos saber onde esses dados estão.
Acesse esse link aqui no seu navegador: https://random-data-api.com/api/beer/random_beer?size=5.
A resposta é algo assim e aparecerá no seu navegador...
[
{
"id": 3084,
"uid": "6ea4c4b6-3cd3-4f82-8a35-6ae56f9237f2",
"brand": "Murphys",
"name": "St. Bernardus Abt 12",
"style": "Merican Ale",
"hop": "Galena",
"yeast": "2007 - Pilsen Lager",
"malts": "Carapils",
"ibu": "97 IBU",
"alcohol": "6.2%",
"blg": "9.9°Blg"
},
{
"id": 1501,
"uid": "a4eee5a3-686c-4c12-abd1-fbdb34463388",
"brand": "Patagonia",
"name": "Founders Breakfast Stout",
"style": "Dark Lager",
"hop": "TriplePearl",
"yeast": "1028 - London Ale",
"malts": "Rye malt",
"ibu": "78 IBU",
"alcohol": "8.2%",
"blg": "19.3°Blg"
},
{
"id": 874,
"uid": "4f1591b4-0225-4c30-9b19-c98ee2186faf",
"brand": "Stella Artois",
"name": "Hercules Double IPA",
"style": "Belgian And French Ale",
"hop": "Vanguard",
"yeast": "2007 - Pilsen Lager",
"malts": "Munich",
"ibu": "29 IBU",
"alcohol": "8.6%",
"blg": "16.7°Blg"
},
{
"id": 1211,
"uid": "892da7ed-db2f-4401-90ef-1c0521821b80",
"brand": "Murphys",
"name": "Kirin Inchiban",
"style": "Bock",
"hop": "Eroica",
"yeast": "2308 - Munich Lager",
"malts": "Special roast",
"ibu": "27 IBU",
"alcohol": "8.7%",
"blg": "18.1°Blg"
},
{
"id": 1745,
"uid": "598a5383-3ea9-4674-8df3-883f8d4cff5c",
"brand": "Hoegaarden",
"name": "Hercules Double IPA",
"style": "Pilsner",
"hop": "Cluster",
"yeast": "2124 - Bohemian Lager",
"malts": "Munich",
"ibu": "16 IBU",
"alcohol": "6.0%",
"blg": "6.2°Blg"
}
]
- Vige, é uma lista de objetos JSON, professor, parecido com o que a gente tá usando como estado do app...
Calma, é quase isso. É um texto que segue a sintaxe JSON. É um String, afinal de contas. Mas se segue a sintaxe de uma lista de objetos JSON, a conversão não há de ser complicada. E convertendo isso numa lista de objetos JSON é só por no estado do app que nossos componentes já estão preparados pra receber atualizações do estado e serem redesenhados.
Para fazer essa conversão, a gente precisa importar o pacote convert. Ele está na distribuição padrão do dart, não precisa nem instalar, basta importar.
Edite o código e acrescente o import.
import 'dart:convert';
Agora vamos primeiro aprender a fazer, pelo nosso código, essa requisição que a gente fez pelo navegador.
Dê uma sacada na requisição novamente.
https://random-data-api.com/api/beer/random_beer?size=5
Ela é o que a gente chama de URI, Universal Resource Identifier, e tem várias partes, e eu vou chamá-las todas pelos nomes em inglês pois é assim que aparecerão no código.
o https é o scheme (ou protocol), representa regras de comunicação entre as partes envolvidas na requisição - seu app e um servidor lá no oco do mundo
o random-data-api.com é o host - essencialmente, isso é a identificação de um computador na Internet, como um endereço IP mais legível pra gente
há um dado implícito aí, que é uma porta, tradução provavelmente abestalhada do inglês port (porto, mais frequentemente). O port é um número inteiro que representa um programa no computador identificado pelo host. Quando a requisição chega ao host, ela é encaminhada para o programa associado ao port. Quando a gente não identifica o port, a requisição é encaminhada para o port padrão do protocolo. Todos esses protocolos de redes bem estabelecidos (http, https, ftp etc.) têm um port padrão, você normalmente não se preocupa em especificar isso na Uri.
o api/beer/random_beer é o path, o endereço de um recurso (arquivo, programa etc.) dentro de um programa identificado pelo port que roda no computador identificado pelo host e que sabe conversar segundo as regras do protocol.
tudo o mais depois do sinal ? é parâmetro para o recurso que, no nosso caso, é uma função, então nada mais natural. Assim, o size é um query parameter e o 5 é o valor do query parameter identificado por size.
Todas essas crianças, juntas, representam os dados de uma requisição a uma API, e eu só tive o cuidado de explicá-las porque vocês precisam estar cientes ao menos desses dados para acessar qualquer API.
Agora vamos ao código.
Modifique sua função carregarCervejas, lá na classe DataService.
void carregarCervejas(){
//1
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '5'});
//2
var jsonString = http.read(beersUri);
//3
var beersJson = jsonDecode(jsonString);
//4
tableStateNotifier.value = beersJson;
}
O algoritmo implementado na função carregarCervejas não poderia ser mais simples. A gente...
encapsula os dados da requisição num objeto Uri
faz a requisição com o método http.read
converte o resultado da requisição
empurra o resultado da requisição do estado do app
É, o algoritmo não poderia ser mais simples...
Sendo que não dá certo.
É um algoritmo com uma chamada a uma função assíncrona, a https.read.
- Opa, e o que é uma função assíncrona mesmo?
É uma função que retorna, mas não retorna.
- Djabo é, homi...
Bem, não é uma definição que você encontre na Internet, mas é uma forma de entender as coisas.
A função http.read vai fazer algum processamento para enviar a requisição. Depois ela tem que esperar ociosamente, sem computar nada, porque ela não pode continuar processando nada até que uma resposta chegue do servidor para o qual ela fez a requisição. Então, no primeiro momento em que ela se depara com um momento de espera, ela simplesmente retorna.
- Mas retorna o que, homi, se ela precisa esperar os dados chegarem da Internet???
É por isso que eu disse que ela retorna mas não retorna. "Retorna" porque no exato momento em que entra em espera nos dá de volta um objeto.
E "não retorna" porque esse objeto não traz dentro de si o resultado desejado, justamente porque a função não tem como dar o resultado completo dela naquele momento. Esse objeto é um objeto que, em algum ponto futuro, terá acesso ao resultado final da computação de toda a função https.read. No mundo dart, a gente chama esse objeto de Future. Em JavaScript, esse tipo de objeto se chama Promisse.
Mas como quer que se chame o fato é que isso meio que acanalha a nossa brincadeira, o nosso algoritmo.
Porque se você chamar...
var jsonString = http.read(beersUri)
... isso vai te retornar um objeto que não tem ainda os dados da resposta da requisição. Mas sua função carregarCervejas continua rodando sem esperar nem por Jesus. Daí quando você chamar...
var beersJson = jsonDecode(jsonString);
... você vai estar passando pra função jsonDecode um objeto que nem String é e nem tem ainda dentro de si o resultado final da requisição feita por http.read.
Há diversos jeitos de contornar isso e o mais fácil para quem está entrando agora no assunto é não lidar com esses objetos Future.
A gente pode fazer isso indicando que a nossa querida função carregarCervejas não deve "passar direto" e continuar executando após a chamada a http.read.
A gente pode dizer que para a função carregarCervejas ficar esperando o resultado final do processamento de http.read. Bem, a gente pode tentar.
Modifique sua função carregarCervejas.
void carregarCervejas(){
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '5'});
var jsonString = await http.read(beersUri);
var beersJson = jsonDecode(jsonString);
tableStateNotifier.value = beersJson;
}
- Ah tá... só esse await? Isso faz a função carregarCervejas esperar?
Faz.
E não faz.
- Ai, ai...
Vamos primeiro ao "faz", porque ele faz duas coisas.
Faz que que a atribuição de...
var jsonString = await http.read(beersUri);
... só aconteça quando o resultado final da função estiver pronto.
E, melhor ainda, nos livra do tal objeto Future. Atribui direto o resultado da requisição.
- Aquele stringão descrevendo uma lista de objetos JSON.
Isso! Aquele stringão descrevendo uma lista de objetos JSON.
Note que não usando o await a chamada retorna um tipo de objeto, o Future. Usando, ela retorna outro tipo de objeto - nesse caso específico, um String.
- Tô ligado. Mas e a parte do "não faz"?
É que não se pode nem se deve, na prática, existir espera ociosa em ambientes monothread de execução, isso trava seu app. Você só tem uma linha de execução, se fizer um loop muito grande ou essa linha de execução ficar esperando sem passar o controle para algum código que pode ser executado, o app trava. E o dart é eminentemente monothread (há casos excepcionais), assim como o Node JS.
Essa é, essencialmente, a tal programação assíncrona: retornar no ponto antes de ficar ocioso e agendar a execução do restante do código para quando estiver em condições de processar, para quando um evento ocorrer - esse evento pode ser a chegada dos dados de uma chamada a uma API, por exemplo.
- Então como a gente faz com nossa função carregarCervejas, se essa http.read que ela chama retorna mas não retorna e a gente não pode executar a linha seguinte imediatamente?
A gente faz exatamente como o cara que implementou a função http.read fez. Como ele não podia fugir da espera, transformou o http.read numa função que retorna mas não retorna. Como a gente que tá escrevendo a função carregarCervejas não tem como fugir da espera, a gente pode, igualmente, transformar essa função numa função que retorna mas não retorna no primeiro ponto de espera ociosa. A gente pode transformar essa função numa função assíncrona.
Modifique a função.
void carregarCervejas() async{
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '5'});
var jsonString = await http.read(beersUri);
var beersJson = jsonDecode(jsonString);
tableStateNotifier.value = beersJson;
}
- Só isso, professor? É só meter async ali na declaração da função?
É. Basicamente, é só isso.
- Tem mais nada?
Não.
- Mas sempre tem. O senhor vive dizendo que sempre tem.
É que sempre tem.
Mas nem sempre.
Botar aquele async ali significa que sua função carregarCervejas() deveria retornar um objeto da classe Future assim que encontrasse o primeiro await dentro dela - o primeiro momento em que é necessário esperar ociosamente. Sendo que, do jeito que a gente programou, ela simplesmente retorna void mesmo, você nem precisaria nem saber o que é Future, por enquanto.
Se a gente quiser ser rigoroso, para a função carregar retornar um objeto Future em vez de nada, a gente pode reescerver a função assim.
Future<void> carregarCervejas() async{
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '5'});
var jsonString = await http.read(beersUri);
var beersJson = jsonDecode(jsonString);
tableStateNotifier.value = beersJson;
}
- Eu sabia que tinha mais alguma coisa. Sempre tem.
Sempre.
Se a função originalmente retornasse um inteiro a gente escreveria Future<int>, que indica que a função retorna um objeto Future que em algum momento, bem, do futuro, vai ter acesso ao int resultante da função. Como a nossa função originalmente retorna void, esse Future vai ter acesso a nada dentro de si, basicamente.
Mas note que, mesmo assim, a gente não está realmente usando o tal Future. Nem return a gente tá escrevendo. Ele está sendo uma mera formalidade na declaração da função.
É que a gente usa async/await justamente quando não quer ter que lidar com objetos Future.
Mas é bom entender, em todo caso, como se dá a execução de sua função. Vou reescrever a função carregarCervejas e a função que a invoca, a função carregar.
Reescreva.
void carregar(index){
var res = null;
print('carregar #1 - antes de carregarCervejas');
if (index == 1) res = carregarCervejas();
print('carregar #2 - carregarCervejas retornou $res');
}
Future<void> carregarCervejas() async{
var beersUri = Uri(
scheme: 'https',
host: 'random-data-api.com',
path: 'api/beer/random_beer',
queryParameters: {'size': '5'});
print('carregarCervejas #1 - antes do await');
var jsonString = await http.read(beersUri);
print('carregarCervejas #2 - depois do await');
var beersJson = jsonDecode(jsonString);
tableStateNotifier.value = beersJson;
}
Agora execute seu app e toque no botão Cervejas.
Além de ver a mágica acontecendo, uma ruma de dados de cervejas aleatórias na sua tabela, repare na saída desses prints (deve ter alguma tela de logs ou output no ambiente de programação que você está usando). Vou escrever a sequência de prints e comentá-la:
carregar #1 - antes de carregarCervejas - chegou a chamada à função carregar, chamada feita pela por um componente da interface gráfica, estamos antes da chamada a nossa função assíncrona carregarCervejas
carregarCervejas #1 - antes do await - a função assíncrona começa a executar imediatamente, até chegar em algum ponto de espera ociosa
carregar #2 - carregarCervejas retornou Instance of '_Future<void>' - ao deparar-se com um ponto de espera, a função assíncrona retorna um objeto Future para quem a chamou, que foi a função carregar. A função carregar segue executando até retornar ao componente gráfico que a chamou, e este encerra essa sequência de chamadas.
carregarCervejas #2 - depois do await - em algum lugar do futuro a função http.read retorna e o função carregarCervejas pode finalmente executar o resto dela própria. Note que o resto da função carregarCervejas não retorna mais para canto nenhum após a execução da última lima, porque a função carregarCervejas já retornou antes. Se "retornasse" novamente, você veria mais prints. Mas o fato é que esses pedaços de código agendados para o futuro ficam numa espécie de limbo, não retornam pra quem chamou como nas fuções convencionais.
- Professor, e a gente não tem que botar await antes da chamada à função carregarCervejas não?
Não, porque a gente não precisa esperar por nada. A gente quer que a função carregarCervejas faça seu serviço e gere um efeito colateral (mudança de estado) que, por sua vez, vai gerar o redesenho dos componentes ligados ao estado modificado. Assim, a gente não precisa esperar nada, o ideal é que a gente retorne logo. Lembre que carregar é uma função de callback chamada por um componente gráfico. Não queremos deixar componentes gráficos aguardando nada. Isso casa perfeitamente com o modelo descritivo de programação que estamos adotando.
É fácil confundir programação assíncrona com programação paralela. Mas não houve absolutamente nenhum paralelismo nesse código em absolutamente nenhum ponto. Programação assíncrona tem mais a ver com ocupar todo seu tempo com alguma coisa pra fazer e não com fazer várias coisas ao mesmo tempo. É como se você tivesse que esperar um amigo que sabe mais do assunto que você para estudar, então você vai ficar vendo fuleiragem no YouTube já com o compromisso agendado na sua cabeça oca de, quando o evento do seu amigo chegar acontecer, você ir estudar. Sendo que, quando seu amigo chega, você não vai estudar imediatamente. Você acaba de ver o vídeo que está vendo e, quando não tem mais nada pra fazer, vai, finalmente, estudar.
Ou seja, você deu um await no seu amigo mas não ficou parado até que ele chegasse.
Você processou, executou as atividades de ver fuleiragem no YouTube e estudar assincronamente, e não paralelamente.
Foi basicamente isso que aconteceu com seu app e é, basicamente, o que vai acontecer em todos os seus apps, então precisamos ter isso em mente pra programar direito.
Como você, que só tem um juízo, o app só tem uma linha de execução para processar seu código.
Programação assíncrona - documentação oficial, vários exemplos de uso, mas sem tanta emoção.
A classe Future - claro que vamos aprender a mexer com objetos Future. Pode ir se adiantando.
Aqui está como carregar dados de cafés e aqui está como carregar dados de nações. Faça o que você tem que fazer.
Que tal permitir que números diferentes de itens sejam carregados? Ponha uma caixa de seleção (em algum lugar de sua interface) com 3 opções (5, 10 e 15). Caso a opção selecionada esteja em 10 e o usuário toque em Cafés, 10 cafés devem ser exibidos na tabela. Não use números muito grandes (sugiro que fique com 5, 10 e 15) e não abuse da "boa vontade" da API (tocando várias vezes sequidas pra gerar várias requisições, por exemplo), pois o site que a hospeda pode bloquear suas requisições.