Flutter: Fundamentos de Widgets
Widgets são os blocos de construção do Flutter — entenda Widgets e você terá compreendido o DNA do Flutter.
📋 Pré-requisitos: Você precisa ter concluído o seguinte primeiro
1. O Que Você Vai Aprender
- Arquitetura de três árvores Widget-Element-RenderObject e suas implicações de desempenho
- Comparação do ciclo de vida: StatelessWidget vs StatefulWidget
- Widgets básicos comuns: Text, Image, Icon, Container, Scaffold
- Propósito e casos de uso de Key: ValueKey / ObjectKey / GlobalKey
- Implementação do cartão de produto do ShopApp como StatelessWidget
2. Uma História Real de um Desenvolvedor Front-end em Transição para o Flutter
(1) O Problema: A Armadilha do Mindset DOM
Bob transitou do React para o Flutter e instintivamente tentou entender Widgets através de um modelo mental de "componente renderiza DOM". Ele descobriu que os cartões de produto eram frequentemente reconstruídos durante a rolagem da lista, fazendo com que uma página de 1000 produtos travasse a 30fps. Ainda mais confuso, adicionar key à lista piorou o desempenho — porque ele estava usando UniqueKey.
(2) A Solução da Arquitetura de Três Árvores
O Flutter não é um simples mapeamento "componente → DOM", mas uma arquitetura de três camadas "Widget → Element → RenderObject". Widgets são descrições de configuração (leves), Elements gerenciam instâncias (núcleo do diff), e RenderObjects cuidam do layout e pintura (pesados). Ao entender as três árvores, Bob percebeu que deveria otimizar na camada Element via diffing, em vez de reconstruir na camada Widget.
import 'package:flutter/material.dart';
// Lightweight widget: just configuration
const ProductCard({required this.product, super.key});
// Flutter internally creates Element and RenderObject
// Widget rebuilds are cheap - Element diffing avoids RenderObject rebuild
> 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: Rolagem Suave a 60fps
Após mudar para ValueKey(product.id), o diff do Element correspondeu os itens com precisão, e a lista de 1000 produtos manteve 60fps estáveis.
3. As Três Árvores: Widget-Element-RenderObject
graph LR
W[Widget Tree] --> E[Element Tree]
E --> R[RenderObject Tree]
W -.->|canUpdate| E
E -.->|adoptChild| R
> 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) Responsabilidades das Três Árvores
| Camada | Criado Por | Responsabilidade | Peso |
|---|---|---|---|
| Widget | build() do desenvolvedor |
Descrição de configuração imutável | Muito leve |
| Element | Framework automaticamente | Gerenciar estrutura da árvore, diff de atualizações | Médio |
| RenderObject | Framework automaticamente | Medir, layout, pintar | Pesado |
(2) Regra canUpdate
O Element determina se deve reutilizar via Widget.canUpdate(oldWidget, newWidget):
import 'package:flutter/material.dart';
// canUpdate logic
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
> 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.
| Cenário | runtimeType | key | Resultado |
|---|---|---|---|
| Mesmo tipo, mesma key | Mesmo | Mesma | Reutilizar Element |
| Mesmo tipo, key diferente | Mesmo | Diferente | Reconstruir Element |
| Tipo diferente | Diferente | - | Reconstruir Element |
4. Widgets Básicos Comuns
(1) Text e Estilos
▶ 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.
: Text com vários estilos
import 'package:flutter/material.dart';
// Basic text
const Text('ShopApp')
// Styled text
Text(
'Flash Sale!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.red,
letterSpacing: 1.5,
),
)
// Rich text with multiple styles
Text.rich(
TextSpan(
text: 'Price: ',
style: const TextStyle(fontSize: 16, color: Colors.grey),
children: [
TextSpan(
text: '\$99.99',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
TextSpan(
text: ' \$149.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) Container: O Contêiner Universal
| Propriedade do Container | Propósito | Tipo |
|---|---|---|
padding |
Espaçamento interno | EdgeInsets |
margin |
Margem externa | EdgeInsets |
decoration |
Decoração de fundo | BoxDecoration |
constraints |
Restrições | BoxConstraints |
alignment |
Alinhamento do widget filho | Alignment |
▶ 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.
: Cartão de produto com Container
import 'package:flutter/material.dart';
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
'https://cdn.shopapp.com/products/laptop.jpg',
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Pro Laptop', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text('\$1,299.99', style: const TextStyle(fontSize: 18, color: Colors.green)),
],
),
),
],
),
)
> 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) Icon e IconButton
▶ 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.
: Ícones em um contexto de e-commerce
import 'package:flutter/material.dart';
// Icon with color and size
const Icon(Icons.shopping_cart, color: Colors.blue, size: 32)
// IconButton for actions
IconButton(
icon: const Icon(Icons.favorite_border),
onPressed: () {
// Add to wishlist
},
tooltip: 'Add to Wishlist',
)
// Badge icon for cart count
Stack(
children: [
const Icon(Icons.shopping_cart, size: 28),
Positioned(
right: 0,
top: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
child: const Text('3', style: TextStyle(fontSize: 10, color: Colors.white),
textAlign: TextAlign.center),
),
),
],
)
> 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.
5. Mecanismo de Key
Keys controlam a estratégia de reutilização de Element, o que é especialmente crítico durante atualizações de lista.
(1) Tipos de Key
| Tipo de Key | Base de Comparação | Caso de Uso |
|---|---|---|
ValueKey(value) |
Igualdade de valor | Itens de lista com identificadores únicos (ex.: product.id) |
ObjectKey(object) |
Referência de objeto | Instâncias de objeto únicas |
UniqueKey() |
Diferente a cada vez | Forçar não reutilização (raramente usado) |
GlobalKey |
Globalmente único | Acesso a State entre widgets |
▶ 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.
: O papel de Key em listas
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});
}
// BAD: No key, Flutter matches by position
ListView(
children: [
ProductTile(product: products[0]), // index 0
ProductTile(product: products[1]), // index 1
],
)
// When products reorder, Element mismatches State
// Solution: Use ValueKey with unique id
// GOOD: ValueKey enables correct Element-Widget matching
ListView(
children: [
ProductTile(key: ValueKey(products[0].id), product: products[0]),
ProductTile(key: ValueKey(products[1].id), product: products[1]),
],
)
> 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. Exemplo Completo: Widget de Cartão de Produto do ShopApp
import 'package:flutter/material.dart';
class Product {
final int id;
final String name;
final double price;
final String imageUrl;
final double rating;
final int reviewCount;
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
this.rating = 0.0,
this.reviewCount = 0,
});
}
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback? onAddToCart;
final VoidCallback? onTap;
const ProductCard({
super.key,
required this.product,
this.onAddToCart,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Card(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: Stack(
children: [
Image.network(product.imageUrl, fit: BoxFit.cover,
width: double.infinity),
if (product.price < 50)
Positioned(top: 8, left: 8,
child: _buildBadge('SALE', Colors.red)),
],
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product.name, maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text('\$${product.price.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.green, fontSize: 16)),
const Spacer(),
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: const Icon(Icons.add_shopping_cart, size: 20),
onPressed: onAddToCart,
),
),
],
),
),
),
],
),
),
);
}
Widget _buildBadge(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
child: Text(text, style: const 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.
❓ Perguntas Frequentes
P: Quando o método build de um StatelessWidget é chamado? R: Quando um widget pai é reconstruído, ou quando
setStateaciona uma reconstrução do pai. Um StatelessWidget por si só não pode acionar uma reconstrução.
P: Por que os Widgets são projetados para serem imutáveis? R: Imutabilidade significa que podem ser criados frequentemente sem preocupação com efeitos colaterais. O framework Flutter pode comparar com segurança Widgets antigos e novos para decidir se deve atualizar o Element.
P: Quando preciso usar um Key? R: Quando Widgets em uma lista compartilham o mesmo tipo mas têm dados diferentes, e itens podem ser adicionados, removidos ou reordenados. Listas estáticas geralmente não precisam de Keys.
P: GlobalKey permite acesso a State entre widgets — isso não quebra o encapsulamento? R: Sim, GlobalKey é uma "saída de emergência" e deve ser usada com moderação. Casos de uso comuns: validação de formulário, gerenciamento de ScrollController. Prefira passar dados via callbacks.
P: Qual a diferença entre Container e Scaffold? R: Container é um contêiner de decoração de uso geral; Scaffold é um scaffold de página Material Design que fornece slots para AppBar/Body/FAB/Drawer/BottomNav.
P: E se Image.network falhar ao carregar? R: Use o parâmetro
errorBuilderpara exibir uma imagem placeholder, combinado com o pacotecached_network_imagepara cache e indicadores de carregamento.
📖 Resumo
- Widget (configuração) → Element (gerenciamento) → RenderObject (pintura) arquitetura de três camadas
- Widgets são imutáveis e leves, podem ser reconstruídos frequentemente; Elements realizam diff para decidir se atualizam RenderObjects
- Keys controlam a estratégia de reutilização de Element: ValueKey corresponde por valor, UniqueKey força não reutilização
- Container é o contêiner universal; Scaffold é o scaffold de página
- Use ValueKey(product.id) em listas para garantir correspondência correta de Element
📝 Exercícios
- Básico (dificuldade ⭐): Crie um
UserAvatarStatelessWidget que aceita name e imageUrl, exibindo um avatar circular e nome de usuário. - Intermediário (dificuldade ⭐⭐): Implemente um widget
PriceTagque suporta exibição de preço original/com desconto, com o preço com desconto em texto verde grande e o preço original com risco. - Desafio (dificuldade ⭐⭐⭐): Construa uma lista de produtos ordenável onde a reordenação por arraste usa ValueKey para garantir que o State siga corretamente, e após a ordenação, exiba a nova ordem via SnackBar.