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


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.

DART
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
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: 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

100%
graph LR
    W[Widget Tree] --> E[Element Tree]
    E --> R[RenderObject Tree]
    W -.->|canUpdate| E
    E -.->|adoptChild| R
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) 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):

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

// canUpdate logic
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}
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.
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

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.

: Text com vários estilos

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

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.

: Cartão de produto com Container

DART
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)),
          ],
        ),
      ),
    ],
  ),
)
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) Icon e IconButton

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

: Ícones em um contexto de e-commerce

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

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.

: O papel de Key em listas

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

// 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]),
  ],
)
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. Exemplo Completo: Widget de Cartão de Produto do ShopApp

DART
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)),
    );
  }
}
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: Quando o método build de um StatelessWidget é chamado? R: Quando um widget pai é reconstruído, ou quando setState aciona 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 errorBuilder para exibir uma imagem placeholder, combinado com o pacote cached_network_image para cache e indicadores de carregamento.


📖 Resumo


📝 Exercícios

  1. Básico (dificuldade ⭐): Crie um UserAvatar StatelessWidget que aceita name e imageUrl, exibindo um avatar circular e nome de usuário.
  2. Intermediário (dificuldade ⭐⭐): Implemente um widget PriceTag que 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.
  3. 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.

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