Aula 09 · Semana 5 · Quarta, 7 de outubro de 2026 · 16:00 às 18:00
Navegação
Teoria: os ecrãs de uma app organizados numa pilha, o efeito do "voltar" no estado, e as rotas declaradas como URLs. Prática, na app das ilhas: abrir uma ilha e voltar, rotas com go_router e uma barra com três separadores.
Repositório: pdm-aula-09
No fim desta aula consegues
Explicar a pilha de navegação, o que push e pop fazem ao estado, e como Android e iOS tratam o "voltar".
Reconhecer os padrões lista → detalhe, separadores com pilhas independentes, modais e bottom sheets.
Justificar rotas declarativas com URL e explicar o que é um deep link.
Navegar com Navigator e com go_router, passando o id da ilha.
Hoje, na app (1 de 2): abrir uma ilha e voltar
1push
Tocas no Pico: o ecrã do Pico entra por cima da grelha.
→
2pop
A seta ← fecha o ecrã do Pico.
→
3a grelha como estava
A grelha continua onde estava: o mesmo scroll, a mesma pesquisa.
É o que fazes nos passos 1 e 2: primeiro com Navigator.push, depois com o go_router e o caminho /island/pico. Na app, os dois passos ficam iguais.
Hoje, na app (2 de 2): três separadores
3
Previsão: o Pico abre por cima da grelha e a barra continua lá.
3
Avisos: o formulário de avisos, agora num separador. Voltas a Previsão e o Pico ainda está aberto.
3
Definições: por agora só a versão; vai ganhar opções nas próximas aulas.
A barra de baixo é o passo 3 (NavigationBar com StatefulShellRoute): cada separador guarda a sua pilha.
A pilha de navegação
Uma pilha por fluxo: o ecrã de baixo fica vivo até fazeres pop; cada separador da barra tem a sua própria pilha.
push põe um ecrã em cima; pop tira-o e volta ao de baixo.
O ecrã de baixo fica vivo: scroll, pesquisa e State como os deixaste.
O ecrã que sai é destruído (dispose).
Para os testes:push mantém o ecrã anterior vivo; pop destrói o ecrã atual.
Voltar: botão Android, gesto iOS, e o estado
A seta da AppBar aparece sozinha quando há um ecrã por baixo; o gesto do iOS vem com o MaterialPageRoute.
Formulário a meio? PopScope pergunta "Descartar alterações?" antes de sair.
Para os testes: o "voltar" é do sistema, não da app; a app pode intercetá-lo com PopScope, mas não pode ignorá-lo.
Padrões de navegação
Hoje fazemos os dois primeiros: a grelha → o Pico, e a barra com três separadores.
dependencies: # vai para dentro da app
flutter:
sdk: flutter
go_router: ^18.0.2
google_fonts: ^8.2.1
O go_router não vem com o Flutter: é um pacote do pub.dev, como o google_fonts. flutter pub add go_router acrescenta a linha ao pubspec.yaml e flutter pub get descarrega-o. No repositório de hoje já lá está.
Para os testes: rotas declarativas descrevem os ecrãs como URLs e derivam a pilha do URL; são a base de deep links e da web.
Deep links em uma frase
Um deep link é um URL que abre a app diretamente num ecrã específico, em vez de no ecrã inicial.
1Um link
num SMS ou notificaçãotempoacores.pt/island/pico
O utilizador toca no link.
→
2O go_router
trata-o comocontext.go('/island/pico')
O mesmo caminho das nossas rotas.
→
3O ecrã certo
A app abre no Pico, com a grelha por baixo.
Configuração e testes na aula 22. Hoje só desenhamos os caminhos.
Começar: o repositório de hoje
git clone https://github.com/Tiago-Rocha/pdm-aula-09.git
cd pdm-aula-09
flutter pub get
Abre a pasta pdm-aula-09 no VS Code. É a Tempo Açores que já conheces, mais três ecrãs ainda por ligar: lib/island_screen.dart (uma ilha), lib/alerts_screen.dart (o formulário de avisos, que saiu do ecrã principal) e lib/settings_screen.dart (definições). Trabalhas nos comentários TODO 1 a TODO 3.
Ctrl+Shift+P → Flutter: Select Device → o teu Android ou o Chrome. Depois F5.
Confirma o que vês: o ecrã de previsão com a pesquisa e a grelha das ilhas. Tocar numa ilha ainda não faz nada.
O go_router já está no pubspec.yaml: o flutter pub get trata dele. Hoje o Chrome tem uma vantagem: a partir do passo 2 vês o caminho de cada ecrã na barra de endereço.
1. Navigator: push e pop
appBar: AppBar(title: Text(island.name)),
Navigator.push põe um ecrã novo em cima da pilha. MaterialPageRoute diz qual é o ecrã e dá a animação de cada plataforma.
O IslandScreen não tem botão de voltar no código: a seta ← da AppBar aparece sozinha quando há um ecrã por baixo, e faz pop.
O botão de retroceder do Android e o gesto do iOS fazem o mesmo pop.
Pergunta à turma Escreves "pi" na pesquisa, abres o Pico e voltas com a seta. A pesquisa ainda lá está?
Passo 1: abrir uma ilha
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => IslandScreen(id: island.id)),
),
child: Column(
// a Column de antes, como está
),
),
);
Faz Em island_card.dart, embrulha a Column num InkWell com este onTap e importa o island_screen.dart.
Na app: tocar no Pico abre o ecrã do Pico, com a foto e os cinco dias; a seta ← volta à grelha, no mesmo sítio.
2. go_router: cada ecrã com um caminho
import 'package:go_router/go_router.dart';
import 'forecast_screen.dart';
import 'island_screen.dart';
/// Every screen of the app, with its path, like the pages of a site.
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const ForecastScreen(),
routes: [
GoRoute(
path: 'island/:id', // /island/pico
builder: (context, state) =>
IslandScreen(id: state.pathParameters['id']!),
),
],
),
],
);
Cada GoRoute liga um caminho a um ecrã. :id é um parâmetro: em /island/pico, state.pathParameters['id'] vale 'pico'.
A rota island/:id está dentro da rota /: abrir /island/pico põe a grelha por baixo e o Pico por cima, mesmo quando se entra diretamente por um link. A seta de voltar funciona.
context.push('/island/pico') empilha por cima, como o Navigator.push. context.go('/island/pico') substitui a pilha pela do caminho. Para "abrir e voltar", push.
Pergunta à turma Porque é que o caminho leva o id da ilha e não a Island inteira?
Passo 2: trocar o Navigator pelo go_router
git stash -u
git checkout passo-1
flutter pub get
return MaterialApp.router(
title: 'Tempo Açores',
theme: ThemeData(
// o tema, como está
),
routerConfig: router,
);
Faz Escreve o router.dart do slide anterior. No main.dart, MaterialApp passa a MaterialApp.router, o home: sai e entra o routerConfig. No IslandCard, o onTap com o Navigator.push dá lugar a uma linha com context.push. Acerta os import.
Na app: tudo igual ao passo 1, e é esse o objetivo. No Chrome, ao abrir o Pico a barra de endereço mostra /island/pico; escrever /island/flores abre as Flores com a grelha por baixo.
3. Barra de navegação com pilhas independentes
StatefulShellRoute.indexedStack(
builder: (context, state, shell) => Scaffold(
body: shell, // the current tab, with its own stack
bottomNavigationBar: NavigationBar(
selectedIndex: shell.currentIndex,
onDestinationSelected: (i) =>
shell.goBranch(i, initialLocation: i == shell.currentIndex),
destinations: const [
NavigationDestination(
icon: Icon(Icons.wb_sunny_outlined),
label: 'Previsão',
),
// e Avisos (Icons.warning_amber), Definições (Icons.settings_outlined)
],
),
),
branches: [
// um StatefulShellBranch por separador
],
),
O StatefulShellRoute desenha um Scaffold comum à volta de todos os separadores, com a NavigationBar em baixo. O shell é o separador atual.
Cada StatefulShellBranch é um separador com a sua pilha. indexedStack mantém os três vivos: mudar de separador não faz pop. É o padrão do Instagram.
Tocar no separador que já está ativo (i == shell.currentIndex) volta à raiz desse separador. É o que os utilizadores esperam.
NavigationBar é a do Material 3; BottomNavigationBar é a antiga. Três a cinco destinos, com ícone e etiqueta.
Pergunta à turma Cada separador tem um Scaffold com a sua AppBar, e o shell tem outro Scaffold com a barra. Dois Scaffold um dentro do outro: é um erro?
Passo 3: três separadores
git stash -u
git checkout passo-2
flutter pub get
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/',
// a rota / do passo 2, com a filha island/:id
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/alerts',
builder: (context, state) => const AlertsScreen(),
),
],
),
// e o terceiro: '/settings' com o SettingsScreen
],
Faz No router.dart, a lista routes do GoRouter passa a ter um só elemento: o StatefulShellRoute.indexedStack do slide anterior. A rota / do passo 2 entra inteira no primeiro StatefulShellBranch; faz os outros dois. Importa o material.dart, o alerts_screen.dart e o settings_screen.dart.
Na app: uma barra em baixo com Previsão, Avisos e Definições. Abres o Pico, vais a Avisos, voltas a Previsão: o Pico continua aberto. Tocas outra vez em Previsão e voltas à grelha.
Onde ficámos
IslandCard abre o IslandScreen com Navigator.push; a seta da AppBar faz pop (passo 1).
router.dart com GoRouter: / e /island/:id, MaterialApp.router e context.push pelo caminho (passo 2).
StatefulShellRoute.indexedStack com a NavigationBar: três separadores, cada um com a sua pilha (passo 3).
O branch passo-3 é o estado final: compara o teu código com ele.
Até segunda
Próxima aula
Segunda, 12 de outubro, das 16:00 às 18:00: acessibilidade, internacionalização e o mini-projeto individual.
O mini-projeto parte do que fizemos até hoje: grelha, ecrã de detalhe e navegação.
O estado final de hoje fica no branch passo-3: git stash -u e depois git checkout passo-3.