PDM · DAW 2

Aula 06 · Semana 4 · Terça, 29 de setembro de 2026 · 08:30 às 10:30

UX mobile e layouts

Teoria: o que os utilizadores esperam de uma app móvel: gestos, tamanhos de toque, padrões de navegação e diferenças entre Android e iOS. Prática: o modelo de layout do Flutter e os widgets Row, Column, Expanded, Flexible e Stack. As listas, as grelhas e os widgets próprios ficam para amanhã.

Repositório: pdm-aula-06

No fim desta aula consegues

Material 3 vs Human Interface Guidelines

Material 3 (Google)HIG (Apple)
Navegação principalNavigation bar em baixo com 3 a 5 destinos, ou navigation drawerTab bar em baixo; sem drawer
VoltarBotão ou gesto do sistema, de qualquer lado; predictive back desde o Android 14Gesto de deslizar a partir da margem esquerda e seta na barra
BotõesFilled, tonal, outlined, text; FAB para a ação principalBotões de texto e de sistema; sem FAB
Diálogos e folhasDiálogo centrado; bottom sheet modalAlerta centrado; action sheet e folhas que deslizam de baixo
TipografiaRoboto; escala com 15 estilos (display, headline, title, body, label)SF Pro; escala Dynamic Type controlada pelo utilizador

Quando seguir a plataforma

Para os testes: Material 3 é o sistema de design do Google e o padrão do Flutter; as HIG são as regras da Apple. Uma app pode ter design próprio, mas deve respeitar os gestos e convenções do sistema.

Gestos que os utilizadores esperam

GestoO que espera que aconteçaSe o ignoras
TapAção imediata com feedback visual (ripple, mudança de cor)Toca duas vezes, dispara duas ações
Long pressMenu de contexto, seleção, arrastarNão encontra a opção "apagar" e vai às definições
Swipe horizontalVoltar (margem esquerda no iOS), mudar de tab, apagar item de listaFica preso num ecrã; sai da app
Pull-to-refreshAtualizar a lista, com indicadorReabre a app para ver dados novos
Pinch e duplo toqueZoom em imagens e mapasNão consegue ler o que está na imagem

A regra é não inventar. Um gesto que só a tua app usa é um gesto que ninguém descobre.

Toque, polegar e legibilidade

Para os testes: alvo mínimo de 48 dp, contraste mínimo de 4,5:1, ações frequentes ao alcance do polegar.

Padrões de navegação

Na quarta, 7 de outubro, implementamos os três primeiros com go_router. Hoje só o layout.

Estados vazios, loading e erro fazem parte do design

Hoje desenhas o estado com dados. Numa aula mais à frente modelamos os quatro estados com uma sealed class e testamo-los com o modo caos da API.

Layout na Tempo Açores

git clone https://github.com/Tiago-Rocha/pdm-aula-06.git
cd pdm-aula-06
flutter pub get

Um único projeto na raiz: abre a pasta pdm-aula-06 no VS Code. É a app da aula 5 já terminada, mais o ecrã de previsão em lib/forecast_screen.dart, onde vais trabalhar. lib/data.dart tem os dados estáticos e não se altera.

A prática tem quatro passos, um slide cada: o primeiro hoje, os outros amanhã. Cada slide de passo começa com três comandos, um por linha, que te põem no ponto de partida desse passo, quer tenhas acabado o anterior quer não. Depois deles, hot restart no VS Code: o botão ↻ da barra de depuração. Depois mostra o código que vais usar, e o que tens de fazer. O branch passo-N é o passo N feito; passo-4 é o estado final.

  1. Abre a pasta no VS Code. Ctrl+Shift+P → Flutter: Select Device → o teu Android. Depois F5: corre a configuração "Tempo Açores (debug)" que já vem no projeto. Olha para o telemóvel: por baixo da temperatura há riscas amarelas e pretas. É um overflow, e é o passo 1.
  2. Corre flutter test: quatro testes vermelhos, um por passo.

1. Constraints go down, sizes go up, parent sets position

Pai (por ex. Center) dá limites, recebe um tamanho, posiciona Filho (Container) escolhe 100 × 100 dentro dos limites ↓ constraints: limites min/max ↑ size: o filho escolhe o pai posiciona o filho: (x, y) constraints go down min e max de largura/altura sizes go up o filho escolhe o tamanho parent sets position o filho não sabe onde está
Três regras num só desenho: os limites descem, o tamanho sobe, e só o pai sabe onde o filho fica.
// 1. O pai diz ao filho os limites (mínimo e máximo de largura e altura)
// 2. O filho escolhe o seu tamanho dentro desses limites
// 3. O pai posiciona o filho

