Flutter: Sistema de Layout

O layout do Flutter é como uma linha de montagem — o pai passa restrições, o filho informa o tamanho, o pai determina a posição.

📋 Pré-requisitos: Você precisa ter concluído o seguinte primeiro

1. O Que Você Vai Aprender


2. Uma História de um Designer de UI em Transição para o Desenvolvimento

(1) O Problema: O Pesadelo das Faixas Amarelo-Pretas de Overflow

Bob recebeu as especificações de UI do ShopApp — os cartões de produto precisavam de uma imagem à esquerda e informações à direita, com uma linha de preço na parte inferior. Ele aninhou um Row dentro de um Column, só para ver avisos de overflow com faixas amarelo-pretas por toda a tela. Após ajustar padding e margin por muito tempo, ou o texto era truncado ou as imagens distorcidas. 1000 produtos, 10 tamanhos de tela, problemas de layout em todo lugar.

(2) A Solução da Passagem de Restrições

O layout do Flutter tem uma regra central: restrições passam para baixo, tamanhos informam para cima, posições são determinadas pelo pai. Entendendo essa regra, Bob podia prever o comportamento de cada Widget.

DART
import 'package:flutter/material.dart';

// Parent gives tight constraints: exactly 200x100
SizedBox(
  width: 200,
  height: 100,
  child: Text('I will be 200x100'), // Text forced to 200x100
)

// Parent gives loose constraints: 0 to maxWidth
Row(
  children: [
    Expanded(child: Text('I take remaining space')),
    SizedBox(width: 80, child: Text('I am 80 wide')),
  ],
)
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

(3) O Resultado: Acertar o Layout de Primeira

Após dominar a passagem de restrições, a precisão de layout de primeira tentativa de Bob melhorou de 30% para 90%, e os avisos de overflow desapareceram completamente.


3. Modelo de Caixa e Passagem de Restrições

100%
graph TD
    subgraph Constraint Passing
        P[Parent Constraints] --> C[Child Layout]
        C --> S[Child Size]
        S --> P
    end
    subgraph Common Layouts
        R[Row] --> EX1[Expanded]
        CO[Column] --> EX2[Flexible]
        ST[Stack] --> POS[Positioned]
    end
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

(1) Tipos de Restrição

Tipo Descrição Exemplo
Tight min = max, tamanho fixo SizedBox(width: 100)
Loose min = 0, max = máximo do pai Filho dentro de Center
Unbounded max = infinito Column dentro de ListView
Bounded Tanto min quanto max são finitos Column dentro de uma Page

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Demonstração de passagem de restrições

DART
import 'package:flutter/material.dart';

// Demo: constraint passing in action
class ConstraintDemo extends StatelessWidget {
  const ConstraintDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // Tight constraint: exactly 200 height
          Container(
            height: 200,
            color: Colors.blue[100],
            child: const Center(child: Text('Height: 200 (tight)')),
          ),
          // Loose constraint: takes remaining space
          Expanded(
            child: Container(
              color: Colors.green[100],
              child: const Center(child: Text('Expanded (loose, fills remaining)')),
            ),
          ),
        ],
      ),
    );
  }
}
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

4. Layout Linear: Row e Column

(1) Propriedades de Alinhamento

Propriedade Direção Efeito no Row Efeito no Column
mainAxisAlignment Eixo principal Alinhamento horizontal Alinhamento vertical
crossAxisAlignment Eixo cruzado Alinhamento vertical Alinhamento horizontal
mainAxisSize Tamanho do eixo principal Largura Altura

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Linha de informações do produto (Row)

DART
import 'package:flutter/material.dart';

// Product info row: image + details
Row(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    // Fixed-size image
    ClipRRect(
      borderRadius: BorderRadius.circular(8),
      child: Image.network(
        'https://cdn.shopapp.com/laptop.jpg',
        width: 100,
        height: 100,
        fit: BoxFit.cover,
      ),
    ),
    const SizedBox(width: 12),
    // Flexible details
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('Pro Laptop 16"',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
          const SizedBox(height: 4),
          Text('Electronics > Laptops',
            style: TextStyle(fontSize: 12, color: Colors.grey[600])),
          const SizedBox(height: 8),
          Row(
            children: [
              const Text('\$1,299.99',
                style: TextStyle(fontSize: 18, color: Colors.green)),
              const SizedBox(width: 8),
              Text('\$1,599.99',
                style: TextStyle(
                  fontSize: 14,
                  decoration: TextDecoration.lineThrough,
                  color: Colors.grey[400],
                )),
            ],
          ),
        ],
      ),
    ),
  ],
)
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

