Flutter: Material Design e Componentes Comuns

Material Design é a linguagem nativa do Flutter — domine-a e seu app terá uma "sensação nativa" consistente com a plataforma.

📋 Pré-requisitos: Você precisa ter concluído o seguinte primeiro

1. O Que Você Vai Aprender


2. Uma História das Especificações de Design ao Código

(1) O Problema: Construir Componentes de UI do Zero Consome Tempo

Bob recebeu as especificações de UI do ShopApp e descobriu que 80% dos componentes são Material Design padrão: AppBar, Card, FAB, BottomSheet. Se codificasse cada um manualmente, levaria um estimado de 3 semanas de desenvolvimento. Pior ainda, seus componentes personalizados não correspondiam ao estilo nativo do sistema, e os usuários reclamavam que o app "não parecia um app de verdade."

(2) A Solução da Biblioteca de Componentes Material

O Flutter inclui uma biblioteca completa de componentes Material Design — use componentes padrão com uma única linha de código, com animações integradas, acessibilidade e adaptação de tema.

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

// 5 lines = full Material page
Scaffold(
  appBar: AppBar(title: const Text('ShopApp')),
  body: const ProductList(),
  floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
  bottomNavigationBar: BottomNavigationBar(items: [...]),
)
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: Velocidade de Desenvolvimento 3x Maior

Após usar componentes Material, a página de detalhes passou de 3 dias para 1 dia, com adaptação automática de tema Claro/Escuro e modo de acessibilidade.


3. Scaffold

O Scaffold é a estrutura fundamental para páginas Material Design, fornecendo uma estrutura de página padronizada.

100%
graph TD
    S[Scaffold] --> A[AppBar]
    S --> B[Body]
    S --> F[FAB]
    S --> BN[BottomNavigationBar]
    S --> D[Drawer]
    B --> C[Card]
    B --> LT[ListTile]
    B --> GV[GridView]
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) Componentes do Scaffold

Propriedade Tipo Descrição
appBar PreferredSizeWidget Barra de navegação superior
body Widget Corpo da página
floatingActionButton Widget Botão flutuante no canto inferior direito
bottomNavigationBar Widget Barra de navegação inferior
drawer Widget Menu gaveta esquerdo
bottomSheet Widget Painel pop-up inferior
snackBar SnackBar Barra de notificação inferior

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

: Scaffold da página inicial do ShopApp

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

Scaffold(
  appBar: AppBar(
    title: const Text('ShopApp'),
    actions: [
      IconButton(icon: const Icon(Icons.search), onPressed: () {}),
      IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () {}),
    ],
  ),
  body: const ProductGrid(),
  floatingActionButton: FloatingActionButton.extended(
    onPressed: () {},
    icon: const Icon(Icons.flash_on),
    label: const Text('Flash Sale'),
  ),
  bottomNavigationBar: NavigationBar(
    destinations: const [
      NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
      NavigationDestination(icon: Icon(Icons.category), label: 'Categories'),
      NavigationDestination(icon: Icon(Icons.favorite), label: 'Wishlist'),
      NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
    ],
  ),
  drawer: Drawer(
    child: ListView(
      children: [
        const DrawerHeader(
          decoration: BoxDecoration(color: Colors.blue),
          child: Text('ShopApp', style: TextStyle(color: Colors.white, fontSize: 24)),
        ),
        ListTile(leading: const Icon(Icons.home), title: const Text('Home'), onTap: () {}),
        ListTile(leading: const Icon(Icons.settings), title: const Text('Settings'), onTap: () {}),
      ],
    ),
  ),
)
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. Componentes Card e Lista

(1) Card e ListTile

▶ 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 (Card + ListTile)

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

// Simplified class definition
class Product {
  final String imageUrl;
  final String name;
  final double price;
  final String category;
  const Product({required this.imageUrl, required this.name, required this.price, required this.category});
}

