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
- Aula 3: Fundamentos de Widgets
1. O Que Você Vai Aprender
- Modelo de caixa: protocolo de passagem de restrições Constraints → Size → Position
- Layout linear: Row/Column com alinhamento Main/CrossAxisAlignment
- Layout elástico: Flex/Expanded/Flexible distribuição proporcional
- Layout em camadas: Stack/Positioned para sobreposições e badges
- Layout de grade de produtos do ShopApp: GridView.count e fluxo em cascata
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.
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')),
],
)
> 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
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
> 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
> 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
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)')),
),
),
],
),
);
}
}
> 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
> 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)
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],
)),
],
),
],
),
),
],
)
> 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
> 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
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'),
),
),
],
),
)
> 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
> 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
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'))),
],
)
> 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
> 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
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)),
),
),
],
)
> 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
> 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
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,
);
},
)
> 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
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),
],
),
],
),
),
),
],
),
);
}
}
> 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
- Núcleo do layout Flutter: restrições passam para baixo, tamanhos informam para cima, posições determinadas pelo pai
- Row/Column são layouts lineares, controlados via Main/CrossAxisAlignment
- Expanded força preenchimento, Flexible adapta elasticamente, flex controla proporções
- Stack + Positioned cria efeitos em camadas (badges, sobreposições)
- GridView.count constrói grades de produtos, LayoutBuilder permite contagens de colunas responsivas
📝 Exercícios
- Básico (dificuldade ⭐): Crie uma barra de tags de categorias com rolagem horizontal (Chip + ListView direção horizontal).
- 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.
- 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.