Aula 19 · Semana 9 · Segunda, 2 de novembro de 2026 · 16:00 às 18:00
Push notifications, review da sprint 1 e revisão para o teste 1
Teoria (30 minutos): percurso de uma notificação push do servidor ao ecrã bloqueado. Review da sprint 1, retrospetiva e planeamento da sprint 2. Revisão para o teste 1: mapa da matéria e perguntas-tipo.
Review da sprint 1Teste 1 na quarta, 4/11
No fim desta aula consegues
Descrever o caminho de uma notificação push do servidor até à app, e o papel do token de dispositivo.
Distinguir mensagem de notificação de mensagem de dados, e o que acontece em foreground, background e com a app morta.
Explicar canais no Android, categorias no iOS e para que servem as notificações silenciosas.
Decidir quando pedir a permissão de notificações e como não a perder.
Apresentar uma review de sprint e sair com o backlog da sprint 2 planeado.
Situar cada tema das aulas 1 a 19 e dizer a ideia-chave de cada um numa frase.
Resolver uma pergunta de escolha múltipla por eliminação de opções, sem adivinhar.
Reconhecer os cinco erros clássicos que aparecem em todos os testes.
Como uma push chega ao ecrã
O servidor nunca fala com o telemóvel: entrega ao FCM ou APNs, e o sistema operativo decide o que fazer conforme o estado da app.
A app regista-se no serviço da plataforma: APNs (Apple Push Notification service) no iOS, FCM (Firebase Cloud Messaging) no Android. Recebe um token de dispositivo: uma string longa que identifica esta instalação neste telemóvel.
A app envia o token ao teu servidor, associado ao utilizador. Sem isto, o servidor não sabe a quem enviar.
O servidor envia o payload ao FCM/APNs (nunca diretamente ao telemóvel), autenticado com uma chave de servidor.
O FCM/APNs entrega ao sistema operativo, que decide: mostrar, acordar a app, ou descartar.
O token muda: reinstalação, restauro de cópia de segurança, limpeza de dados. A app tem de ouvir onTokenRefresh e reenviar.
No iOS, o FCM também funciona: envolve o APNs, e precisa da chave APNs configurada no projeto Firebase.
Para os testes: o teu servidor nunca fala com o telemóvel; fala com o FCM ou o APNs, que falam com o sistema operativo.
Analogia Os correios. A app não fica à porta à espera do carteiro: regista a sua morada, o token de dispositivo, nos correios da plataforma, o FCM ou o APNs. O servidor entrega a carta aos correios com essa morada, e os correios entregam-na quando o telemóvel estiver contactável. Uma única ligação do telemóvel aos correios serve todas as apps instaladas, e é por isso que a bateria aguenta.
Permissões: pedir no momento certo
iOS pede sempre, com um diálogo do sistema que só aparece uma vez. Se o utilizador diz não, só nas Definições se volta atrás.
Android 13+ (API 33) também pede em tempo de execução: POST_NOTIFICATIONS. Em versões anteriores as notificações vêm ligadas por defeito.
Pedir no arranque da app é a forma mais rápida de receber um "não": o utilizador ainda não sabe para que serve.
Pedir em contexto: depois de o utilizador marcar Ponta Delgada como favorita, "Queres ser avisado quando o IPMA emitir um aviso para Ponta Delgada?", e só então o diálogo do sistema.
O iOS tem autorização provisória: notificações silenciosas na central, sem diálogo, até o utilizador decidir.
Os estudos da indústria apontam para cerca de metade dos utilizadores a aceitar em iOS e bastante mais em Android; a diferença vem de quando e como se pede.
Para os testes: um pedido de permissão explica-se antes de aparecer, e faz-se quando o utilizador já vê o benefício.
Payload: notificação vs dados
Estado da app
Mensagem de notificação (notification)
Mensagem de dados (data)
Foreground
Entregue ao teu código; por defeito não aparece nada. A app decide mostrar.
Entregue ao teu código.
Background
O sistema mostra-a na bandeja sem correr a app. Ao tocar, a app abre com os dados.
Acorda a app por instantes para processar (com limites).
Morta
Igual ao background: o sistema trata.
Android: pode acordar a app. iOS: só se for silenciosa e o sistema deixar.
O payload tem title, body e um mapa data de strings, por exemplo {"tipo": "aviso", "localidadeId": "3"}. É com o data que a app sabe que ecrã abrir.
Limite prático: 4 KB. Não se manda a previsão inteira; manda-se um identificador e a app vai buscar.
Para os testes: com a app em background, uma mensagem só de notificação é mostrada pelo sistema sem executar código da app.
Canais, categorias, som, badge e silêncio
Canais (Android 8+): cada notificação pertence a um canal com nome e importância. O utilizador desliga canais nas definições do sistema, um a um: "Avisos meteorológicos" ligado, "Novidades" desligado. Uma app com um único canal obriga a tudo ou nada.
Categorias (iOS): definem os botões de ação ("Ver previsão", "Silenciar 24h") e agrupamento.
Som e badge: o número no ícone (badge) é gerido pela app no iOS e pelo launcher no Android. Sons personalizados têm de vir empacotados na app.
Notificações silenciosas (content-available no iOS, mensagem de dados com prioridade no Android): não mostram nada; acordam a app para sincronizar. O iOS limita-as a poucas por hora e não garante entrega.
Exemplo: o WhatsApp usa uma silenciosa para descarregar a mensagem e depois mostra a notificação já com o texto.
Para os testes: canais são do Android e servem para o utilizador escolher que tipos de notificação quer.
Locais vs remotas
Local
Remota (push)
Quem a cria
A própria app, no telemóvel
Um servidor, via FCM/APNs
Precisa de rede
Não
Sim, pelo menos no momento da entrega
Quando serve
Lembretes, agendadas ("previsão de amanhã às 20:00"), eventos que a app já conhece
Novidades que só o servidor sabe: aviso novo do IPMA, mensagem de outra pessoa
Aspeto para o utilizador
Idêntico. O utilizador não distingue.
Package Flutter
flutter_local_notifications
firebase_messaging
Na aula 22 fazemos notificações locais: a app deteta um aviso novo para um favorito quando atualiza, e agenda a previsão do dia seguinte. Push remoto exige um projeto Firebase por grupo; fica como extra do projeto.
Analogia Uma notificação local é um lembrete que a própria app deixa no frigorífico: escreve-o e agenda-o no telemóvel, sem passar pelos correios. Uma push remota é uma carta que vem de fora, e só os correios (FCM ou APNs) a sabem entregar. Para quem lê, o papel no frigorífico e a carta têm o mesmo aspeto.
Boas práticas
Não abusar. Uma notificação inútil custa a permissão para todas as seguintes. O Duolingo é personalizado e frequente, e mesmo assim muita gente o silencia.
Personalizar. "Aviso laranja de vento forte para Angra do Heroísmo até às 18:00" em vez de "Há novos avisos".
Deep link para o conteúdo. Tocar na notificação abre o ecrã da localidade, não a página inicial.
Permitir desligar por tipo dentro da app, e respeitar horas de silêncio.
Agrupar e substituir. Três avisos para a mesma localidade são uma notificação atualizada, não três.
Funcionar sem elas. A app não pode depender de uma push chegar; o iOS descarta-as quando quer.
Para os testes: uma notificação sem deep link para o conteúdo é uma notificação mal feita.
Review da sprint 1: 5 minutos por grupo
O que cada grupo mostra
1 min: objetivo da sprint e o que entrou.
3 min: demo no telemóvel, projetada, a partir de main. Percurso: abrir a app, lista, detalhe, um estado de erro (modo avião).
1 min: o que ficou por fazer e porquê; o que muda na sprint 2.
O que o docente observa
A app corre a partir de main, sem cabo do docente.
Cada membro tem commits e PRs revistos por outro.
Estados de loading, erro e vazio existem.
Token e segredos fora do repositório.
Cada membro consegue explicar a sua parte.
Retrospetiva: 15 minutos
Cada grupo, em conjunto, escreve três colunas num documento partilhado ou em post-its:
Começar
Parar
Continuar
O que não fizemos e devíamos ter feito. Ex.: rever PRs no próprio dia.
O que nos atrasou. Ex.: PRs de 800 linhas; trabalhar todos no mesmo ficheiro.
O que funcionou. Ex.: reunião de 10 min ao início de cada aula.
Fala-se do processo, não das pessoas. "Os PRs ficaram dias por rever", não "o X não reviu".
Cada coluna com um a três itens. Mais do que isso não se cumpre.
Um item de "Começar" vira uma issue na sprint 2, com dono.
Planear a sprint 2 (3/11 a 18/11)
Três semanas com um teste no meio (4/11). Contem com uma semana de produtividade mais baixa e planeiem menos do que na sprint 1.
Candidatos ao backlog, pela ordem das aulas: favoritos sincronizados com o backend (aula 17), cache offline com indicação de antiguidade (aula 16), mapa com localidade mais próxima (workshop de 6/11), notificação de aviso e deep link (aula 22).
Issues com critérios de aceitação: "Como utilizador, ao abrir a app sem rede vejo a última previsão e a hora em que foi obtida". Estimativa grosseira: P, M, G.
Distribuição: cada membro dono de pelo menos uma issue M ou G. Quem fez UI na sprint 1 faz dados na sprint 2, e vice-versa.
Definição de pronto não muda: corre no telemóvel, sem avisos, PR revisto, estados de erro tratados.
Até sexta, 6/11 Backlog da sprint 2 fechado no board do repositório, com issues atribuídas. O docente comenta as issues no fim de semana.
Mapa da matéria (1 de 3): fundações
Aula 1 · Panorama mobile. Ecrã pequeno, bateria, rede intermitente, ciclo de vida imposto, sandbox, lojas. Cinco abordagens: nativo, Flutter, React Native/KMP, híbrido, PWA. O Flutter desenha cada pixel.
Aulas 2 e 3 · Flutter e Dart. Framework (Dart), engine (C++), embedder. Widget imutável; build descreve, não desenha. Hot reload mantém estado; hot restart não. JIT em desenvolvimento, AOT em release.
Aula 4 · Ciclo de vida e assincronia. Foreground, background, suspensa, morta; AppLifecycleState. Uma thread de UI, event loop, 16 ms por frame. Future, async/await, Stream.
Aula 5 · Projeto Flutter.lib/, pubspec.yaml, ^ nas versões, pubspec.lock. Debug, profile, release. DevTools.
Aulas 6 e 7 · UX e layouts. Material 3 vs HIG, 48 dp de toque, gestos esperados. Constraints descem, tamanhos sobem, o pai posiciona. ListView.builder constrói os itens sob demanda.
Aula 8 · Estado. Efémero vs de aplicação. setState → Provider → BLoC/Riverpod conforme a escala. Fluxo unidirecional, imutabilidade, copyWith.
Mapa da matéria (2 de 3): construir
Aula 9 · Navegação e temas. Pilha push/pop; go substitui, push empilha. URL como estado (go_router). ColorScheme.fromSeed, ThemeMode.
Aula 10 · Acessibilidade e i18n. TalkBack e VoiceOver leem a árvore de Semantics. Contraste, textScaler, layouts que não partem. Ficheiros .arb, plurais, intl.
Aula 11 · Qualidade. Lints em analysis_options.yaml. Organização por feature. Widgets pequenos e const. Branch por tarefa, PR pequeno, ninguém faz push para main.
Aula 12 · Rede. Códigos 2xx, 4xx, 5xx, 429 com Retry-After. REST, GraphQL, gRPC, WebSockets. OpenAPI. Timeouts, retries com backoff, um cliente HTTP único com interceptors.
Aula 13 · Arquitetura. UI → Notifier → Repository → DataSource. Dependências só para dentro. Injeção por construtor; nada de singletons escondidos.
Aula 14 · Resiliência. Tudo falha. Estados Loading, Erro, Vazio, Dados como sealed class. Offline-first. ETag e 304. Backoff exponencial com jitter; nunca repetir POST não idempotente.
Mapa da matéria (3 de 3): pôr em produção
Aula 15 · Metodologia. MVP corta funcionalidades, não qualidade. Backlog em issues com critérios de aceitação. Definição de pronto. Sprints de duas semanas com review.
Aula 16 · Persistência. Chave-valor, SQLite, documentos, ficheiros, segredos: cada coisa no seu sítio. Sandbox. Keychain e Keystore. Cache-first com antiguidade visível.
Aula 17 · Autenticação. Autenticar ≠ autorizar. JWT é assinado, não cifrado. OAuth2 com PKCE: a app nunca vê a password. Access curto, refresh longo, ambos no armazenamento seguro. Biometria desbloqueia localmente.
Aula 18 · Segurança. OWASP Mobile Top 10. Segredos nunca no repositório; um APK abre-se em 5 minutos. HTTPS sempre; pinning é caro de manter. Permissões mínimas. RGPD e App Tracking Transparency.
Aula 18 · Performance. Isolates para cálculo; async só para espera. const e widgets pequenos contra rebuilds. ListView.builder, imagens redimensionadas. Medir em profile.
Aula 19 · Push. Servidor → FCM/APNs → SO → app. Token de dispositivo muda. Notificação vs dados. Canais no Android. Pedir permissão em contexto.
Perguntas-tipo (1 de 3)
Método Lê a pergunta até ao fim. Risca as duas opções claramente erradas. Entre as duas que sobram, escolhe a que usa o vocabulário exato das aulas; a outra costuma ser "quase verdade".
1. Alteraste o corpo de main() e carregaste em r. A app não reflete a alteração. Porquê?
O hot reload só funciona em modo release.
O hot reload não volta a executar main(); é preciso hot restart.
Faltou correr flutter pub get.
O telemóvel está em modo avião.
2. Um cálculo de 2 segundos dentro de build faz a app…
continuar fluida, porque o Dart é multithread por defeito.
congelar, porque a interface corre numa única thread.
ser terminada de imediato pelo sistema operativo.
falhar a compilação.
3. Onde vive o estado de um StatefulWidget?
No próprio widget, que por isso é mutável.
No objeto State, que sobrevive aos rebuilds do widget.
No BuildContext.
No MaterialApp.
Perguntas-tipo (2 de 3)
4. Com go_router, qual é a diferença entre context.go('/x') e context.push('/x')?
go substitui a pilha pela rota; push empilha por cima da atual.
São equivalentes; push é o nome antigo.
push só funciona na web.
go só funciona com Navigator.push.
5. A API do curso devolve 429. O que deve a app fazer?
Repetir imediatamente até obter 200.
Esperar o tempo indicado em Retry-After e repetir com backoff.
Mostrar um erro fatal e fechar a app.
Trocar o método POST por GET.
6. Qual destes pedidos não deves repetir automaticamente depois de um timeout?
GET /v1/locations
GET /v1/forecast/3
POST /v1/me/favorites
GET /v1/warnings
7. Onde se guarda o token de acesso da API?
Em shared_preferences, porque é rápido.
Num ficheiro em getApplicationDocumentsDirectory().
Em flutter_secure_storage, que usa o Keychain e o Keystore.
Numa constante no código, para não se perder.
Perguntas-tipo (3 de 3)
8. Um JWT…
é cifrado; ninguém consegue ler o conteúdo.
é assinado; qualquer pessoa lê o conteúdo, mas não o altera sem invalidar a assinatura.
nunca expira.
deve conter a password do utilizador para renovar a sessão.
9. Para fazer o parse de uma resposta JSON de 5 MB sem jank, usas…
setState depois do parse.
Future.delayed para dar tempo à interface.
compute() ou Isolate.run.
async/await, que liberta a thread durante o parse.
10. Uma push só com campo notification, sem data, chega com a app em background. O que acontece?
A app é acordada e decide o que mostrar.
O sistema operativo mostra-a na bandeja sem executar código da app.
É descartada.
Fica guardada até a app ser aberta manualmente.
Respostas e porquê
b. O hot reload injeta código e volta a chamar build; main e variáveis globais só com R.
b. Uma thread de UI, um event loop. Cálculo pesado bloqueia até acabar.
b. O widget é imutável; o State é o objeto que persiste.
a.go é navegação declarativa para uma localização; push empilha um ecrã que se pode fechar com voltar.
b. 429 é "abranda"; repetir de imediato piora e pode levar a bloqueio.
c. POST não é idempotente: repetir pode criar o favorito duas vezes. GETs repetem-se sem risco.
c.shared_preferences é um ficheiro legível num telemóvel com root; segredos vão para o armazenamento seguro do sistema.
b. Base64 não é cifra. A assinatura garante integridade, não confidencialidade. Expira via exp.
c.await só cede a thread em operações de espera; jsonDecode é cálculo puro.
b. Mensagens de notificação em background são tratadas pelo sistema; só ao tocar é que a app recebe os dados.
Erros frequentes no teste
Erro
Porque está errado
Confundir hot reload com hot restart
Reload mantém estado e não re-executa main; restart recomeça a app e perde o estado. Rebuild completo é outra coisa: flutter run de novo.
Widget "mutável" com campos que mudam
Widgets são imutáveis e recriados a cada build; o que muda é o State ou o estado de aplicação num notifier.
! como solução para o null safety
Força um valor que pode ser null e transfere o erro do compilador para um crash em produção. Usa ?., ??, if ou padrões.
Token em shared_preferences
Texto legível no disco; num telemóvel com root ou numa cópia de segurança fica exposto. Keychain e Keystore existem para isto.
Retry automático em POST
Um POST não idempotente repetido cria duplicados. Retry só em operações seguras de repetir, com backoff e limite.
Para os testes: quando duas opções parecem certas, a certa é a que não tem uma palavra a mais ("sempre", "nunca", "só em iOS").
Até quarta, dia do teste
Para fazer
Estudar a componente teórica das aulas 1 a 19: os slides de teoria de cada aula e as frases "Para os testes".
Refazer as dez perguntas desta aula sem ver as respostas, e escrever uma pergunta nova por aula.
Backlog da sprint 2 fechado até sexta: issues com critérios de aceitação, estimativa e dono, organizadas no board.
Sprint 2: a branch de cada membro com o primeiro commit até sexta.
Para ler
How Flutter works: widgets, elements e render objects, o que build faz e porque o hot reload é possível (aulas 2 e 18).
Concurrency and isolates: event loop, isolate principal e quando usar isolates; explica porque await não resolve cálculo pesado (aulas 4 e 18).