Flutter: Prática Abrangente da Fase 1

Aprender sem praticar é como comprar ingredientes e nunca cozinhar — esta aula transforma todo o conhecimento das primeiras 6 aulas em um banquete completo do ShopApp.

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

1. O Que Você Vai Aprender


2. Uma História Real do Zero ao Um

(1) O Problema: Conhecimento Fragmentado

Bob concluiu as primeiras 6 aulas — consegue escrever Widgets, criar layouts e lidar com interações. Mas ao montar esse conhecimento em uma aplicação completa, não sabia por onde começar. Como organizar arquivos? Como passar dados? Como navegar entre páginas? Era como ter peças de um quebra-cabeça mas não conseguir completar a imagem.

(2) A Solução Baseada em Projeto

Ao construir a edição inicial do ShopApp, o conhecimento fragmentado é conectado em uma cadeia completa: modelo de dados → exibição em lista → interação → navegação de páginas → gerenciamento de estado.

(3) O Resultado: De "Sabe Escrever Componentes" a "Sabe Construir Projetos"

Após completar a edição inicial do ShopApp, Bob ganhou a capacidade de construir independentemente aplicações Flutter completas. A Fase 2 requer apenas substituir o gerenciamento de estado e as camadas de rede.


3. Planejamento da Estrutura do Projeto

(1) Estrutura de Diretórios

TEXT
shop_app/
├── lib/
│   ├── main.dart              # App entry point
│   ├── models/
│   │   └── product.dart       # Product data model
│   ├── widgets/
│   │   ├── product_card.dart  # Reusable product card
│   │   └── quantity_selector.dart
│   ├── screens/
│   │   ├── home_page.dart     # Product grid
│   │   ├── detail_page.dart   # Product detail
│   │   └── cart_page.dart     # Shopping cart
│   └── data/
│       └── mock_products.dart # Mock data generator
├── pubspec.yaml
└── test/
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) Princípios de Camadas

Camada Responsabilidade Direção de Dependência
models Definições de dados puros, sem UI Sem dependências
widgets Componentes de UI reutilizáveis Depende de models
screens Widgets de nível de página Depende de models + widgets
data Provedores de dados (mock/API) Depende de models
main Entrada do app + roteamento Depende de screens

4. Modelo de Dados e Dados Simulados

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

: Modelo de dados Product

DART
class Product {
  final int id;
  final String name;
  final double price;
  final String imageUrl;
  final double rating;
  final int reviewCount;
  final String category;

  const Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
    this.rating = 0.0,
    this.reviewCount = 0,
    this.category = 'General',
  });
}

Esta classe Product será usada em todos os blocos de código desta aula.

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.

: Gerador de dados simulados

DART
// Product class definition shown above

class MockProducts {
  static final List<String> _names = [
    'Pro Laptop 16"', 'Wireless Earbuds', 'Smart Watch Pro',
    'Cotton T-Shirt', 'Running Shoes', 'Backpack Ultra',
    'Coffee Maker', 'Desk Lamp LED', 'Yoga Mat Premium',
    'Novel Collection', 'Wireless Mouse', 'USB-C Hub',
  ];

  static final List<String> _categories = [
    'Electronics', 'Clothing', 'Home', 'Books', 'Sports',
  ];

  static List<Product> generate({int count = 1000}) {
    return List.generate(count, (i) => Product(
      id: i + 1,
      name: _names[i % _names.length],
      price: (9.99 + (i % 50) * 10).toDouble(),
      imageUrl: 'https://cdn.shopapp.com/products/item_${i + 1}.jpg',
      rating: (3.0 + (i % 20) * 0.1),
      reviewCount: (i % 5) * 100,
      category: _categories[i % _categories.length],
    ));
  }
}
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. Página Inicial: Grade de Produtos

▶ 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 da HomePage

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

// Product class definition shown above

// Simplified class definition
class CartItem {
  final Product product;
  final int quantity;
  const CartItem({required this.product, required this.quantity});
  CartItem copyWith({Product? product, int? quantity}) =>
      CartItem(product: product ?? this.product, quantity: quantity ?? this.quantity);
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<Product> _products = MockProducts.generate(count: 1000);
  String _selectedCategory = 'All';

  List<Product> get _filtered => _selectedCategory == 'All'
      ? _products
      : _products.where((p) => p.category == _selectedCategory).toList();

