Aula 04 · Semana 3 · Sexta, 25 de setembro de 2026 · 08:30 às 10:30
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.
Future, async/await, tratamento de erros e Stream.fromJson e toJson.| Estado | O que o utilizador vê | O que o SO faz |
|---|---|---|
| Foreground (ativa) | A app no ecrã, a responder ao toque | Dá-lhe prioridade de CPU, rede e GPS |
| Inativa (transição) | Chamada a entrar, painel de notificações, multitarefa | Pausa animações; a app ainda está visível |
| Background | Outra app no ecrã | Limita CPU e rede; ao fim de segundos, suspende |
| Suspensa | Nada; aparece na lista de apps recentes | Mantém em memória, mas não executa código |
| Morta | Nada, ou um ecrã de arranque quando volta | Libertou a memória; o próximo arranque é a frio |
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.
| Android | iOS | |
|---|---|---|
| Unidade do ciclo de vida | Activity: um ecrã com janela | Scene (a partir do iOS 13) dentro da UIApplication |
| Callbacks principais | onCreate → onStart → onResume → onPause → onStop → onDestroy | sceneDidBecomeActive, sceneWillResignActive, sceneDidEnterBackground, sceneWillEnterForeground |
| Morte em background | O processo pode ser terminado sem callback; onSaveInstanceState guarda o essencial antes | Suspensa ao fim de segundos, terminada sem aviso; state restoration para voltar ao mesmo sítio |
| Trabalho em background | Serviç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 Flutter | Uma só FlutterActivity para toda a app | Um 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 Flutterclass _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.
await devolve-lhe a thread, um cálculo síncrono não.then, await) e eventos (toques, timers, respostas de rede, o pedido para desenhar o próximo frame). Esvazia sempre as microtasks antes de passar ao evento seguinte.await não bloqueia: suspende a função, devolve a thread ao loop, e a continuação entra na fila quando o valor chegar.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.
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.build, layout, pintura e entregar à GPU. Se o teu código ocupar a thread por mais do que isso, o frame é saltado.Para os testes: 60 fps significa 16 ms de orçamento por frame; um frame perdido é jank.
jsonDecode de 80 ms é um cliente que ocupa o balcão durante cinco pedidos: os outros ficam à espera e o ecrã encrava.Isolate.run(() => jsonDecode(texto)) ou compute(parse, texto) corre a função noutro isolate e devolve o resultado como Future. A UI continua a 60 fps.Antes da prática, cada um corre num terminal novo:
flutter doctor
flutter devices
git --version
flutter devices: o telemóvel por USB (depuração ligada, cabo de dados, confiança aceite) ou o emulador criado no Device Managergit --version a responder, e o repositório pdm-dart no GitHub com os exercícios de quartaContinuamos 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.
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)');
}
Future<T> é a promessa de um valor T que ainda não existe. async marca a função; await espera pelo valor sem parar a thread.async devolve sempre um Future, mesmo que escrevas return 21.0.await dentro de uma função async. Se não fizeres await, o Future corre na mesma; só não esperas por ele.then é o que o await faz por baixo. Prefere await: lê-se de cima para baixo.Para os testes: await suspende a função, não a thread.
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.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
}
}
}
on Tipo catch (e) apanha só esse tipo; catch (e) apanha tudo. A ordem importa: do mais específico para o mais geral.finally corre sempre, com ou sem erro. É onde desligas o indicador de loading e fechas o que abriste.implements Exception) com código e mensagem deixa a UI decidir: 404 mostra "não encontrado", 500 mostra "tenta mais tarde".await que falha lança a exceção no sítio do await. Sem try à volta, sobe até terminar a app com erro.Para os testes: erros esperados (rede, API) tratam-se com on; erros de programação (ArgumentError, ! num null) devem falhar cedo, em desenvolvimento.
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.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');
}
}
Future.wait lança todos ao mesmo tempo e devolve uma lista pela ordem original. Se um falhar, o await lança..timeout lança TimeoutException se o valor não chegar a tempo. Sem timeout, a app fica "a carregar" para sempre dentro de um túnel.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.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');
}
Stream é uma sequência de valores no tempo: leituras de um sensor, posição GPS, mensagens de um WebSocket, o relógio do ecrã.async* e yield criam; await for consome; listen devolve uma subscrição que tens de cancelar em dispose (aula 8).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'.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,
};
}
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
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.jsonDecode devolve dynamic: uma Map<String, dynamic> para objetos, uma List<dynamic> para listas. Fazes o cast e depois constróis os teus objetos.fromJson é um factory: recebe o mapa e devolve uma instância. toJson faz o caminho inverso. Este par vai existir em todos os modelos do projeto.17 de 17.0; em Dart o primeiro é int. (x as num).toDouble() evita um crash.json['tMax'] as num lança TypeError. Validar isto é o trabalho de casa.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).
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.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
}
const, getters e métodos estáticos. values é a lista de todos; name é o identificador como texto.firstWhere com orElse transforma a string da API num valor do enum sem falhar quando aparece um valor novo.extension X on Tipo acrescenta métodos a tipos que não são teus (double, DateTime, String). Precisa de estar importada no ficheiro onde a usas.BuildContext (context.theme) e em num em muitos packages do pub.dev.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.
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.Previsao.fromJson com validação de campos em falta, e fazer push para o pdm-dart.flutter doctor sem erros e um dispositivo em flutter devices (telemóvel ou emulador). Segunda-feira começa com git clone e flutter run.Isolate.run, quando usar e o custo de passar dados entre isolates.