Card(
  margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
  elevation: 2,
  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  child: ListTile(
    leading: ClipRRect(
      borderRadius: BorderRadius.circular(8),
      child: Image.network(product.imageUrl, width: 56, height: 56, fit: BoxFit.cover),
    ),
    title: Text(product.name, style: const TextStyle(fontWeight: FontWeight.w600)),
    subtitle: Text('$${product.price.toStringAsFixed(2)} - ${product.category}'),
    trailing: IconButton(
      icon: const Icon(Icons.add_shopping_cart),
      onPressed: () => addToCart(product),
    ),
    onTap: () => navigateToDetail(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.

: Lista de pedidos (ListTile + Divider)

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

// Simplified class definition
class Order {
  final int id;
  final double total;
  final String date;
  final String status;
  const Order({required this.id, required this.total, required this.date, required this.status});
}

ListView.separated(
  itemCount: orders.length,
  separatorBuilder: (_, __) => const Divider(height: 1, indent: 16),
  itemBuilder: (context, index) {
    final order = orders[index];
    return ListTile(
      leading: CircleAvatar(
        backgroundColor: order.status == 'Shipped' ? Colors.green : Colors.orange,
        child: Icon(order.status == 'Shipped' ? Icons.local_shipping : Icons.schedule,
          color: Colors.white),
      ),
      title: Text('Order #${order.id}'),
      subtitle: Text('$${order.total.toStringAsFixed(2)} - ${order.date}'),
      trailing: const Icon(Icons.chevron_right),
      onTap: () => navigateToOrderDetail(order),
    );
  },
)
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.

Componente Posição Interação Caso de Uso
AlertDialog Pop-up central Confirmar/Cancelar Confirmação de ação importante (excluir pedido)
BottomSheet Desliza de baixo Conteúdo personalizado Seleção de tamanho, filtragem
Snackbar Notificação temporária inferior Pode incluir ação Feedback leve (adicionado ao carrinho)
Dialog Pop-up central Totalmente personalizável Conteúdo de pop-up personalizado

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

: AlertDialog de confirmação de exclusão

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

// Simplified class definition
class CartItem {
  final String name;
  const CartItem({required this.name});
}

void showDeleteConfirmation(BuildContext context, CartItem item) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Remove Item'),
      content: Text('Remove "${item.name}" from your cart?'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('Cancel'),
        ),
        FilledButton(
          onPressed: () {
            removeItem(item);
            Navigator.pop(context);
          },
          child: const Text('Remove'),
        ),
      ],
    ),
  );
}
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.

: BottomSheet de seleção de tamanho

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

// Product class definition shown above

void showSizeSelector(BuildContext context, Product product) {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (context) => DraggableScrollableSheet(
      initialChildSize: 0.5,
      minChildSize: 0.3,
      maxChildSize: 0.8,
      expand: false,
      builder: (context, scrollController) => Column(
        children: [
          // Handle bar
          Center(child: Container(
            margin: const EdgeInsets.symmetric(vertical: 8),
            width: 40, height: 4,
            decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
          )),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text('Select Size', style: Theme.of(context).textTheme.titleLarge),
          ),
          Expanded(
            child: ListView(
              controller: scrollController,
              children: ['S', 'M', 'L', 'XL', 'XXL'].map((size) => ListTile(
                title: Text('Size $size'),
                trailing: size == 'M' ? const Icon(Icons.check, color: Colors.green) : null,
                onTap: () => Navigator.pop(context, size),
              )).toList(),
            ),
          ),
        ],
      ),
    ),
  );
}
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.

: Feedback Snackbar do carrinho

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

// Product class definition shown above

void showCartSnackbar(BuildContext context, Product product) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text('${product.name} added to cart'),
      duration: const Duration(seconds: 2),
      action: SnackBarAction(
        label: 'View Cart',
        onPressed: () => navigateToCart(),
      ),
    ),
  );
}
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. Componentes de Alternância

(1) Comparação de Componentes de Alternância

Componente Interação Tipo de Dado Caso de Uso
Switch Alternância deslizante bool Configurações ligado/desligado (modo escuro)
Checkbox Clique para marcar bool Confirmação de múltipla seleção (concordar com termos)
Radio Clique para seleção única T Seleção única de um grupo (método de pagamento)
Slider Arraste o controle deslizante double Seleção de faixa (faixa de preço)

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

: Seleção de método de pagamento com Radio

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

class PaymentSelector extends StatefulWidget {
  const PaymentSelector({super.key});

  @override
  State<PaymentSelector> createState() => _PaymentSelectorState();
}

class _PaymentSelectorState extends State<PaymentSelector> {
  String _payment = 'credit_card';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        RadioListTile<String>(
          title: const Text('Credit Card'),
          subtitle: const Text('Visa **** 4242'),
          secondary: const Icon(Icons.credit_card),
          value: 'credit_card',
          groupValue: _payment,
          onChanged: (v) => setState(() => _payment = v!),
        ),
        RadioListTile<String>(
          title: const Text('PayPal'),
          secondary: const Icon(Icons.account_balance_wallet),
          value: 'paypal',
          groupValue: _payment,
          onChanged: (v) => setState(() => _payment = v!),
        ),
        RadioListTile<String>(
          title: const Text('Apple Pay'),
          secondary: const Icon(Icons.phone_iphone),
          value: 'apple_pay',
          groupValue: _payment,
          onChanged: (v) => setState(() => _payment = v!),
        ),
      ],
    );
  }
}
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.

: Slider de faixa de preço

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

RangeValues _priceRange = const RangeValues(0, 500);

