Capítulo 04: Layouts Fundamentais (Container, Column, Row, Stack, Expanded)

Especialização Multiplataforma com Flutter • Flutter 3.x & Dart 3


🗺️ Mapa Conceitual do Algoritmo de Box Constraints no Flutter

flowchart TD
    PARENT["Widget Pai (Ex: Scaffold ou Column)"] -->|"1. Repassa Constraints (minWidth, maxWidth, minHeight, maxHeight)"| CHILD["Widget Filho (Ex: Container ou Text)"]
    CHILD -->|"2. Calcula e Devolve seu Tamanho Próprio (Size: width, height)"| PARENT
    PARENT -->|"3. Determina a Posição Cartesiana Final (Offset: dx, dy)"| RENDER["RenderObject (Gravação no Canvas da GPU)"]

    subgraph CONSTRAINT_TYPES["Tipos de Restrições (BoxConstraints)"]
        TIGHT["Tight: minWidth == maxWidth (Tamanho Forçado e Fixo)"]
        LOOSE["Loose: minWidth == 0 && maxWidth < infinity (Tamanho Flexível)"]
        UNBOUNDED["Unbounded: maxHeight == infinity (Cuidado com Column/ListView!)"]
    end

    CHILD -.-> CONSTRAINT_TYPES

    subgraph OVERFLOW_GUARD["Prevenção de RenderFlex Overflow"]
        ROW_COL["Row / Column com Filhos Longos"] -->|"Sem Limites"| YELLOW_BLACK["Erro: A RenderFlex overflowed by X pixels"]
        ROW_COL -->|"Envolvido em Expanded / Flexible"| SAFE["Adaptação Flexível ao Espaço Restante"]
    end

    style PARENT fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style CHILD fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style RENDER fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style YELLOW_BLACK fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style SAFE fill:#b3e5fc,stroke:#0288d1,stroke-width:2px

🏛️ 1. A Regra de Ouro do Layout no Flutter

Quase todas as dificuldades que desenvolvedores encontram ao compor layouts no Flutter derivam da incompreensão da regra canônica de três passos formulada pelo time de engenharia da Google:

“Constraints go down. Sizes go up. Parent sets position.”

  1. Constraints go down (As restrições descem): O widget pai dita os limites geométricos mínimos e máximos (BoxConstraints) que o widget filho pode assumir. Um filho não pode, por vontade própria, ter um tamanho maior que o maxWidth ou menor que o minWidth imposto pelo seu pai.
  2. Sizes go up (Os tamanhos sobem): O widget filho avalia seu próprio conteúdo interno respeitando as restrições recebidas e decide seu tamanho exato (Size(largura, altura)), informando de volta ao pai.
  3. Parent sets position (O pai define a posição): O widget pai recebe as dimensões dos filhos e decide onde posicionar cada um no plano cartesiano de coordenadas $(X, Y)$ da tela.

📊 2. Comparativo de Contêineres e Gerenciadores de Espaço

Widget Eixo Principal Comportamento com Espaço Restante Resolução de Overflow
Row Horizontal (x) Encolhe para caber o tamanho intrínseco dos filhos Dispara erro amarelo/preto se os filhos excederem a largura da tela
Column Vertical (y) Encolhe para caber o tamanho intrínseco dos filhos Dispara erro se a lista de filhos for maior que a altura do display
Expanded O mesmo do pai Força o filho a preencher todo o espaço restante disponível Divide proporcionalmente o espaço restante usando flex: int
Flexible O mesmo do pai Permite que o filho seja menor que o espaço restante (loose) Impede overflow sem forçar esticamento desnecessário
Stack Eixo $Z$ (Profundidade) Sobrepõe widgets uns sobre os outros em camadas Controlado por Positioned(top, left, right, bottom)
Wrap Horizontal/Vertical Quebra automaticamente para a próxima linha Evita overflow em listas dinâmicas de tags ou botões

💻 3. Laboratório Prático: Layout Responsivo com LayoutBuilder e Flexbox

