PDM · DAW 2

Aula 07 · Semana 4 · Quarta, 30 de setembro de 2026 · 16:00 às 18:00

Layouts (continuação): Stack, listas e widgets próprios

Continuação da aula de ontem, no mesmo repositório. Prática: o nome e o chip sobre a imagem com Stack e Positioned, a fila dos dias com ListView, a grelha das ilhas com GridView, e dois widgets próprios, o DayChip e o IslandCard.

Repositório: pdm-aula-06

No fim desta aula consegues

Retomar: onde ficámos ontem

git stash -u
git checkout passo-1
flutter pub get
  1. Abre a pasta pdm-aula-06 no VS Code e corre os três comandos de cima no terminal, um de cada vez: ficas no passo 1 feito, quer o tenhas acabado ontem quer não.
  2. Ctrl+Shift+P → Flutter: Select Device → o teu Android. Se o Android ainda não corre no teu portátil, escolhe Chrome (ou Edge): os passos de hoje funcionam igual no browser. Depois F5.
  3. Confirma o que vês: o ecrã de previsão com a foto de São Miguel no topo, ainda sem o nome da ilha por cima.

Não estiveste ontem? Antes dos comandos de cima: git clone https://github.com/Tiago-Rocha/pdm-aula-06.git e cd pdm-aula-06.

O Android não arranca? Quase sempre é o Java que o Gradle está a usar: vê os problemas frequentes do guia de instalação e chama o docente. Entretanto segue a aula no Chrome.

3. Stack e Positioned: o cabeçalho da ilha revisão

/// Island photo with a gradient so that text on top of it stays readable.
class IslandHeader extends StatelessWidget {
  const IslandHeader({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 200,
      child: Stack(
        fit: StackFit.expand, // children without a position fill the box
        children: [
          Image.asset(currentIsland.image, fit: BoxFit.cover),
          const DecoratedBox(
            decoration: BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
                colors: [Colors.transparent, Colors.black54],
              ),
            ),
          ),
          // TODO 2: add the island name at the bottom left (Positioned) and
          // a Chip with the weather description at the top right.
        ],
      ),
    );
  }
}
Pergunta à turma Se tirar o fit: StackFit.expand, o que acontece à imagem? E se trocasse a ordem da imagem e do gradiente na lista? Em que sítio da lista tem de entrar o nome da ilha para ficar por cima dos dois?

Passo 2: nome e chip sobre a imagem

git stash -u
git checkout passo-1
flutter pub get
// Pista: um filho com Positioned fica onde disseres; os outros ficam por baixo
Positioned(
  left: 16,
  bottom: 16,
  child: Text(
    '...',
    style: textTheme.headlineMedium?.copyWith(color: Colors.white),
  ),
),
Positioned(
  top: 12,
  right: 12,
  child: Chip(label: Text('...')),
),

// Os dados que precisas, em lib/data.dart
currentIsland.name                   // 'São Miguel'
forecasts.first.type.description     // 'Parcialmente nublado'
Faz Troca o comentário TODO 2 pelos dois Positioned, com o nome da ilha e a descrição do tempo no lugar dos '...'. Repara que textTheme ainda não existe nesse build: cria-o como no CurrentConditions.

Na app: o nome da ilha em branco em baixo à esquerda, por cima do gradiente, e o chip em cima à direita.

4. ListView: as duas listas do ecrã

body: ListView(
  children: const [
    IslandHeader(),
    Padding(padding: EdgeInsets.all(16), child: CurrentConditions()),
    DaysRow(),
    Padding(padding: EdgeInsets.all(16), child: DetailCard()),
    IslandsGrid(),
  ],
),
return SizedBox(
  height: 96,
  child: ListView.separated(
    scrollDirection: Axis.horizontal,
    padding: const EdgeInsets.symmetric(horizontal: 16),
    itemCount: forecasts.length,
    separatorBuilder: (_, _) => const SizedBox(width: 8),
    // TODO 3: create lib/day_chip.dart with a DayChip widget (a Card with
    // the weekday, the icon and the max temperature) and use it here:
    // itemBuilder: (_, i) => DayChip(forecast: forecasts[i]),
    itemBuilder: (_, i) => SizedBox(
      width: 72,
      child: Card(child: Center(child: Text(forecasts[i].weekday))),
    ),
  ),
);
Pergunta à turma Se a lista tivesse mil dias, qual das duas congelava a app? Porque é que a DaysRow embrulha a lista num SizedBox com altura? Para que serve o separatorBuilder?