RangeSlider(
  values: _priceRange,
  min: 0,
  max: 2000,
  divisions: 40,
  labels: RangeLabels(
    '$${_priceRange.start.toStringAsFixed(0)}',
    '$${_priceRange.end.toStringAsFixed(0)}',
  ),
  onChanged: (values) => setState(() => _priceRange = values),
)
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 de Detalhes do Produto do ShopApp

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

// Simplified class definition
class Product {
  final String name;
  final double price;
  final String imageUrl;
  final double? originalPrice;
  final double rating;
  final int reviewCount;
  const Product({required this.name, required this.price, required this.imageUrl, this.originalPrice, this.rating = 0.0, this.reviewCount = 0});
}

class ProductDetailPage extends StatelessWidget {
  final Product product;
  const ProductDetailPage({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // Collapsible app bar with product image
          SliverAppBar(
            expandedHeight: 300,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text(product.name, style: const TextStyle(fontSize: 16)),
              background: Stack(
                fit: StackFit.expand,
                children: [
                  Image.network(product.imageUrl, fit: BoxFit.cover),
                  const DecoratedBox(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(begin: Alignment.bottomCenter, end: Alignment.topCenter,
                        colors: [Colors.black54, Colors.transparent]),
                    ),
                  ),
                ],
              ),
            ),
          ),
          // Product info
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(children: [
                    Text('$${product.price.toStringAsFixed(2)}',
                      style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.green)),
                    const SizedBox(width: 12),
                    if (product.originalPrice != null)
                      Text('$${product.originalPrice!.toStringAsFixed(2)}',
                        style: const TextStyle(decoration: TextDecoration.lineThrough, color: Colors.grey)),
                  ]),
                  const SizedBox(height: 8),
                  Row(children: [
                    Icon(Icons.star, color: Colors.amber[700], size: 18),
                    Text(' ${product.rating} (${product.reviewCount} reviews)'),
                  ]),
                  const SizedBox(height: 16),
                  const Text('Description', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
                  const SizedBox(height: 8),
                  const Text('Premium quality product with fast shipping worldwide. '
                      'Free returns within 30 days. USD settlement supported.'),
                ],
              ),
            ),
          ),
        ],
      ),
      // Bottom action bar
      bottomNavigationBar: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Row(children: [
            IconButton.outlined(onPressed: () {}, icon: const Icon(Icons.favorite_border)),
            const SizedBox(width: 8),
            Expanded(
              child: FilledButton.icon(
                onPressed: () => _showSizeSelector(context),
                icon: const Icon(Icons.shopping_cart),
                label: const Text('Add to Cart'),
              ),
            ),
          ]),
        ),
      ),
    );
  }

  void _showSizeSelector(BuildContext context) {
    showModalBottomSheet(
      context: context,
      builder: (ctx) => const SizeSelectorSheet(),
    );
  }
}
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: O body e o bottomNavigationBar do Scaffold podem ser usados juntos? R: Sim, essa é a prática padrão. body ocupa o espaço principal, bottomNavigationBar fica fixo na parte inferior.

P: Snackbar não aparece? R: Você deve chamar ScaffoldMessenger.of(context).showSnackBar() usando o context do Scaffold. Se usar um Builder ou Scaffold aninhado, o context pode estar errado.

P: Como definir cantos arredondados em um BottomSheet? R: Use a propriedade shape: shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))).

P: O que é o groupValue do Radio? R: É o valor atualmente selecionado, compartilhado por todos os Radios do grupo. Quando o valor de um Radio é igual ao groupValue, ele exibe o estado selecionado.

P: Qual a diferença entre Card e Container? R: Card tem elevação padrão (sombra) e cantos arredondados, em conformidade com as especificações do Material Design; Container é mais flexível mas não tem decoração padrão. Use Card quando precisar de aparência de cartão.

P: Qual a diferença entre pinned e floating do SliverAppBar? R: pinned=true mantém o AppBar fixo no topo durante a rolagem; floating=true mostra o AppBar imediatamente ao rolar para baixo. Ambos podem ser usados juntos.


📖 Resumo


📝 Exercícios

  1. Básico (dificuldade ⭐): Crie um Scaffold com um Drawer contendo 5 itens de menu (Início/Carrinho/Pedidos/Configurações/Sobre).
  2. Intermediário (dificuldade ⭐⭐): Implemente um painel de filtro BottomSheet com um Slider de preço e Checkboxes de categoria, mostrando resultados do filtro via Snackbar após confirmar.
  3. Desafio (dificuldade ⭐⭐⭐): Implemente uma página completa de detalhes do produto: SliverAppBar com imagem colapsável + informações do produto + BottomSheet de seleção de tamanho + feedback Snackbar de adicionar ao carrinho.

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