(2) Comparação dos Valores de Alinhamento Comuns

MainAxisAlignment Efeito
start Alinhar ao início (padrão)
center Centralizar
end Alinhar ao fim
spaceBetween Primeiro/último nas bordas, espaçamento igual entre eles
spaceAround Espaçamento igual em ambos os lados de cada item
spaceEvenly Todo o espaçamento igual

5. Layout Elástico: Expanded e Flexible

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Layout elástico de preço e botão

DART
import 'package:flutter/material.dart';

// Bottom bar: price (expanded) + button (fixed)
Container(
  padding: const EdgeInsets.all(12),
  child: Row(
    children: [
      Expanded(
        flex: 2, // Takes 2/3 of space
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('Total', style: TextStyle(fontSize: 12)),
            Text('\$${total.toStringAsFixed(2)}',
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
          ],
        ),
      ),
      Expanded(
        flex: 1, // Takes 1/3 of space
        child: ElevatedButton(
          onPressed: onCheckout,
          style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(vertical: 16)),
          child: const Text('Checkout'),
        ),
      ),
    ],
  ),
)
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
Widget Comportamento Espaço Restante Caso de Uso
Expanded Preenchimento forçado Distribuído proporcionalmente Deve preencher todo o espaço
Flexible Preenchimento opcional Distribuído proporcionalmente, pode ser menor Flexível, mas não forçado

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Expanded vs Flexible

DART
import 'package:flutter/material.dart';

Row(
  children: [
    // Expanded: must fill allocated space
    Expanded(
      flex: 1,
      child: Container(color: Colors.red, child: const Text('Expanded')),
    ),
    // Flexible: can be smaller than allocated space
    Flexible(
      flex: 1,
      child: Container(
        color: Colors.blue,
        child: const Text('Flexible'), // May not fill all space
      ),
    ),
    // Fixed width
    SizedBox(width: 80, child: Container(color: Colors.green, child: const Text('Fixed'))),
  ],
)
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

6. Layout em Camadas: Stack e Positioned

O Stack permite que os filhos se sobreponham na ordem de pintura, e o Positioned controla o posicionamento absoluto.

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Badges na imagem do produto

DART
import 'package:flutter/material.dart';

// Simplified class definition
class Product {
  final String imageUrl;
  const Product({required this.imageUrl});
}

// Product image with discount badge and favorite button
Stack(
  children: [
    // Base image
    ClipRRect(
      borderRadius: BorderRadius.circular(12),
      child: Image.network(
        product.imageUrl,
        height: 200,
        width: double.infinity,
        fit: BoxFit.cover,
      ),
    ),
    // Discount badge (top-left)
    Positioned(
      top: 8,
      left: 8,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: Colors.red,
          borderRadius: BorderRadius.circular(4),
        ),
        child: Text('${discountPercent}% OFF',
          style: const TextStyle(color: Colors.white, fontSize: 12)),
      ),
    ),
    // Favorite button (top-right)
    Positioned(
      top: 8,
      right: 8,
      child: CircleAvatar(
        backgroundColor: Colors.white.withOpacity(0.8),
        child: const Icon(Icons.favorite_border, size: 18),
      ),
    ),
    // Stock indicator (bottom)
    Positioned(
      bottom: 8,
      left: 8,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
        decoration: BoxDecoration(
          color: Colors.black54,
          borderRadius: BorderRadius.circular(4),
        ),
        child: const Text('In Stock', style: TextStyle(color: Colors.white, fontSize: 10)),
      ),
    ),
  ],
)
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
Propriedade do Stack Descrição Padrão
alignment Alinhamento para filhos não-Positioned AlignmentDirectional.topStart
fit Como os filhos se adaptam StackFit.loose
clipBehavior Recorte de overflow Clip.hardEdge

7. Layout em Grade: GridView

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Grade de produtos do ShopApp

DART
import 'package:flutter/material.dart';

// Simplified class definition
class Product {
  final int id;
  final String name;
  final double price;
  const Product({required this.id, required this.name, required this.price});
}

// Two-column product grid
GridView.count(
  crossAxisCount: 2,
  mainAxisSpacing: 8,
  crossAxisSpacing: 8,
  childAspectRatio: 0.75, // height = width / 0.75
  padding: const EdgeInsets.all(8),
  children: products.map((product) => ProductCard(
    key: ValueKey(product.id),
    product: product,
    onAddToCart: () => addToCart(product),
  )).toList(),
)

