PDM · DAW 2

Aula 10 · Semana 6 · Segunda, 12 de outubro de 2026 · 16:00 às 18:00

Navegação a fundo e mini-projeto

Primeira parte: revemos a navegação da aula 9 com calma. As rotas como uma árvore, push e go, e o StatefulShellRoute peça a peça. Segunda parte: o primeiro mini-projeto avaliado, individual. Acrescentas à Tempo Açores um separador novo, Trilhos, com lista e detalhe.

Repositório: pdm-aula-10

No fim desta aula consegues

As rotas são uma árvore

GoRouter StatefulShellRoute.indexedStack a moldura: Scaffold com a NavigationBar branch 0 · Previsão branch 1 · Avisos branch 2 · Trilhos branch 3 · Definições '/' ForecastScreen '/alerts' AlertsScreen '/trails' TrailsScreen '/settings' SettingsScreen 'island/:id' IslandScreen ':id' TrailScreen rota filha: abre por cima da mãe, no mesmo separador, com a seta de voltar
A tracejado, o que vais acrescentar hoje no mini-projeto. Uma rota filha não começa por /: o caminho dela junta-se ao da mãe.

Três peças, uma pergunta cada

PeçaResponde aNa Tempo Açores
GoRouteQue ecrã aparece neste caminho?/alerts abre o AlertsScreen
StatefulShellRouteO que fica à volta de todos os separadores?Um Scaffold só com a NavigationBar
StatefulShellBranchQue rotas pertencem a este separador?Previsão: / e island/:id
body: shell o ecrã do separador atual, com o seu Scaffold e AppBar ForecastScreen, IslandScreen… NavigationBar muda quando trocas de separador ou abres um ecrã desenhada uma vez, pelo builder do shell
O shell é só o miolo; a barra é desenhada à volta dele.
Pergunta à turma Abres o Pico, vais a Avisos e voltas a Previsão. Porque é que o Pico ainda lá está, e não a grelha?

O código da moldura

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',
        ),
        NavigationDestination(
          icon: Icon(Icons.warning_amber),
          label: 'Avisos',
        ),
        NavigationDestination(
          icon: Icon(Icons.settings_outlined),
          label: 'Definições',
        ),
      ],
    ),
  ),
  branches: [
    // um StatefulShellBranch por separador, pela mesma ordem
  ],
),
Pergunta à turma Acrescentas uma NavigationDestination e esqueces-te do branch. O que acontece quando tocas no separador novo?

push ou go?

Animação: na lista de Trilhos, toque na Lagoa do Fogo; o detalhe abre por cima da lista, no mesmo separador; toque na seta de voltar e a lista volta.

context.push(...)
Põe por cima da pilha onde estás. O separador não muda; a seta de voltar regressa à lista.

Animação: na ilha do Faial, no separador Previsão, toque no trilho Volta à Caldeira do Faial; a barra passa para Trilhos com o trilho aberto; toque em Previsão e a ilha do Faial continua lá.

context.go(...)
Vai para a morada, como no URL. A barra salta para Trilhos; Previsão fica como estava, com o Faial aberto.

push pensa em pilhas; go pensa em moradas. Com go, o go_router monta a pilha do caminho: /trails por baixo, /trails/caldeira-faial por cima.

Usa quandoExemplo
context.pushAbres um detalhe por cima do ecrã onde estásDa grelha para uma ilha; da lista para um trilho
context.goVais para outro sítio da app, que tem a sua pilhaDa ilha para um trilho no separador Trilhos; um link de uma notificação

Receita: um separador novo

  1. O ecrã. Um widget com Scaffold e AppBar, como os outros separadores.
  2. O destino na barra. Uma NavigationDestination com ícone e etiqueta, na posição em que o queres ver.
  3. O branch. Um StatefulShellBranch na mesma posição, com a GoRoute do caminho do separador.
  4. O detalhe, se houver. Uma rota filha dentro dessa GoRoute, com o parâmetro: ':id', sem / no início.
  5. O ecrã de detalhe recebe o id, procura o objeto nos dados e trata o caso de não o encontrar.