  @override
  Widget build(BuildContext context) {
    final categories = ['All', ...MockProducts._categories.toSet()];
    return Scaffold(
      appBar: AppBar(
        title: const Text('ShopApp'),
        actions: [
          Stack(
            alignment: Alignment.center,
            children: [
              IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () => _navigateToCart()),
              if (_cartItems.isNotEmpty)
                Positioned(right: 4, top: 4,
                  child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
                    child: Text('${_cartItems.length}', style: const TextStyle(fontSize: 9, color: Colors.white)))),
            ],
          ),
        ],
      ),
      body: Column(
        children: [
          // Category filter chips
          SizedBox(
            height: 48,
            child: ListView(
              scrollDirection: Axis.horizontal,
              padding: const EdgeInsets.symmetric(horizontal: 8),
              children: categories.map((cat) => Padding(
                padding: const EdgeInsets.symmetric(horizontal: 4),
                child: ChoiceChip(
                  label: Text(cat),
                  selected: cat == _selectedCategory,
                  onSelected: (_) => setState(() => _selectedCategory = cat),
                ),
              )).toList(),
            ),
          ),
          // Product grid
          Expanded(
            child: GridView.builder(
              padding: const EdgeInsets.all(8),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                mainAxisSpacing: 8,
                crossAxisSpacing: 8,
                childAspectRatio: 0.68,
              ),
              itemCount: _filtered.length,
              itemBuilder: (context, index) => ProductCard(
                key: ValueKey(_filtered[index].id),
                product: _filtered[index],
                onAddToCart: () => _addToCart(_filtered[index]),
                onTap: () => _navigateToDetail(_filtered[index]),
              ),
            ),
          ),
        ],
      ),
    );
  }

  final List<CartItem> _cartItems = [];

  void _addToCart(Product product) {
    setState(() {
      final idx = _cartItems.indexWhere((c) => c.product.id == product.id);
      if (idx >= 0) {
        _cartItems[idx] = _cartItems[idx].copyWith(quantity: _cartItems[idx].quantity + 1);
      } else {
        _cartItems.add(CartItem(product: product, quantity: 1));
      }
    });
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('${product.name} added to cart'), duration: const Duration(seconds: 1)),
    );
  }

  void _navigateToCart() {
    Navigator.push(context, MaterialPageRoute(
      builder: (_) => CartPage(items: _cartItems, onUpdate: (items) => setState(() => _cartItems.clear()..addAll(items))),
    ));
  }

  void _navigateToDetail(Product product) {
    Navigator.push(context, MaterialPageRoute(
      builder: (_) => DetailPage(product: product, onAddToCart: () => _addToCart(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.

6. Página do Carrinho

▶ 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 da CartPage

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

// Product class definition shown above

class CartItem {
  final Product product;
  final int quantity;
  const CartItem({required this.product, required this.quantity});
  double get total => product.price * quantity;
  CartItem copyWith({Product? product, int? quantity}) =>
      CartItem(product: product ?? this.product, quantity: quantity ?? this.quantity);
}

class CartPage extends StatefulWidget {
  final List<CartItem> items;
  final ValueChanged<List<CartItem>> onUpdate;
  const CartPage({super.key, required this.items, required this.onUpdate});

  @override
  State<CartPage> createState() => _CartPageState();
}

class _CartPageState extends State<CartPage> {
  late List<CartItem> _items;

  @override
  void initState() {
    super.initState();
    _items = List.from(widget.items);
  }

  double get _total => _items.fold(0.0, (sum, item) => sum + item.total);

  void _updateQty(int index, int qty) {
    setState(() {
      if (qty <= 0) _items.removeAt(index);
      else _items[index] = _items[index].copyWith(quantity: qty);
    });
    widget.onUpdate(_items);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Cart (${_items.length})')),
      body: _items.isEmpty
          ? const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
              Icon(Icons.shopping_cart_outlined, size: 64, color: Colors.grey),
              SizedBox(height: 16),
              Text('Your cart is empty', style: TextStyle(fontSize: 18, color: Colors.grey)),
            ]))
          : Column(children: [
              Expanded(child: ListView.separated(
                itemCount: _items.length,
                separatorBuilder: (_, __) => const Divider(),
                itemBuilder: (context, i) {
                  final item = _items[i];
                  return ListTile(
                    leading: Image.network(item.product.imageUrl, width: 50, height: 50, fit: BoxFit.cover),
                    title: Text(item.product.name),
                    subtitle: Text('$${item.total.toStringAsFixed(2)}'),
                    trailing: Row(mainAxisSize: MainAxisSize.min, children: [
                      IconButton(icon: const Icon(Icons.remove), onPressed: () => _updateQty(i, item.quantity - 1)),
                      Text('${item.quantity}'),
                      IconButton(icon: const Icon(Icons.add), onPressed: () => _updateQty(i, item.quantity + 1)),
                    ]),
                  );
                },
              )),
              // Total bar
              Container(padding: const EdgeInsets.all(16), color: Colors.grey[100],
                child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
                  Text('Total: $${_total.toStringAsFixed(2)}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                  FilledButton(onPressed: () {}, child: const Text('Checkout')),
                ])),
            ]),
    );
  }
}
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. Página de Detalhes do Produto

