Flutter: StatefulWidget e Interação
State é a memória de um Widget — sem State, uma interface é como um amnésico que não te reconhece a cada vez.
📋 Pré-requisitos: Você precisa ter concluído o seguinte primeiro
- Aula 3: Fundamentos de Widgets
1. O Que Você Vai Aprender
- Ciclo de vida completo do StatefulWidget: createState → initState → build → dispose
- Mecanismo de reconstrução do
setStatee escopo mínimo de reconstrução - Tratamento de eventos: GestureDetector, InkWell, callbacks onPressed
- Interação de entrada: TextField, escuta de texto com TextEditingController
- Implementação do seletor de quantidade do carrinho do ShopApp
2. Uma História Real Sobre um Dilema de Interação
(1) O Problema: Cliques Sem Resposta
Bob encontrou um bug estranho na página do carrinho do ShopApp: os usuários clicavam no botão "-", a quantidade ia de 1 para 0, mas a UI ainda exibia 1. Ele modificou diretamente a variável _count = _count - 1 sem chamar setState — o Widget não sabia que o estado mudou, então naturalmente não reconstruiria a UI.
(2) A Solução com setState
Princípio central do StatefulWidget: mudar o estado não equivale a mudar a UI. Você deve notificar o framework via setState que "eu mudei, por favor reconstrua."
import 'package:flutter/material.dart';
// WRONG: Direct mutation, UI not updated
void _decrement() {
_count = _count - 1; // State changed, but UI doesn't know
}
// RIGHT: setState triggers rebuild
void _decrement() {
setState(() {
_count = _count - 1; // Framework rebuilds this widget
});
}
> 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: UI e Estado Sempre Sincronizados
Após adicionar setState, a quantidade do carrinho atualizou em tempo real e o preço total foi recalculado automaticamente. A partir de então, Bob se lembrou: quando você muda dados, deve chamar setState.
3. Ciclo de Vida do StatefulWidget
stateDiagram-v2
[*] --> createState
createState --> initState
initState --> didChangeDependencies
didChangeDependencies --> build
build --> didUpdateWidget: setState / parent rebuild
didUpdateWidget --> build
build --> deactivate: removed from tree
deactivate --> dispose
dispose --> [*]
> 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) Callbacks do Ciclo de Vida
| Callback | Momento do Gatilho | Propósito |
|---|---|---|
createState |
Framework cria o State | Inicializar objeto State |
initState |
State inserido na árvore | Inicialização única (inscrições, controladores) |
didChangeDependencies |
InheritedWidget dependente muda | Ler tema, locale, etc. |
build |
Cada vez que a UI precisa ser renderizada | Construir árvore de Widgets |
didUpdateWidget |
Widget pai reconstrói | Comparar Widget antigo/novo e responder |
setState |
Desenvolvedor chama explicitamente | Marcar como sujo, acionar reconstrução |
deactivate |
State removido da árvore | Remoção temporária (pode ser reinserido) |
dispose |
State permanentemente destruído | Liberar recursos, cancelar inscrições |
▶ 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.
: Rastreamento do ciclo de vida
import 'package:flutter/material.dart';
class LifecycleTracker extends StatefulWidget {
const LifecycleTracker({super.key});
@override
State<LifecycleTracker> createState() => _LifecycleTrackerState();
}
class _LifecycleTrackerState extends State<LifecycleTracker> {
int _buildCount = 0;
@override
void initState() {
super.initState();
debugPrint('initState called');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint('didChangeDependencies called');
}
@override
void didUpdateWidget(covariant LifecycleTracker oldWidget) {
super.didUpdateWidget(oldWidget);
debugPrint('didUpdateWidget called');
}
@override
Widget build(BuildContext context) {
_buildCount++;
debugPrint('build #$_buildCount');
return Text('Build count: $_buildCount');
}
@override
void dispose() {
debugPrint('dispose called');
super.dispose();
}
}
> 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. Mecanismo setState em Profundidade
(1) Fluxo do setState
| Etapa | Ação |
|---|---|
| 1 | Chamar setState(fn) |
| 2 | Executar fn, atualizar variáveis de estado |
| 3 | Marcar Element como sujo |
| 4 | Próximo frame, Framework chama build |
| 5 | Diff das árvores de Widget antiga/nova, atualizar RenderObject |
▶ 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.
: Seletor de quantidade do carrinho
import 'package:flutter/material.dart';
class QuantitySelector extends StatefulWidget {
final int initialValue;
final ValueChanged<int> onChanged;
const QuantitySelector({
super.key,
this.initialValue = 1,
required this.onChanged,
});
@override
State<QuantitySelector> createState() => _QuantitySelectorState();
}
class _QuantitySelectorState extends State<QuantitySelector> {
late int _count;
@override
void initState() {
super.initState();
_count = widget.initialValue;
}
@override
void didUpdateWidget(covariant QuantitySelector oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialValue != widget.initialValue) {
_count = widget.initialValue;
}
}
void _increment() {
setState(() {
_count = (_count + 1).clamp(1, 99);
});
widget.onChanged(_count);
}
void _decrement() {
setState(() {
_count = (_count - 1).clamp(1, 99);
});
widget.onChanged(_count);
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.remove), onPressed: _decrement),
Text('$_count', style: const TextStyle(fontSize: 18)),
IconButton(icon: const Icon(Icons.add), onPressed: _increment),
],
),
);
}
}
> 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. Tratamento de Eventos
(1) GestureDetector vs InkWell
| Widget | Efeito Visual | Ripple | Caso de Uso |
|---|---|---|---|
GestureDetector |
Sem feedback visual | Não | Gestos personalizados (pressão longa/arrastar/toque duplo) |
InkWell |
Ripple Material | Sim | Cliques em cartão/item de lista |
IconButton |
Ícone + ripple | Sim | Ações na barra de ferramentas |
ElevatedButton |
Estilo de botão | Sim | Ações primárias |
▶ 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.
: Reconhecimento de gestos com GestureDetector
import 'package:flutter/material.dart';
// Simplified class definition
class Product {
final String name;
final double price;
final String imageUrl;
final int id;
const Product({required this.name, required this.price, required this.imageUrl, required this.id});
}
GestureDetector(
onTap: () => debugPrint('Tapped'),
onDoubleTap: () => debugPrint('Double tapped'),
onLongPress: () => debugPrint('Long pressed'),
onHorizontalDragEnd: (details) {
// Swipe to add/remove from cart
if (details.primaryVelocity! < 0) {
addToCart(product); // Swipe left
} else {
removeFromCart(product); // Swipe right
}
},
child: ProductCard(product: product),
)
> 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.
▶ 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.
: Clique em cartão de produto com InkWell
import 'package:flutter/material.dart';
// Product class definition shown above
InkWell(
onTap: () => Navigator.pushNamed(context, '/product/${product.id}'),
borderRadius: BorderRadius.circular(12),
child: Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Image.network(product.imageUrl, width: 60, height: 60),
const SizedBox(width: 12),
Expanded(child: Text(product.name)),
Text('$${product.price.toStringAsFixed(2)}'),
],
),
),
),
)
> 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. Interação de Entrada: TextField
(1) TextEditingController
| Recurso | Método |
|---|---|
| Obter texto | controller.text |
| Definir texto | controller.text = 'novo' |
| Escutar mudanças | controller.addListener(fn) |
| Selecionar texto | controller.selection |
| Limpar entrada | controller.clear() |
▶ 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.
: Implementação de barra de busca
import 'package:flutter/material.dart';
class SearchBar extends StatefulWidget {
final ValueChanged<String> onSearch;
const SearchBar({super.key, required this.onSearch});
@override
State<SearchBar> createState() => _SearchBarState();
}
class _SearchBarState extends State<SearchBar> {
final _controller = TextEditingController();
@override
void initState() {
super.initState();
_controller.addListener(() {
widget.onSearch(_controller.text);
});
}
@override
void dispose() {
_controller.dispose(); // Important: prevent memory leak
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Search products...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_controller.clear();
widget.onSearch('');
},
)
: null,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(24)),
),
);
}
}
> 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.
7. Exemplo Completo: Página do Carrinho do ShopApp
import 'package:flutter/material.dart';
class CartPage extends StatefulWidget {
const CartPage({super.key});
@override
State<CartPage> createState() => _CartPageState();
}
class _CartPageState extends State<CartPage> {
final List<CartItem> _items = [
CartItem(name: 'Pro Laptop', price: 1299.99, quantity: 1),
CartItem(name: 'Wireless Mouse', price: 29.99, quantity: 2),
CartItem(name: 'USB-C Hub', price: 49.99, quantity: 1),
];
double get _total => _items.fold(0.0, (sum, item) => sum + item.price * item.quantity);
int get _itemCount => _items.fold(0, (sum, item) => sum + item.quantity);
void _updateQuantity(int index, int newQty) {
setState(() {
if (newQty <= 0) {
_items.removeAt(index);
} else {
_items[index] = _items[index].copyWith(quantity: newQty);
}
});
}
void _removeItem(int index) {
setState(() => _items.removeAt(index));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Cart ($_itemCount items)')),
body: _items.isEmpty
? const Center(child: Text('Your cart is empty'))
: Column(
children: [
Expanded(
child: ListView.separated(
itemCount: _items.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, index) {
final item = _items[index];
return ListTile(
leading: const Icon(Icons.shopping_bag),
title: Text(item.name),
subtitle: Text('$${item.price.toStringAsFixed(2)} each'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.remove_circle_outline),
onPressed: () => _updateQuantity(index, item.quantity - 1)),
Text('${item.quantity}', style: const TextStyle(fontSize: 16)),
IconButton(icon: const Icon(Icons.add_circle_outline),
onPressed: () => _updateQuantity(index, item.quantity + 1)),
IconButton(icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () => _removeItem(index)),
],
),
);
},
),
),
// Total bar
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.grey[100],
border: Border(top: BorderSide(color: Colors.grey[300]!))),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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)),
]),
ElevatedButton(
onPressed: _items.isEmpty ? null : () {},
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16)),
child: const Text('Checkout'),
),
],
),
),
],
),
);
}
}
class CartItem {
final String name;
final double price;
final int quantity;
const CartItem({required this.name, required this.price, required this.quantity});
CartItem copyWith({String? name, double? price, int? quantity}) =>
CartItem(name: name ?? this.name, price: price ?? this.price, quantity: quantity ?? this.quantity);
}
> 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: Pode-se chamar setState dentro de initState? R: Não. Quando initState executa, o Widget ainda não terminou de ser construído. Se você precisa atualizar após a inicialização, use
WidgetsBinding.instance.addPostFrameCallback.
P: Por que usar uma closure em setState mesmo para uma simples atribuição de variável? R: A closure marca claramente qual estado mudou, ajudando o framework a otimizar. Também facilita rastrear mudanças de estado durante a depuração.
P: onTap do GestureDetector não funciona? R: Verifique se o filho tem um competidor de gestos que absorve eventos (ex.: rolagem do ListView). Use
behavior: HitTestBehavior.opaquepara garantir que toda a área seja clicável.
P: Quando o TextEditingController deve ser descartado? R: Chame
controller.dispose()no métododisposedo State, caso contrário causará um vazamento de memória.
P: Por que a UI não muda após setState? R: Verifique se você modificou a variável fora da closure do setState. Apenas mudanças dentro da closure acionam uma reconstrução.
P: Múltiplas chamadas a setState causam múltiplas reconstruções? R: Não. O Flutter agrupa múltiplas chamadas a setState dentro de um único frame e reconstrói apenas uma vez.
📖 Resumo
- StatefulWidget tem um ciclo de vida completo: initState → build → dispose
- setState é a única forma de acionar atualizações de UI: mudar o estado sem chamar setState não atualizará a UI
- GestureDetector suporta gestos ricos; InkWell fornece feedback ripple Material
- TextEditingController gerencia o estado do campo de entrada e deve ser descartado no método dispose
- O seletor de quantidade do carrinho é uma aplicação clássica de setState + passagem de callback
📝 Exercícios
- Básico (dificuldade ⭐): Crie uma página de contador com botões "adicionar/subtrair/resetar" e exibição de número centralizado.
- Intermediário (dificuldade ⭐⭐): Implemente uma barra de busca que filtra uma lista em tempo real conforme você digita (usando setState + TextEditingController).
- Desafio (dificuldade ⭐⭐⭐): Implemente um carrinho de compras completo: suporte a ajuste de quantidade, remoção de itens, cálculo automático do total, e exiba "Carrinho vazio" para o estado vazio.