PDM · DAW 2

Aula 12 · Semana 6 · Quarta, 14 de outubro de 2026 · 16:00 às 18:00

Futures, HTTP e JSON

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.

No fim desta aula consegues

Um valor que ainda não chegou

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}');
Pergunta à turma Enquanto esta função está parada no await, o utilizador consegue fazer scroll na app?

async e await

Future<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();
}

HTTP em 10 minutos (1 de 2): pedido e resposta

Pedido GET /v1/islands/pico/forecast Authorization: Bearer eyJ0eX... Accept: application/json corpo: vazio num GET, JSON num POST Servidor API do curso Resposta HTTP/1.1 200 OK Content-Type: application/json {"location": "Madalena", ...} corpo: Códigos de estado 2xx correu bem 4xx erro do pedido 429 demasiados pedidos 5xx erro do servidor
Pedido: método, URL, headers e corpo opcional. Resposta: código de estado, headers e corpo em JSON.

Para os testes: a idempotência decide o que a app pode repetir sozinha quando a rede falha.

HTTP em 10 minutos (2 de 2): códigos e erros

CódigoSignificaO que a app faz
200, 201, 204Correu bem; 201 criou; 204 não traz corpoUsa os dados
400, 404, 422O pedido está errado ou o recurso não existeBug nosso ou estado vazio; não repetir
401, 403Sem token válido; sem permissão401: pedir token novo; 403: não adianta repetir
429Demasiados pedidosEsperar e repetir mais tarde
5xxErro do servidorRepetir 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 e WebSockets

RESTGraphQLgRPCWebSockets
FormatoJSON sobre HTTP, um URL por recursoUma rota /graphql, o cliente escreve a queryProtobuf binário sobre HTTP/2Ligação aberta nos dois sentidos
Ponto forteSimples, cache HTTP, ferramentas por todo o ladoO cliente pede só os campos que querRápido, contratos geradosO servidor envia quando há novidades
Em mobileQuase tudo; a API do cursoApps com muitos ecrãs (GitHub, Shopify)Serviços internos, apps da GoogleChat, cotações, jogos, mapas ao vivo
CustoPedir dados a mais ou a menosCache difícil, servidor mais complexoDifícil de inspecionarBateria, 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.

A API do curso

App Flutter clientId e clientSecret trocados por um token válido 1 hora HTTPS + Bearer JSON API Tempo Açores autenticação com JWT ilhas, trilhos, relatos da turma fotografias de 30 em 30 min previsão IPMA dados abertos previsão a 5 dias A app nunca chama o IPMA diretamente: um só endereço, um só token, um só formato.
A API do curso fica no meio: autentica, guarda em cache e junta os dados num formato só.
PedidoDevolve
POST /v1/auth/tokenO token, a partir do teu clientId e clientSecret
GET /v1/islandsAs nove ilhas, com a fotografia em imageUrl
GET /v1/islands/{id}/forecastA previsão a 5 dias do IPMA
GET /v1/trails · GET /v1/reportsOs trilhos · os relatos da turma

Endereço: https://api-pdm.sublimecipher.com. Documentação completa na página API; documentação interativa em /docs.

Ler o Swagger (1 de 4): o mapa da API

Topo da página /docs da API Tempo Açores: título, versão 1.0.0, link para /openapi.json, as instruções de autenticação, o botão Authorize e as operações agrupadas em Autenticação e Ilhas e trilhos.

Ler o Swagger (2 de 4): uma operação

Operação GET /v1/islands/{id}/forecast aberta: o parâmetro id, obrigatório, do tipo string, no caminho, com o valor sao-miguel.

Ler o Swagger (3 de 4): o schema

Separador Schema da resposta 200 de GET /v1/islands/{id}/forecast, na parte de cada dia (Day): date string com formato date; weatherType com id integer, description string e category string com o enum sunny, partlyCloudy, cloudy, rain, storm; tMin e tMax number; rainChance integer. Todos os campos com asterisco vermelho.

