PDM · DAW 2

Aula 04 · Semana 3 · Sexta, 25 de setembro de 2026 · 08:30 às 10:30

Ciclo de vida, assincronia e Dart II

Teoria: ciclo de vida de uma app móvel, com os estados impostos pelo sistema operativo, e o modelo de execução numa única thread com 16 milissegundos por frame. Prática: a parte assíncrona do Dart: Future, async/await, Stream, tratamento de erros e JSON.

DartPad: dartpad.dev

No fim desta aula consegues

Estados de uma app

EstadoO que o utilizador vêO que o SO faz
Foreground (ativa)A app no ecrã, a responder ao toqueDá-lhe prioridade de CPU, rede e GPS
Inativa (transição)Chamada a entrar, painel de notificações, multitarefaPausa animações; a app ainda está visível
BackgroundOutra app no ecrãLimita CPU e rede; ao fim de segundos, suspende
SuspensaNada; aparece na lista de apps recentesMantém em memória, mas não executa código
MortaNada, ou um ecrã de arranque quando voltaLibertou a memória; o próximo arranque é a frio
resumed foreground, ativa interrupção inactive transição, visível outra app à frente paused background, suspensa a app vai fechar detached sem vista, a terminar o SO termina o processo sem aviso: o que não gravaste, perdeste pode acontecer a qualquer momento
Os cinco estados da tabela vistos pelo Flutter: guarda em paused, porque depois pode não haver mais nenhum aviso.

Para os testes: em segundo plano a app pode ser morta a qualquer momento e sem aviso. O que não gravaste, perdeste.

Analogia Uma loja num centro comercial. Aberta e com clientes é o foreground. Com as luzes apagadas mas o stock no lugar é o background e a suspensão. De madrugada, o centro comercial pode despejar a loja sem avisar para dar o espaço a outra: é o processo terminado. Quando o cliente volta, a loja tem de reabrir como se nada fosse, e o que não estava guardado no armazém (o disco) perdeu-se.

Android e iOS em traços largos

AndroidiOS
Unidade do ciclo de vidaActivity: um ecrã com janelaScene (a partir do iOS 13) dentro da UIApplication
Callbacks principaisonCreate → onStart → onResume → onPause → onStop → onDestroysceneDidBecomeActive, sceneWillResignActive, sceneDidEnterBackground, sceneWillEnterForeground
Morte em backgroundO processo pode ser terminado sem callback; onSaveInstanceState guarda o essencial antesSuspensa ao fim de segundos, terminada sem aviso; state restoration para voltar ao mesmo sítio
Trabalho em backgroundServiços em primeiro plano (música, navegação) e WorkManager (aula 23)Modos declarados (áudio, localização) e BGTaskScheduler sem hora garantida (aula 23)
No FlutterUma só FlutterActivity para toda a appUm só FlutterViewController

O Spotify continua a tocar em segundo plano porque declara um serviço em primeiro plano no Android e um modo de áudio no iOS. Sem isso, morria como qualquer outra app.

AppLifecycleState no Flutter

class _EcraPrevisaoState extends State<EcraPrevisao> {
  late final AppLifecycleListener _ciclo;

  @override
  void initState() {
    super.initState();
    _ciclo = AppLifecycleListener(
      onResume: _atualizarPrevisoes,          // voltou ao ecrã: os dados podem estar velhos
      onPause: _guardarRascunho,              // vai para segundo plano: grava já
      onStateChange: (estado) => debugPrint('ciclo de vida: $estado'),
    );
  }

  @override
  void dispose() {
    _ciclo.dispose();
    super.dispose();
  }

  void _atualizarPrevisoes() { /* pedir à API se passaram mais de 10 min */ }
  void _guardarRascunho() { /* shared_preferences, aula 16 */ }
}

Os estados são resumed, inactive, paused, hidden e detached. Para os testes: resumed é o momento de atualizar; paused é o último momento seguro para gravar.

Uma só thread de UI