Scaffold(
  body: Container(color: Colors.teal),          // ocupa o ecrã todo: o pai obriga
)

Scaffold(
  body: Center(
    child: Container(color: Colors.teal, width: 100, height: 100),   // o Center deixa escolher
  ),
)
Pergunta à turma Se trocar o Center por Align(alignment: Alignment.topRight), onde fica o quadrado? E se tirar o width e o height ao Container que está dentro do Center?

Para os testes: as constraints descem, os tamanhos sobem, o pai posiciona. Um filho nunca escolhe onde fica.

Analogia O pai diz ao filho "podes ter entre 0 e 300 de largura": são as constraints a descer. O filho responde "fico com 200": é o tamanho a subir. O pai decide então onde o põe. Um filho nunca escolhe a sua posição e nunca pode ser maior do que o pai permite.

2. Row, Column, Expanded e Flexible

Column(
  crossAxisAlignment: CrossAxisAlignment.start,   // alinha à esquerda
  children: [
    Text('Ponta Delgada', style: textTheme.headlineSmall),
    const Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,   // um a cada ponta
      children: [Text('Hoje'), Text('21 °C')],
    ),
    const SizedBox(height: 12),
    Row(
      children: [
        Expanded(flex: 2, child: Container(height: 40, color: Colors.teal)),    // 2 partes
        const SizedBox(width: 8),
        Expanded(child: Container(height: 40, color: Colors.orange)),           // 1 parte
        const SizedBox(width: 8),
        Flexible(child: Container(height: 40, width: 300, color: Colors.grey)), // até 1 parte
      ],
    ),
  ],
)
Pergunta à turma O que muda se trocar spaceBetween por spaceEvenly? Se o Container cinzento tivesse Expanded em vez de Flexible, que largura ficava? Como fazia para o teal ocupar três partes e o laranja uma?

2. Row e Column: o que interessa

Passo 1: corrigir o overflow

git stash -u
git checkout main
flutter pub get
// Está assim: três larguras fixas, 3 × 160 = 480, e o telemóvel tem 360
Row(
  children: [
    SizedBox(width: 160, child: _Measure('Mín', '${today.tMin.round()} °C')),
    SizedBox(width: 160, child: _Measure('Máx', '${today.tMax.round()} °C')),
    SizedBox(width: 160, child: _Measure('Chuva', '${today.rainChance} %')),
  ],
)

// Pista: Expanded dá a cada filho uma parte igual do que sobra
Expanded(child: _Measure('Mín', '${today.tMin.round()} °C')),
Faz Antes de tocar no código, diz o que vai acontecer quando trocares os três SizedBox por Expanded. Depois faz e guarda o ficheiro: o hot reload é automático.

No telemóvel: as riscas amarelas e pretas desaparecem e os três valores ficam igualmente espaçados. No terminal: flutter test, o teste Passo 1 fica verde.

3. Stack e Positioned: imagem com texto por cima

SizedBox(
  height: 200,
  child: Stack(
    fit: StackFit.expand,                          // os filhos sem posição ocupam tudo
    children: [
      Image.asset('assets/img/sao_miguel.jpg', fit: BoxFit.cover),
      const DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [Colors.transparent, Colors.black54],   // escurece para o texto se ler
          ),
        ),
      ),
      Positioned(
        left: 16,
        bottom: 16,
        child: Text('São Miguel', style: textTheme.headlineMedium?.copyWith(color: Colors.white)),
      ),
    ],
  ),
)
Pergunta à turma Se tirar o fit: StackFit.expand, o que acontece à imagem? Como punha o texto em cima à direita? E se trocasse a ordem da imagem e do gradiente na lista?

Até amanhã

Para fazer

  • Se o teste Passo 1 ainda não está verde, acaba o passo 1. Depois compara com git stash -u e depois git checkout passo-1.
  • Lê Understanding constraints. Tem 29 exemplos; corre pelo menos os primeiros dez no DartPad.
  • Amanhã às 16:00 continuamos no mesmo repositório, a partir do passo 2: o nome e o chip sobre a imagem, a fila dos dias e a grelha das ilhas.

Para ler