// Responsive grid with LayoutBuilder
LayoutBuilder(
  builder: (context, constraints) {
    int columns = (constraints.maxWidth / 180).floor().clamp(2, 4);
    return GridView.count(
      crossAxisCount: columns,
      mainAxisSpacing: 8,
      crossAxisSpacing: 8,
      childAspectRatio: 0.75,
      children: productWidgets,
    );
  },
)
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
Propriedade do GridView Descrição
crossAxisCount Número de colunas por linha
mainAxisSpacing Espaçamento no eixo principal
crossAxisSpacing Espaçamento no eixo cruzado
childAspectRatio Proporção largura/altura do item filho
padding Espaçamento interno

8. Exemplo Completo: Layout da Página Inicial do ShopApp

DART
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ShopApp'),
        actions: [
          Stack(
            children: [
              IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () {}),
              Positioned(right: 4, top: 4,
                child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
                  child: const Text('5', style: TextStyle(fontSize: 10, color: Colors.white)))),
            ],
          ),
        ],
      ),
      body: CustomScrollView(
        slivers: [
          // Banner section
          SliverToBoxAdapter(
            child: Container(
              height: 180,
              color: Colors.blue[50],
              child: const Center(child: Text('Flash Sale - Up to 50% OFF',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold))),
            ),
          ),
          // Category chips
          SliverToBoxAdapter(
            child: SizedBox(
              height: 50,
              child: ListView(
                scrollDirection: Axis.horizontal,
                padding: const EdgeInsets.symmetric(horizontal: 8),
                children: ['All', 'Electronics', 'Clothing', 'Books', 'Home']
                  .map((c) => Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 4),
                    child: Chip(label: Text(c)),
                  )).toList(),
              ),
            ),
          ),
          // Product grid
          SliverPadding(
            padding: const EdgeInsets.all(8),
            sliver: SliverGrid.count(
              crossAxisCount: 2,
              mainAxisSpacing: 8,
              crossAxisSpacing: 8,
              childAspectRatio: 0.7,
              children: List.generate(20, (i) => _buildProductCard(i)),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildProductCard(int index) {
    return Card(
      clipBehavior: Clip.antiAlias,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(
            flex: 3,
            child: Container(
              color: Colors.grey[200],
              child: const Icon(Icons.image, size: 48, color: Colors.grey),
            ),
          ),
          Expanded(
            flex: 2,
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Product ${index + 1}', maxLines: 1, overflow: TextOverflow.ellipsis),
                  const Spacer(),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text('\$${(19.99 + index * 10).toStringAsFixed(2)}',
                        style: const TextStyle(color: Colors.green)),
                      const Icon(Icons.add_circle, size: 20),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

❓ Perguntas Frequentes

P: O que fazer com as faixas amarelo-pretas de overflow em um Row? R: A largura total dos filhos do Row excede a largura do Row. Envolva filhos de largura variável com Expanded, ou use Flexible para dimensionamento adaptativo.

P: Qual a diferença entre Expanded e Flexible? R: Expanded força o filho a preencher seu espaço alocado (equivalente a Flexible(fit: FlexFit.tight)); Flexible permite que o filho seja menor que sua alocação (fit: FlexFit.loose).

P: Como filhos não-Positioned são posicionados em um Stack? R: São posicionados de acordo com a propriedade alignment do Stack, com padrão no canto superior esquerdo.

P: Qual a diferença entre GridView.count e GridView.builder? R: count é para um número pequeno e fixo de itens; builder usa carregamento preguiçoso para listas grandes de dados (use builder para listas de milhões de produtos).

P: Por que um ListView dentro de um Column causa um erro? R: Column dá a seus filhos restrições de altura não limitada, e o ListView não consegue determinar sua própria altura. Envolva o ListView com Expanded, ou use shrinkWrap: true.

P: Como calcular childAspectRatio? R: ratio = largura / altura. Se cada item tem 180 de largura e 240 de altura, ratio = 180/240 = 0.75.


📖 Resumo


📝 Exercícios

  1. Básico (dificuldade ⭐): Crie uma barra de tags de categorias com rolagem horizontal (Chip + ListView direção horizontal).
  2. Intermediário (dificuldade ⭐⭐): Implemente um layout de página de detalhes do produto: imagem grande no topo + seção de informações no meio + barra de preço/botão de compra na parte inferior, usando Column + Expanded.
  3. Desafio (dificuldade ⭐⭐⭐): Use LayoutBuilder para implementar uma grade de produtos responsiva: 2 colunas no celular, 3 no tablet, 4 no desktop, alternando em tempo real ao redimensionar a janela.

← Anterior | Próximo →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%