Thread de UI: um frame a cada 16 ms (60 Hz) frame normal: build → layout → paint cabe, sobra tempo 10 ms 10 ms 10 ms 10 ms 10 ms jsonDecode de 5 MB: 80 ms na thread, 4 frames saltados 80 ms sem desenhar nem tratar toques: jank 0 ms ✗ 16 ✗ 32 ✗ 48 ✗ 64 80 ms Event loop fila de microtasks then, await: esvazia-se primeiro fila de eventos toques, timers, rede, frames repetir: uma tarefa de cada vez
O loop só volta a desenhar quando a tarefa atual acaba: await devolve-lhe a thread, um cálculo síncrono não.

Para os testes: async/await não cria threads. Serve para esperar por rede e disco sem bloquear; não serve para acelerar cálculo.

Analogia O event loop é um empregado de balcão que atende um pedido de cada vez, pela ordem em que chegaram. await é o empregado a pôr um pedido de lado enquanto o café sai da máquina e a atender o seguinte; não fica parado a olhar para a máquina. Quando o café está pronto, o pedido volta à fila.

Jank: 16 milissegundos por frame

Para os testes: 60 fps significa 16 ms de orçamento por frame; um frame perdido é jank.

Analogia O ecrã pede uma imagem nova 60 vezes por segundo, e cada pedido tem 16 ms para ser servido. Um jsonDecode de 80 ms é um cliente que ocupa o balcão durante cinco pedidos: os outros ficam à espera e o ecrã encrava.

Isolates: a resposta ao trabalho pesado

Hoje fica a ideia. Na aula 18 medimos com o DevTools e movemos trabalho para um isolate.
Analogia Dois cozinheiros em cozinhas separadas, cada um com os seus tachos (a memória). Não partilham nada; passam pratos pela janela, e cada prato é uma cópia (as mensagens). O cozinheiro da sala, o isolate principal, nunca deixa a sala vazia: o trabalho pesado vai para a outra cozinha.

Verificação do ambiente (10 min)

Antes da prática, cada um corre num terminal novo:

flutter doctor
flutter devices
git --version

Tem de dar

  • ✓ Flutter, ✓ Android toolchain, ✓ Android Studio
  • Um dispositivo em flutter devices: o telemóvel por USB (depuração ligada, cabo de dados, confiança aceite) ou o emulador criado no Device Manager
  • git --version a responder, e o repositório pdm-dart no GitHub com os exercícios de quarta

Se falhar

  • Anota o erro exato e continua a aula com o DartPad; a prática de hoje não precisa do Flutter instalado.
  • Nos últimos 15 minutos o docente passa pelos casos por resolver, começando pelos de Windows: PATH, OneDrive, Modo de programador, drivers USB.
  • Segunda-feira precisa de tudo isto a funcionar: é a primeira aula no repositório.

Dart II no DartPad

Continuamos em dartpad.dev, sem Flutter. Seis blocos: Future, erros, paralelismo e timeouts, Stream, JSON, enums e extensões. No fim tens a classe Previsao que a app vai usar na segunda, já a ler JSON.

  1. Escreve cada bloco à mão e corre. Lê os erros do compilador antes de perguntar.
  2. Resolve o exercício antes de passar ao bloco seguinte.
  3. Guarda o link de partilha do DartPad no fim: é o teu trabalho de hoje.
Ritmo Cerca de 12 minutos por bloco. Se ficares preso mais de 5 minutos, chama.

1. Future e async/await

import 'dart:math';

Future<double> lerTemperatura(String localidade) async {
  await Future.delayed(const Duration(seconds: 1));    // simula a espera pela rede
  return 15 + Random().nextInt(10) + Random().nextDouble();
}

void main() async {
  print('A pedir Ponta Delgada...');
  final t = await lerTemperatura('Ponta Delgada');      // espera aqui, sem bloquear
  print('Ponta Delgada: ${t.toStringAsFixed(1)} °C');

  final futuro = lerTemperatura('Horta');               // sem await: é só um valor futuro
  print('Já pedi Horta e continuei');
  futuro.then((t) => print('Horta: ${t.toStringAsFixed(1)} °C'));
  print('Fim do main (a Horta chega depois)');
}