StatefulShellBranch(
  routes: [
    GoRoute(
      path: '/alerts',
      builder: (context, state) => const AlertsScreen(),
    ),
  ],
),

É isto que vais fazer no mini-projeto, com Trilhos. O separador Previsão, com / e island/:id, é o modelo completo: está no mesmo ficheiro.

Mini-projeto: Trilhos

Já vem feito

  • A Tempo Açores com os três separadores: Previsão, Avisos e Definições.
  • lib/data.dart com 13 trilhos, a classe Trail e o enum Difficulty.
  • lib/trails_screen.dart e lib/trail_screen.dart, vazios.

Fazes tu

  • Tarefa 1: o separador na barra e as rotas.
  • Tarefa 2: a lista de trilhos.
  • Tarefa 3: o detalhe de um trilho.
  • Tarefa 4: os trilhos de cada ilha.

Criar o teu repositório

  1. Em github.com/Tiago-Rocha/pdm-aula-10, com a sessão iniciada: botão verde Use this template e depois Create a new repository. Não é o Fork.
Página do pdm-aula-10 no GitHub com o menu Use this template aberto e a opção Create a new repository assinalada.
  1. Nome pdm-miniprojeto, visibilidade Private, e Create repository.
Formulário Create a new repository com o template pdm-aula-10, o nome pdm-miniprojeto, a visibilidade Private e o botão Create repository assinalados.
  1. No teu repositório novo, Settings, depois Collaborators, depois Add people: escreve Tiago-Rocha.
Definições de um repositório no GitHub com o separador Settings, a secção Collaborators e o botão Add people assinalados.

Clonar e primeiro commit

  1. No teu repositório, botão verde Code, separador HTTPS: copia o URL. Tem de ter o teu utilizador e pdm-miniprojeto, não pdm-aula-10.
Menu Code de um repositório no GitHub com o separador HTTPS e o campo do URL assinalados.
cd C:\dev
git clone https://github.com/<utilizador>/pdm-miniprojeto.git
cd pdm-miniprojeto
flutter pub get
  1. Na linha do git clone, cola o URL que copiaste. Abre a pasta pdm-miniprojeto no VS Code (File, Open Folder), escolhe o Android ou o Chrome, e F5.
  2. Primeiro commit, todos juntos: no README.md, na primeira linha, **Student:** Maria Silva Medeiros · 2024123 com o teu nome e número. Commit com a mensagem Nome do aluno no README e Sync Changes. No GitHub, o nome aparece no topo.

Na app: a Tempo Açores com três separadores: Previsão, Avisos e Definições. O separador Trilhos ainda não existe; é a Tarefa 1.

Guardar e entregar

Em cada tarefa acabada

  1. Vista Source Control (Ctrl+Shift+G).
  2. Mensagem que diga o que mudou: Separador Trilhos com as rotas.
  3. Commit e depois Sync Changes.
  4. Atualiza a página do teu repositório no GitHub: o commit está lá?

Para entregar

  1. Último commit, com Sync, antes de quarta às 16:00.
  2. No README: o que fizeste, o que ficou por fazer, e duas capturas (lista e detalhe).
  3. Mais nada. O docente vê o teu repositório pelo convite de colaborador: a entrega é o repositório.
O que conta O que está no GitHub à hora do prazo. Um commit sem Sync só existe no teu portátil, e para a avaliação não existe.

Os dados: lib/data.dart

enum Difficulty {
  easy('Fácil', Colors.green),
  medium('Média', Colors.orange),
  hard('Difícil', Colors.red);
  // ...
}

class Trail {
  final String id; // used in the route: /trails/lagoa-do-fogo
  final String name;
  final String islandId; // the id of an Island: 'sao-miguel', 'pico', ...
  final double distanceKm;
  final String duration; // '3 h 30'
  final Difficulty difficulty;
  final String description;

  /// The island of this trail, with the name and the photo.
  Island get island => islands.firstWhere((i) => i.id == islandId);
  // e distance: '10,5 km'
}

const trails = [
  // 13 trilhos, pelo menos um em cada ilha
];

Como deve ficar

O separador Trilhos: lista dos trilhos, cada um com a foto da ilha num círculo, o nome, a ilha e a distância, e a dificuldade à direita. 2 1

