Aula 12 · Semana 6 · Quarta, 14 de outubro de 2026 · 16:00 às 18:00
Como uma app vai buscar dados a um servidor sem congelar: Future e async/await, pedidos HTTP, JSON convertido em objetos Dart e o FutureBuilder. Pelo meio, lemos juntos a documentação Swagger da API do curso e experimentamos pedidos no browser.
Future e porque um pedido à rede nunca pode bloquear a app.fromJson e saber onde o parsing costuma partir.FutureBuilder: a carregar, erro e dados.Future (um valor) de uma Stream (muitos valores ao longo do tempo).http.get não devolve a resposta: devolve um Future<Response>, uma promessa de que a resposta vem mais tarde.Future está num de três estados: por completar, completo com um valor ou completo com um erro.print('1. antes do pedido');
final future = http.get(url); // o pedido parte, a app continua
print('2. o pedido já partiu');
final response = await future; // aqui a função para até chegar a resposta
print('3. chegou: ${response.statusCode}');
await, o utilizador consegue fazer scroll na app?async e awaitFuture<List<Island>> fetchIslands(String token) async {
final response = await http.get(
Uri.parse('$baseUrl/v1/islands'),
headers: {'Authorization': 'Bearer $token'},
);
final list = jsonDecode(response.body) as List<dynamic>;
return list
.map((item) => Island.fromJson(item as Map<String, dynamic>))
.toList();
}
async marca a função: ela devolve sempre um Future, mesmo que o return devolva uma lista.await só se usa dentro de uma função async. Lê-se de cima para baixo como código normal.Future com erro. Apanha-se com try/catch à volta do await.await é o erro mais comum: ficas com um Future na mão em vez da resposta.GET lê e nunca altera. POST cria. PUT substitui. DELETE apaga.GET, PUT e DELETE são; POST não é. Um GET que falhou pode repetir-se; um POST repetido pode criar dois relatos, ou cobrar duas vezes.Para os testes: a idempotência decide o que a app pode repetir sozinha quando a rede falha.
| Código | Significa | O que a app faz |
|---|---|---|
| 200, 201, 204 | Correu bem; 201 criou; 204 não traz corpo | Usa os dados |
| 400, 404, 422 | O pedido está errado ou o recurso não existe | Bug nosso ou estado vazio; não repetir |
| 401, 403 | Sem token válido; sem permissão | 401: pedir token novo; 403: não adianta repetir |
| 429 | Demasiados pedidos | Esperar e repetir mais tarde |
| 5xx | Erro do servidor | Repetir mais tarde, mostrar dados antigos |
Na API do curso, quando algo corre mal o corpo explica o erro em português:
{
"error": {
"code": "not_found",
"message": "Não há nenhuma ilha com o id \"atlantida\"."
}
}
Para os testes: 4xx é culpa do pedido, 5xx é culpa do servidor, 429 é um limite.
| REST | GraphQL | gRPC | WebSockets | |
|---|---|---|---|---|
| Formato | JSON sobre HTTP, um URL por recurso | Uma rota /graphql, o cliente escreve a query | Protobuf binário sobre HTTP/2 | Ligação aberta nos dois sentidos |
| Ponto forte | Simples, cache HTTP, ferramentas por todo o lado | O cliente pede só os campos que quer | Rápido, contratos gerados | O servidor envia quando há novidades |
| Em mobile | Quase tudo; a API do curso | Apps com muitos ecrãs (GitHub, Shopify) | Serviços internos, apps da Google | Chat, cotações, jogos, mapas ao vivo |
| Custo | Pedir dados a mais ou a menos | Cache difícil, servidor mais complexo | Difícil de inspecionar | Bateria, ligação que cai e volta |
Para os testes: REST é o normal em mobile; GraphQL evita pedir dados a mais; gRPC é binário e rápido; WebSockets são para tempo real.
| Pedido | Devolve |
|---|---|
POST /v1/auth/token | O token, a partir do teu clientId e clientSecret |
GET /v1/islands | As nove ilhas, com a fotografia em imageUrl |
GET /v1/islands/{id}/forecast | A previsão a 5 dias do IPMA |
GET /v1/trails · GET /v1/reports | Os trilhos · os relatos da turma |
Endereço: https://api-pdm.sublimecipher.com. Documentação completa na página API; documentação interativa em /docs.