1. Future: o que interessa

Para os testes: await suspende a função, não a thread.

Exercício 1 Escreve Future<List<double>> lerSemana(String localidade) que chama lerTemperatura cinco vezes em sequência e devolve as cinco temperaturas. Mede o tempo com Stopwatch e imprime-o. Guarda o resultado: vais melhorá-lo no bloco 3.

2. Erros: try, on, catch, finally

class ApiException implements Exception {
  const ApiException(this.codigo, this.mensagem);
  final int codigo;
  final String mensagem;
  @override
  String toString() => 'ApiException($codigo): $mensagem';
}

Future<double> lerTemperatura(String localidade) async {
  await Future.delayed(const Duration(milliseconds: 500));
  if (localidade == 'Atlântida') throw const ApiException(404, 'localidade desconhecida');
  return 21.0;
}

void main() async {
  for (final l in ['Horta', 'Atlântida']) {
    try {
      print('$l: ${await lerTemperatura(l)} °C');
    } on ApiException catch (e) {
      print('A API recusou: ${e.mensagem} (${e.codigo})');   // erro esperado: trata-se
    } finally {
      print('--- pedido "$l" terminado');                      // corre sempre
    }
  }
}

2. Erros: o que interessa

Para os testes: erros esperados (rede, API) tratam-se com on; erros de programação (ArgumentError, ! num null) devem falhar cedo, em desenvolvimento.

Exercício 2 Acrescenta if (localidade.isEmpty) throw ArgumentError('localidade vazia'); e um catch (e) genérico depois do on. Corre com ['Horta', 'Atlântida', ''] e confirma que cada pedido cai no ramo certo e que o finally corre as três vezes.

3. Pedidos em paralelo e timeouts

import 'dart:async';

void main() async {
  final localidades = ['Ponta Delgada', 'Angra do Heroísmo', 'Horta'];
  final cronometro = Stopwatch()..start();

  for (final l in localidades) {
    await lerTemperatura(l);                           // um de cada vez: 3 × 1 s
  }
  print('Em sequência: ${cronometro.elapsedMilliseconds} ms');

  cronometro.reset();
  final temps = await Future.wait(localidades.map(lerTemperatura));
  print('Em paralelo: ${cronometro.elapsedMilliseconds} ms → $temps');   // ~1 s, mesma ordem

  try {
    final t = await lerTemperatura('Flores').timeout(const Duration(milliseconds: 300));
    print('Flores: $t');
  } on TimeoutException {
    print('Rede lenta: mostra os dados antigos e tenta mais tarde');
  }
}
Exercício 3 Reescreve lerSemana com Future.wait e compara os tempos com o exercício 1. Depois aplica .timeout(const Duration(seconds: 2)) ao conjunto e trata o TimeoutException.

4. Stream: valores ao longo do tempo

import 'dart:async';

Stream<double> leituras() async* {
  for (var i = 0; i < 5; i++) {
    await Future.delayed(const Duration(milliseconds: 400));
    yield 18 + i * 0.5;                                // emite um valor e continua
  }
}

Stream<DateTime> relogio() =>
    Stream.periodic(const Duration(seconds: 1), (_) => DateTime.now());

void main() async {
  await for (final t in leituras()) {                  // consome até o stream fechar
    print('Leitura: $t °C');
  }

  final subscricao = relogio().listen(
    (agora) => print('${agora.hour}:${agora.minute}:${agora.second}'),
  );
  await Future.delayed(const Duration(seconds: 3));
  await subscricao.cancel();                           // um stream infinito tem de ser cancelado
  print('Fim');
}
Exercício 4 Escreve Stream<String> avisos() que emite 'amarelo', 'laranja' e 'vermelho' com 1 s de intervalo. Consome com await for, imprime em maiúsculas e sai do ciclo com break ao receber 'vermelho'.

5. JSON (1 de 2): o modelo

import 'dart:convert';

class Previsao {
  const Previsao({required this.localidade, required this.dia, required this.tMin, required this.tMax});

  final String localidade;
  final DateTime dia;
  final double tMin;
  final double tMax;