Ler o Swagger (4 de 4): os erros

Resposta 404, Não existe, com o exemplo de erro: code not_found e message Não há nenhuma ilha com o id atlantida.

Parte 1: toda a gente com token

Faz
  1. Abre https://api-pdm.sublimecipher.com/docs. O teu clientId e o teu clientSecret estão na issue #1 do teu repositório pdm-miniprojeto.
  2. Abre 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.
  3. Sobe ao topo da página, carrega no botão verde Authorize e cola só o token, sem a palavra "Bearer" nem aspas. Os cadeados fecham.
  4. Abre GET /v1/me, Try it out, Execute: tem de aparecer o teu nome. Quando aparecer, mão no ar.
Com o Postman O mesmo endereço; no separador Authorization escolhe Bearer Token e cola o token. Mais rápido: no Swagger, copia o 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ã.

Parte 2: caça ao erro

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ãoPedidoPista
1GET /v1/islandsA descrição da operação diz quantas ilhas há. Conta as que chegam. Qual falta?
2GET /v1/trails?islandId=faialChegam dois trilhos. Um tem um campo a menos. Qual, e o schema diz que é obrigatório?
3GET /v1/trails?islandId=sao-miguelUm campo tem o tipo errado. Compara cada valor com o tipo no Schema.
4GET /v1/islands/pico/forecastUm dia tem um valor que não está na lista Enum do schema.
ExtraGET /v1/reportsSem pistas. Está tudo certo?

Para cada erro, anota três coisas: o pedido, o que o contrato diz e o que chegou.

Pergunta à turma Quem tem de corrigir estes erros: a app ou o backend?

Do JSON ao objeto Dart

A resposta chega como texto. Até ser um objeto que a app pode usar passa sempre pelos mesmos dois passos: descodificar e dar tipos.

String corpo da resposta response.body jsonDecode dart:convert de texto a estrutura Map<String, dynamic> ou List<dynamic> lê-se por chave; nada é verificado Island.fromJson lê cada campo com o tipo e falha logo se não bater Island objeto com tipos o editor completa List<Island> .map(...).toList() objetos, nunca texto widgets ListView
jsonDecode passa de texto a uma estrutura sem tipos; fromJson passa a objetos com tipos. Daqui para a frente a app só vê objetos.

Uma ilha: do JSON ao 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,
  );
}

JSON com objetos dentro de objetos

{
  "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"
  }
}

Um dia: DayForecast.fromJson

factory 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,
  );
}

Onde o parsing parte

A API mandaO código esperaO que aconteceComo se escreve
"tMin": 20as doubleErro: 20 é int, não double(json['tMin'] as num).toDouble()
campo em falta ou nullas StringErro: null não é Stringas String? e um valor por omissão
"category": "fog"values.byNameErro: valor que o enum não temProcurar e usar um valor por omissão
"date": "14/10/2026"DateTime.parseFormatExceptionCombinar o formato com a API (ISO 8601)
uma listaas MapErro de tipoVer a resposta real no /docs antes de escrever
Pergunta à turma Se a API mandar "category": "fog" num só dia, o que vê o utilizador no ecrã da previsão?

O pedido completo, com erros

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();
}

O token e o segredo

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

FutureBuilder: o ecrã que espera

class _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 interessa

Pergunta à turma O build de um ecrã corre quantas vezes? E quantos pedidos fazia o servidor se o Future fosse criado lá dentro?

E se os dados não param de chegar?

Future<T>Stream<T>
Quantos valoresUm, uma vezMuitos, ao longo do tempo
Como se esperaawaitawait for ou listen
WidgetFutureBuilderStreamBuilder
ExemplosA previsão de hoje, o token, uma fotografiaA posição do GPS, mensagens de chat, cotações, um autocarro em movimento

Até segunda

Próxima aula

  • Segunda, 19 de outubro, das 16:00 às 18:00.
  • Arquitetura por camadas: tirar os pedidos HTTP de dentro dos ecrãs.

Para quem quiser ler mais