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
- Aula 1: Introdução ao Flutter e Configuração do Ambiente
- Aula 2: Curso Intensivo da Linguagem Dart
- Aula 3: Fundamentos de Widgets
- Aula 4: Sistema de Layout
- Aula 5: StatefulWidget e Interação
- Aula 6: Material Design e Componentes Comuns
1. O Que Você Vai Aprender
- Planejamento de estrutura de projeto: camadas models / widgets / screens sob lib/
- Modelo de dados do produto: id / name / price(USD) / imageUrl / rating
- Página inicial com GridView exibindo 1000+ cartões de produto (dados simulados)
- Página do carrinho: StatefulWidget gerenciando quantidades e cálculo do preço total
- Página de detalhes do produto: SliverAppBar + BottomSheet de seleção de tamanho
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
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/
> 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
> 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
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.
> 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.
: Gerador de dados simulados
// 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],
));
}
}
> 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
> 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
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)),
));
}
}
> 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
> 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
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')),
])),
]),
);
}
}
> 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
> 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
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}'))),
]))),
);
}
}
> 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
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(),
);
}
}
> 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 comawait.
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
- Camadas do projeto: models (dados) / widgets (componentes) / screens (páginas) / data (fontes de dados)
- Dados simulados para validação rápida de UI, substitua por API real depois
- GridView.builder com carregamento preguiçoso suporta listas de milhares de produtos
- Carrinho gerenciado com StatefulWidget + setState, dados passados via callbacks
- SliverAppBar cria experiência imersiva na página de detalhes
📝 Exercícios
- Básico (dificuldade ⭐): Crie o projeto seguindo a estrutura do tutorial e garanta que a HomePage exiba 20 cartões de produto.
- Intermediário (dificuldade ⭐⭐): Adicione funcionalidade de busca à HomePage: digite palavras-chave para filtrar nomes de produtos, usando setState para atualizar a lista.
- 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.