/openapi.json): um ficheiro que descreve cada rota, os parâmetros, os dados e os erros. É a fonte de verdade.GET /v1/islands.
id é obrigatório (* required), é texto e vai no caminho (path): /v1/islands/sao-miguel/forecast. Outros vão na query: /v1/trails?islandId=faial.curl, o URL, o código e o corpo da resposta. Se o curl não tiver a linha Authorization: Bearer, faltou o Authorize.
* vermelho quer dizer que o campo vem sempre.string, integer, number, objetos dentro de objetos ({ }) e listas ([Day]). Enum é a lista fechada de valores: dá um enum em Dart.
/v1/islands/{id}/forecast são 200, 401, 404 e 502.code para o programa decidir (not_found, unauthorized...), message para mostrar à pessoa.https://api-pdm.sublimecipher.com/docs. O teu clientId e o teu clientSecret estão na issue #1 do teu repositório pdm-miniprojeto.POST /v1/auth/token, carrega em Try it out, cola os dois no pedido e carrega em Execute. Tem de dar 200 e um accessToken.GET /v1/me, Try it out, Execute: tem de aparecer o teu nome. Quando aparecer, mão no ar.curl que aparece depois do Execute e, no Postman, carrega em Import e cola-o.Ninguém passa à parte 2 sem o nome no ecrã.
A API tem agora erros de propósito: respostas que não cumprem o contrato do Swagger. Cada missão diz onde procurar; o erro encontras tu.
| Missão | Pedido | Pista |
|---|---|---|
| 1 | GET /v1/islands | A descrição da operação diz quantas ilhas há. Conta as que chegam. Qual falta? |
| 2 | GET /v1/trails?islandId=faial | Chegam dois trilhos. Um tem um campo a menos. Qual, e o schema diz que é obrigatório? |
| 3 | GET /v1/trails?islandId=sao-miguel | Um campo tem o tipo errado. Compara cada valor com o tipo no Schema. |
| 4 | GET /v1/islands/pico/forecast | Um dia tem um valor que não está na lista Enum do schema. |
| Extra | GET /v1/reports | Sem pistas. Está tudo certo? |
Para cada erro, anota três coisas: o pedido, o que o contrato diz e o que chegou.
A resposta chega como texto. Até ser um objeto que a app pode usar passa sempre pelos mesmos dois passos: descodificar e dar tipos.
jsonDecode passa de texto a uma estrutura sem tipos; fromJson passa a objetos com tipos. Daqui para a frente a app só vê objetos.Island{
"id": "sao-miguel",
"name": "São Miguel",
"imageUrl": "https://api-pdm.sublimecipher.com/static/islands/sao_miguel.jpg",
"trailCount": 3
}
factory Island.fromJson(Map<String, dynamic> json) {
return Island(
id: json['id'] as String,
name: json['name'] as String,
imageUrl: json['imageUrl'] as String,
);
}
{
"date": "2026-10-14",
"weatherType": {
"id": 7,
"description": "Aguaceiros/chuva fracos",
"category": "rain"
},
"tMin": 20.0,
"tMax": 24.2,
"rainChance": 57,
"wind": {
"direction": "SW",
"class": 2,
"description": "Moderado"
}
}
Map; uma lista é uma List<dynamic> que se percorre com map.fromJson converte (DateTime.parse, values.byName).DayForecast.fromJsonfactory DayForecast.fromJson(Map<String, dynamic> json) {
final weather = json['weatherType'] as Map<String, dynamic>;
final wind = json['wind'] as Map<String, dynamic>;
return DayForecast(
date: DateTime.parse(json['date'] as String),
type: WeatherType.values.byName(weather['category'] as String),
tMin: (json['tMin'] as num).toDouble(),
tMax: (json['tMax'] as num).toDouble(),
rainChance: json['rainChance'] as int,
windClass: wind['class'] as int,
);
}
windKmh das aulas passa a windClass.| A API manda | O código espera | O que acontece | Como se escreve |
|---|---|---|---|
"tMin": 20 | as double | Erro: 20 é int, não double | (json['tMin'] as num).toDouble() |
campo em falta ou null | as String | Erro: null não é String | as String? e um valor por omissão |
"category": "fog" | values.byName | Erro: valor que o enum não tem | Procurar e usar um valor por omissão |
"date": "14/10/2026" | DateTime.parse | FormatException | Combinar o formato com a API (ISO 8601) |
| uma lista | as Map | Erro de tipo | Ver a resposta real no /docs antes de escrever |
fromJson completa o Future com erro: o ecrã mostra a mensagem de erro em vez dos dados, mas a app não fecha./openapi.json) diz o que pode faltar. O que não está no contrato, não contes com isso."category": "fog" num só dia, o que vê o utilizador no ecrã da previsão?void _check(http.Response response) {
if (response.statusCode >= 200 && response.statusCode < 300) return;
final body = jsonDecode(response.body) as Map<String, dynamic>;
final error = body['error'] as Map<String, dynamic>;
throw ApiException(response.statusCode, error['message'] as String);
}
Future<List<Island>> fetchIslands(String token) async {
final response = await http
.get(
Uri.parse('$baseUrl/v1/islands'),
headers: {'Authorization': 'Bearer $token'},
)
.timeout(const Duration(seconds: 10));
_check(response);
final list = jsonDecode(response.body) as List<dynamic>;
return list
.map((item) => Island.fromJson(item as Map<String, dynamic>))
.toList();
}
http não lança erro num 404 ou num 500: a resposta chega na mesma. Quem verifica o statusCode és tu.timeout, um pedido num túnel pode ficar pendurado minutos. Com ele, ao fim de 10 s o Future completa com erro.Future<String> fetchToken(String clientId, String clientSecret) async {
final response = await http.post(
Uri.parse('$baseUrl/v1/auth/token'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'clientId': clientId, 'clientSecret': clientSecret}),
);
_check(response);
final json = jsonDecode(response.body) as Map<String, dynamic>;
return json['accessToken'] as String;
}
const clientId = String.fromEnvironment('CLIENT_ID');
const clientSecret = String.fromEnvironment('CLIENT_SECRET');
flutter run -d chrome \
--dart-define=CLIENT_ID=pdm-ab12cd \
--dart-define=CLIENT_SECRET=o-teu-segredo
POST o corpo vai em JSON: jsonEncode faz o contrário do jsonDecode.--dart-define passa-o na hora de correr.FutureBuilder: o ecrã que esperaclass _IslandsScreenState extends State<IslandsScreen> {
late final Future<List<Island>> _islands;
@override
void initState() {
super.initState();
_islands = fetchIslands(widget.token); // once, not in build
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Island>>(
future: _islands,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}'));
}
final islands = snapshot.data!;
return ListView.builder(
itemCount: islands.length,
itemBuilder: (context, i) => ListTile(
leading: Image.network(islands[i].imageUrl, width: 56),
title: Text(islands[i].name),
),
);
},
);
}
}
FutureBuilder: o que interessabuilder corre de novo cada vez que o Future muda de estado. O snapshot diz em que estado está.connectionState), erro (hasError, error) e dados (data). Os três estados do Future, desenhados.Future cria-se uma vez, no initState. Se escreveres future: fetchIslands(token) dentro do build, cada rebuild faz um pedido novo.Image.network em vez de Image.asset: a fotografia também vem da rede, e o Flutter guarda-a em cache.Future novo dentro de setState.build de um ecrã corre quantas vezes? E quantos pedidos fazia o servidor se o Future fosse criado lá dentro?Future<T> | Stream<T> | |
|---|---|---|
| Quantos valores | Um, uma vez | Muitos, ao longo do tempo |
| Como se espera | await | await for ou listen |
| Widget | FutureBuilder | StreamBuilder |
| Exemplos | A previsão de hoje, o token, uma fotografia | A posição do GPS, mensagens de chat, cotações, um autocarro em movimento |
Future é uma encomenda: chega uma vez. Uma Stream é uma assinatura: chega sempre que há uma edição nova, até cancelares.StreamBuilder tem o mesmo snapshot do FutureBuilder, mas o builder corre de novo a cada valor novo.http com FutureBuilder, passo a passo.Future.