  factory Previsao.fromJson(Map<String, dynamic> json) => Previsao(
        localidade: json['localidade'] as String,
        dia: DateTime.parse(json['dia'] as String),
        tMin: (json['tMin'] as num).toDouble(),        // a API pode mandar 17 ou 17.0
        tMax: (json['tMax'] as num).toDouble(),
      );

  Map<String, dynamic> toJson() => {
        'localidade': localidade,
        'dia': dia.toIso8601String(),
        'tMin': tMin,
        'tMax': tMax,
      };
}

5. JSON (2 de 2): decifrar e serializar

const corpo = '''
[
  {"localidade": "Horta", "dia": "2026-09-23", "tMin": 17, "tMax": 22.5},
  {"localidade": "Horta", "dia": "2026-09-24", "tMin": 16, "tMax": 21}
]''';

void main() {
  final bruto = jsonDecode(corpo) as List<dynamic>;        // ainda não sabe o que é
  final previsoes = bruto
      .map((e) => Previsao.fromJson(e as Map<String, dynamic>))
      .toList();

  print(previsoes.length);                                     // 2
  print(previsoes.first.tMax);                                 // 22.5
  print(previsoes.map((p) => p.dia.day).toList());             // [23, 24]

  final texto = jsonEncode(previsoes.map((p) => p.toJson()).toList());
  print(texto);                                                // pronto para enviar ou gravar
}

Pipeline: String (corpo da resposta) → jsonDecode → Map / List → fromJson → Previsao → List<Previsao> → widgets

Analogia JSON é texto com etiquetas, como um formulário preenchido à mão. jsonDecode transforma esse texto num dicionário (Map<String, dynamic>): já se lê por chave, mas nada é verificado. Previsao.fromJson passa o formulário a limpo para uma ficha tipada: cada campo no sítio certo, com o tipo certo, e um erro claro se faltar algo. A partir daí o resto da app trabalha com fichas, nunca com texto.

5. JSON: o que interessa

Para os testes: em Dart o JSON é decifrado para Map e List genéricos; a conversão para classes é código teu (ou gerado com json_serializable).

Exercício 5 Cria class Localidade com nome, ilha e List<Previsao> previsoes, com um fromJson que converte a lista aninhada. Decifra um JSON com uma localidade e duas previsões e imprime a tMax mais alta.

6. Enums com valores e extension methods

enum TipoTempo {
  sol('☀️', 'Céu limpo'),
  nublado('☁️', 'Nublado'),
  chuva('🌧️', 'Chuva'),
  trovoada('⛈️', 'Trovoada');

  const TipoTempo(this.icone, this.descricao);
  final String icone;
  final String descricao;

  static TipoTempo fromApi(String codigo) =>
      values.firstWhere((t) => t.name == codigo, orElse: () => nublado);
}

extension Temperatura on double {
  String get celsius => '${toStringAsFixed(1)} °C';
  double get fahrenheit => this * 9 / 5 + 32;
}

void main() {
  final t = TipoTempo.fromApi('chuva');
  print('${t.icone} ${t.descricao}');   // 🌧️ Chuva
  print(21.456.celsius);                // 21.5 °C
  print(21.0.fahrenheit);               // 69.8
}

6. Enums e extensões: o que interessa

Para os testes: um enum em Dart 3 é uma classe com um número fixo de instâncias; pode ter campos e métodos, mas não pode ser estendido.

Exercício 6 Escreve extension DataPt on DateTime com curta ('23/9') e diaSemana ('segunda' a 'domingo', a partir de weekday). Acrescenta a Previsao um getter resumo que devolve 'quarta 23/9: 17.0 °C a 22.5 °C' usando as duas extensões.

Até segunda

Para fazer até segunda

  • Completar Previsao.fromJson com validação de campos em falta, e fazer push para o pdm-dart.
  • Ler Asynchronous programming no dart.dev.
  • flutter doctor sem erros e um dispositivo em flutter devices (telemóvel ou emulador). Segunda-feira começa com git clone e flutter run.

Para ler