▶ 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 da DetailPage

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

// Product class definition shown above

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(slivers: [
        SliverAppBar(expandedHeight: 300, pinned: true,
          flexibleSpace: FlexibleSpaceBar(
            title: Text(product.name, style: const TextStyle(fontSize: 14, shadows: [Shadow(color: Colors.black54)])),
            background: Image.network(product.imageUrl, fit: BoxFit.cover),
          ),
        ),
        SliverToBoxAdapter(child: Padding(padding: const EdgeInsets.all(16), child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(product.category, style: TextStyle(color: Colors.grey[600])),
            const SizedBox(height: 4),
            Row(children: [
              Text('$${product.price.toStringAsFixed(2)}', style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.green)),
              const SizedBox(width: 8),
              Icon(Icons.star, color: Colors.amber[700], size: 16),
              Text(' ${product.rating} (${product.reviewCount})'),
            ]),
            const SizedBox(height: 16),
            const Text('Description', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
            const SizedBox(height: 8),
            const Text('Premium quality product. Free shipping on orders over $50. USD settlement supported.'),
          ],
        ))),
      ]),
      bottomNavigationBar: SafeArea(child: Padding(padding: const EdgeInsets.all(12), child: Row(children: [
        Expanded(child: FilledButton.icon(onPressed: onAddToCart,
          icon: const Icon(Icons.shopping_cart), label: const Text('Add to Cart - $${product.price}'))),
      ]))),
    );
  }
}
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.

8. Exemplo Completo: ShopApp Edição Inicial main.dart

DART
import 'package:flutter/material.dart';
import 'models/product.dart';
import 'data/mock_products.dart';
import 'screens/home_page.dart';

void main() => runApp(const ShopAppStarter);

const ShopAppStarter = ShopApp();

class ShopApp extends StatelessWidget {
  const ShopApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ShopApp Starter',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.blue,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}
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: Com tantos arquivos no projeto, é fácil errar nos caminhos de import? R: Use o auto-completar de caminhos relativos do VS Code. Convenção: models/ terminam com model.dart, widgets/ são nomeados pelo componente.

P: Os dados do carrinho passados via setState para outra página são perdidos ao retornar? R: Atualmente usando funções de callback. A Fase 2 usará Riverpod para gerenciamento de estado global para resolver isso.

P: Um GridView com 1000 itens ficará lento? R: GridView.builder carrega preguiçosamente apenas os itens visíveis — 1000 itens não é problema. Se cada item for complexo, considere a otimização itemExtent.

P: Imagens dos dados simulados não carregam? R: Os exemplos usam URLs de placeholder. No desenvolvimento real, substitua por placeholder: AssetImage() ou Icons.

P: Como passar parâmetros com navegação Navigator.push e retornos? R: Passe via construtor ao navegar; retorne via Navigator.pop(context, result) e receba com await.

P: O código da Fase 1 pode evoluir diretamente para a Fase 2? R: Sim. As camadas de modelo de dados e Widget permanecem praticamente inalteradas — basta substituir setState por Riverpod Provider.


📖 Resumo


📝 Exercícios

  1. Básico (dificuldade ⭐): Crie o projeto seguindo a estrutura do tutorial e garanta que a HomePage exiba 20 cartões de produto.
  2. Intermediário (dificuldade ⭐⭐): Adicione funcionalidade de busca à HomePage: digite palavras-chave para filtrar nomes de produtos, usando setState para atualizar a lista.
  3. Desafio (dificuldade ⭐⭐⭐): Implemente o fluxo completo de navegação de três páginas: Início → Detalhes → Carrinho, com o número do badge na AppBar atualizando automaticamente ao retornar do carrinho após modificar quantidades.

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