4. GridView: a grelha das ilhas

return GridView.extent(
  maxCrossAxisExtent: 160, // as many columns as fit, each up to 160 dp
  padding: const EdgeInsets.all(16),
  mainAxisSpacing: 8,
  crossAxisSpacing: 8,
  shrinkWrap: true, // takes only the height it needs, inside the ListView
  physics: const NeverScrollableScrollPhysics(), // the ListView scrolls
  children: [
    for (final i in islands)
      Card(
        clipBehavior: Clip.antiAlias,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Expanded(child: Image.asset(i.image, fit: BoxFit.cover)),
            Padding(
              padding: const EdgeInsets.all(8),
              child: Text(i.name, textAlign: TextAlign.center),
            ),
          ],
        ),
      ),
  ],
);
Pergunta à turma Quantas colunas tem a grelha num telemóvel com 360 dp de largura? E numa janela de browser com 1200? Porque é que esta grelha precisa de shrinkWrap e de não fazer scroll?

4. Listas: o que interessa

Para os testes: ListView.builder constrói os itens sob demanda; ListView com children constrói todos.

Passo 3: escrever o DayChip

git stash -u
git checkout passo-2
flutter pub get
import 'package:flutter/material.dart';

import 'data.dart';

class DayChip extends StatelessWidget {
  const DayChip({super.key, required this.forecast});

  final DayForecast forecast;

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return SizedBox(
      width: 72,
      child: Card(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Text com forecast.weekday (textTheme.labelLarge)
            // Icon com forecast.type.icon (size: 24)
            // Text com '${forecast.tMax.round()}°' (textTheme.titleMedium)
            // e um SizedBox(height: 4) entre cada dois
          ],
        ),
      ),
    );
  }
}
import 'day_chip.dart';

itemBuilder: (_, i) => DayChip(forecast: forecasts[i]),
Faz Cria o ficheiro, troca os quatro comentários pelos filhos da Column, importa-o em forecast_screen.dart e troca o itemBuilder provisório da DaysRow pelo de cima. É a mesma forma do LocationCard de segunda: campos final, construtor const e um build. A grelha das ilhas em IslandsGrid já está feita.

Na app: cinco cartões na fila horizontal, cada um com o dia, o ícone e a máxima.

Passo 4: extrair o IslandCard

git stash -u
git checkout passo-3
flutter pub get
children: [
  for (final i in islands)
    Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(child: Image.asset(i.image, fit: BoxFit.cover)),
          Padding(
            padding: const EdgeInsets.all(8),
            child: Text(i.name, textAlign: TextAlign.center),
          ),
        ],
      ),
    ),
],
import 'island_card.dart';

children: [for (final i in islands) IslandCard(island: i)],
Faz Cria lib/island_card.dart com IslandCard(island: ...), como fizeste com o DayChip: um campo final Island island, construtor const, e o Card de cima no build. Importa-o e usa-o na grelha. O ecrã não muda nada: é isso mesmo.

Na app: a grelha igual ao que estava. No fim compara com git stash -u e depois git checkout passo-4.

Onde ficámos

  1. IslandHeader: Stack com a imagem, o gradiente, o nome e o chip (passo 2).
  2. CurrentConditions: nome e temperatura numa Row, mínima, máxima e chuva com Expanded (passo 1).
  3. DaysRow: ListView.separated horizontal com cinco DayChip (passo 3).
  4. DetailCard e IslandsGrid: já vinham feitos. Um GridView dentro de um ListView precisa de shrinkWrap e de não fazer scroll ele próprio.
  5. Tudo dentro de um ListView vertical. No passo 4, o Card da grelha passa a ser um IslandCard com ficheiro próprio.

O branch passo-4 é o estado final. Compara o teu código com ele. Ainda não há toque nem estado: isso é na terça.

Até terça

Próxima aula

  • Segunda, 5 de outubro, é feriado.
  • Terça, 6 de outubro, das 08:30 às 11:30: estado, formulários e pesquisa. O ecrã de hoje vai ganhar vida.
  • O estado final de hoje fica no branch passo-4: git stash -u e depois git checkout passo-4.

Para quem quiser ler mais