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

1. O Que Você Vai Aprender


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."

DART
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
  });
}
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: 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

100%
stateDiagram-v2
    [*] --> createState
    createState --> initState
    initState --> didChangeDependencies
    didChangeDependencies --> build
    build --> didUpdateWidget: setState / parent rebuild
    didUpdateWidget --> build
    build --> deactivate: removed from tree
    deactivate --> dispose
    dispose --> [*]
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) 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

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.

: Rastreamento do ciclo de vida

DART
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();
  }
}
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. 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

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.

: Seletor de quantidade do carrinho

DART
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),
        ],
      ),
    );
  }
}
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.

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

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.

: Reconhecimento de gestos com GestureDetector

DART
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),
)
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.

▶ 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.

: Clique em cartão de produto com InkWell

DART
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)}'),
        ],
      ),
    ),
  ),
)
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. 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

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.

: Implementação de barra de busca

DART
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)),
      ),
    );
  }
}
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.

7. Exemplo Completo: Página do Carrinho do ShopApp

DART
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);
}
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: 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.opaque para garantir que toda a área seja clicável.

P: Quando o TextEditingController deve ser descartado? R: Chame controller.dispose() no método dispose do 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


📝 Exercícios

  1. Básico (dificuldade ⭐): Crie uma página de contador com botões "adicionar/subtrair/resetar" e exibição de número centralizado.
  2. Intermediário (dificuldade ⭐⭐): Implemente uma barra de busca que filtra uma lista em tempo real conforme você digita (usando setState + TextEditingController).
  3. 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.

← 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%