O código a seguir apresenta a construção de um dashboard corporativo adaptativo que reorganiza dinamicamente métricas em colunas ou linhas com base na largura disponível do dispositivo:

// Arquivo: lib/painel_layout_responsivo.dart
import 'package:flutter/material.dart';

class PainelMetricasResponsivo extends StatelessWidget {
  const PainelMetricasResponsivo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Layouts Fundamentais & Flexbox')),
      body: LayoutBuilder(
        builder: (context, constraints) {
          // Se a largura for superior a 600px, adota visualização multi-coluna (Tablet/Web)
          final isDispositivoAmplo = constraints.maxWidth > 600;

          return SingleChildScrollView(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const Text(
                  'Indicadores Financeiros Consolidados',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 16),

                // Composição Flexível Condicional
                if (isDispositivoAmplo)
                  const Row(
                    children: [
                      Expanded(child: CartaoMetrica(titulo: 'Receita Bruta', valor: 'R\$ 840.000', cor: Colors.teal)),
                      SizedBox(width: 12),
                      Expanded(child: CartaoMetrica(titulo: 'Custos Operacionais', valor: 'R\$ 310.000', cor: Colors.orange)),
                      SizedBox(width: 12),
                      Expanded(child: CartaoMetrica(titulo: 'Lucro Líquido', valor: 'R\$ 530.000', cor: Colors.green)),
                    ],
                  )
                else
                  const Column(
                    children: [
                      CartaoMetrica(titulo: 'Receita Bruta', valor: 'R\$ 840.000', cor: Colors.teal),
                      SizedBox(height: 10),
                      CartaoMetrica(titulo: 'Custos Operacionais', valor: 'R\$ 310.000', cor: Colors.orange),
                      SizedBox(height: 10),
                      CartaoMetrica(titulo: 'Lucro Líquido', valor: 'R\$ 530.000', cor: Colors.green),
                    ],
                  ),

                const SizedBox(height: 24),
                const Text(
                  'Categorias com Wrap (Sem Risco de Overflow)',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 12),

                // Wrap substitui Row quando a quantidade de itens é dinâmica
                Wrap(
                  spacing: 8.0,
                  runSpacing: 8.0,
                  children: const [
                    Chip(label: Text('Infraestrutura Cloud')),
                    Chip(label: Text('Folha de Pagamento')),
                    Chip(label: Text('Licenciamento SaaS')),
                    Chip(label: Text('P&D Mobile')),
                    Chip(label: Text('Marketing Digital')),
                    Chip(label: Text('Segurança da Informação')),
                  ],
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

class CartaoMetrica extends StatelessWidget {
  final String titulo;
  final String valor;
  final MaterialColor cor;

  const CartaoMetrica({
    super.key,
    required this.titulo,
    required this.valor,
    required this.cor,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: cor.shade50,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: cor.shade200),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(titulo, style: TextStyle(color: cor.shade900, fontWeight: FontWeight.w600)),
          const SizedBox(height: 6),
          Text(valor, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: cor.shade800)),
        ],
      ),
    );
  }
}

⚠️ 4. Armadilhas Comuns no Manejo de Layouts

  1. Colocar ListView dentro de Column sem Restrição: Um ListView tenta ter altura infinita por padrão. Uma Column dá altura ilimitada aos seus filhos. O resultado é o erro fatal: Vertical viewport was given unbounded height. Solução: envolva o ListView em um Expanded ou defina shrinkWrap: true e physics: NeverScrollableScrollPhysics().
  2. Usar Expanded fora de Row, Column ou Flex: O widget Expanded exige estritamente um pai flexível. Utilizá-lo dentro de um Container ou Stack dispara uma exceção imediata em tempo de execução.
  3. Ignorar as Margens de Segurança do Sistema (SafeArea): Em iPhones com Dynamic Island / Notch ou dispositivos Android modernos com barras de navegação transparentes, desenhar sem SafeArea fará botões e textos ficarem inacessíveis ou cortados atrás de elementos de hardware do sistema.