Trilhos (Tarefas 1 e 2): um ListTile por trilho. À esquerda a foto da ilha num CircleAvatar; o nome; por baixo "Ilha · distância"; à direita a dificuldade, com bolinha de cor e texto.

O detalhe da Montanha do Pico: seta para voltar, foto, ilha, distância, duração, dificuldade e descrição. 3

Detalhe (Tarefa 3): o nome na AppBar, com a seta de voltar. A foto da ilha em cima; por baixo, a ilha, uma linha com a distância, a duração e a dificuldade, e a descrição.

A ilha do Faial no separador Previsão: foto, os cinco dias e, por baixo, o título Trilhos com os dois trilhos da ilha. 4

Ilha (Tarefa 4): por baixo dos dias, o título "Trilhos" e as mesmas linhas da lista, só com os trilhos dessa ilha.

Não tem de ficar igual ao pixel, mas tem de ter estes elementos. Dica: a linha da lista e a linha da ilha são iguais. Faz um widget para a linha e usa-o nos dois sítios.

Tarefas 1 e 2: o separador e a lista

Tarefa 1 Em lib/router.dart: uma NavigationDestination Trilhos (Icons.hiking) e o seu StatefulShellBranch, na mesma posição. A GoRoute '/trails' abre o TrailsScreen; dentro dela, a rota filha ':id' abre o TrailScreen.

Na app: quatro separadores na barra. Trilhos abre um ecrã que diz "Ainda sem trilhos". No Chrome, o URL é /trails.

Tarefa 2 Em lib/trails_screen.dart: um ListView.builder com um ListTile por trilho. Foto da ilha (CircleAvatar com AssetImage), nome, ilha e distância, e a dificuldade. onTap: context.push('/trails/${...}').

Na app: os 13 trilhos, de São Miguel ao Corvo. Tocar num abre um ecrã com o id no título; a seta de voltar volta à lista.

Acabaste uma tarefa? Commit e Sync Changes, antes de passares à seguinte.

Tarefas 3 e 4: o detalhe e a ilha

Tarefa 3 Em lib/trail_screen.dart: procura o trilho com este id em trails, como o IslandScreen faz com as ilhas. Mostra a foto da ilha, o nome, a distância, a duração, a dificuldade e a descrição. Se o id não existir, "Trilho não encontrado".

Na app: a Montanha do Pico com foto, 7,6 km, 7 h, Difícil e a descrição. No Chrome, /trails/xyz mostra "Trilho não encontrado".

Tarefa 4 Em lib/island_screen.dart, por baixo dos dias: um título "Trilhos" e os trilhos dessa ilha (trails.where(...) com o islandId). Tocar num: context.go('/trails/${...}').

Na app: no Faial aparecem dois trilhos. Tocar na Caldeira muda para o separador Trilhos, com a Caldeira aberta. Voltas a Previsão: o Faial continua lá.

final island = islands.where((i) => i.id == id).firstOrNull;
if (island == null) {
  // an id that does not exist, for example a mistyped link
  return Scaffold(
    appBar: AppBar(),
    body: const Center(child: Text('Ilha não encontrada')),
  );
}

Extras, para quem acabar

Os extras não contam para a nota. Primeiro as quatro tarefas.

Entrega e avaliação

Hoje, antes de saíres Repositório criado a partir do template, docente convidado como colaborador, primeiro commit com o teu nome, e as tarefas começados.

Vale 2 valores (10 % da nota final):

TarefaValor
1: o separador Trilhos, com as rotas0,5
2: a lista de trilhos0,5
3: o detalhe, com o caso "não encontrado"0,5
4: os trilhos na ilha, com context.go0,5

Se a app não compilar, a nota é 0. Podes usar IA, mas o docente pode pedir-te numa aula para explicares o teu código. Código que não sabes explicar não conta.

Até amanhã

Próxima aula

  • Terça, 13 de outubro, das 8:30 às 10:30: qualidade de código, lints, organização e git. Traz o mini-projeto como estiver: vamos trabalhar nele.
  • Entrega do mini-projeto: quarta, 14 de outubro, às 16:00, no GitHub.

Para quem quiser ler mais