Construindo um E-commerce com Flutter: Autenticação, Checkout e Pedidos (Parte 4)
Neste capitulo, transformaremos nosso catálogo de produtos em um sistema de vendas real.
O que vamos implementar:
-
Fluxo de Autenticação: Telas de Login, Registro, “Esqueci minha senha” e Logout usando componentes modernos do Material 3.
-
Modelagem de Pedidos: Criar as coleções no banco Isar para registrar o histórico de compras.
-
Checkout (Resumo do Pedido): Uma tela para revisar os itens do carrinho, exibir o total e finalizar a compra.
-
Área Restrita: Uma tela de gestão para cadastrar novos produtos, acessível apenas após o login.
🔐 Passo 1: O Fluxo de Autenticação (Material 3)
Um bom E-commerce precisa identificar seus clientes. Vamos criar um fluxo de telas limpo e focado na conversão, tirando máximo proveito dos botões e campos do Material 3.
Crie lib/modules/auth/login_screen.dart:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
void _login() {
// Simulação de login: Se for admin@shop.com, vai para área de cadastro.
// Senão, vai para a loja (Home).
if (_emailController.text == 'admin@shop.com') {
context.go('/admin/products');
} else {
context.go('/home');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(Icons.shopping_bag, size: 80, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 24),
Text(
'Bem-vindo de volta!',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Campos Material 3
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'E-mail',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Senha',
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
),
// Esqueci a Senha
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => context.push('/auth/forgot-password'),
child: const Text('Esqueci minha senha'),
),
),
const SizedBox(height: 24),
FilledButton(
onPressed: _login,
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
child: const Text('ENTRAR', style: TextStyle(fontSize: 16)),
),
const SizedBox(height: 16),
OutlinedButton(
onPressed: () => context.push('/auth/register'),
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
child: const Text('CRIAR NOVA CONTA'),
),
],
),
),
),
);
}
}
Nota: As telas de Registro e Esqueci a Senha seguem a mesma estrutura visual com TextField e FilledButton. A de Recuperação pediria apenas o E-mail para enviar um link de reset.
O Gemini disse
Esta é a Parte 4 (e o grande fechamento) da nossa série E-commerce com Flutter.
Na Parte 3, criamos a vitrine da nossa loja, animações de detalhes dos produtos e a lógica do carrinho de compras em memória. Agora, vamos fechar o ciclo de vendas!
Vamos focar na segurança e na conversão: criar um fluxo completo de Autenticação (Login, Registro, Recuperação de Senha), revisar o Cadastro de Produtos (agora protegido para administradores) e, o mais importante, a Tela de Checkout, onde o usuário revisa o pedido e o salva no banco de dados Isar.
Construindo um E-commerce com Flutter: Autenticação, Checkout e Pedidos (Parte 4)
Neste artigo, transformaremos nosso catálogo de produtos em um sistema de vendas real.
O que vamos implementar:
-
Fluxo de Autenticação: Telas de Login, Registro, “Esqueci minha senha” e Logout usando componentes modernos do Material 3.
-
Modelagem de Pedidos: Criar as coleções no banco Isar para registrar o histórico de compras.
-
Checkout (Resumo do Pedido): Uma tela para revisar os itens do carrinho, exibir o total e finalizar a compra.
-
Área Restrita: Uma tela de gestão para cadastrar novos produtos, acessível apenas após o login.
🔐 Passo 1: O Fluxo de Autenticação (Material 3)
Um bom E-commerce precisa identificar seus clientes. Vamos criar um fluxo de telas limpo e focado na conversão, tirando máximo proveito dos botões e campos do Material 3.
Crie lib/modules/auth/login_screen.dart:
Dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
void _login() {
// Simulação de login: Se for admin@shop.com, vai para área de cadastro.
// Senão, vai para a loja (Home).
if (_emailController.text == 'admin@shop.com') {
context.go('/admin/products');
} else {
context.go('/home');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(Icons.shopping_bag, size: 80, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 24),
Text(
'Bem-vindo de volta!',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Campos Material 3
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'E-mail',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Senha',
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
),
// Esqueci a Senha
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => context.push('/auth/forgot-password'),
child: const Text('Esqueci minha senha'),
),
),
const SizedBox(height: 24),
FilledButton(
onPressed: _login,
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
child: const Text('ENTRAR', style: TextStyle(fontSize: 16)),
),
const SizedBox(height: 16),
OutlinedButton(
onPressed: () => context.push('/auth/register'),
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
child: const Text('CRIAR NOVA CONTA'),
),
],
),
),
),
);
}
}
Nota: As telas de Registro e Esqueci a Senha seguem a mesma estrutura visual com
TextFieldeFilledButton. A de Recuperação pediria apenas o E-mail para enviar um link de reset.
📦 Passo 2: Modelando o Pedido no Banco de Dados (Isar)
Quando o usuário clica em “Comprar”, precisamos salvar esse registro. Vamos criar o modelo ShopOrder e usar objetos embutidos (@embedded) para os itens do pedido, pois eles não precisam existir sozinhos fora de um pedido.
Crie lib/data/models/shop_order.dart:
import 'package:isar/isar.dart';
part 'shop_order.g.dart';
@collection
class ShopOrder {
Id id = Isar.autoIncrement;
late DateTime orderDate;
late double totalAmount;
late String customerEmail;
late String deliveryAddress;
// Status do pedido (Pendente, Pago, Enviado)
String status = 'Pendente';
// Usamos uma lista de objetos embutidos para salvar os itens exatamente como foram comprados
List<OrderItem> items = [];
}
// @embedded significa que este objeto vive apenas dentro do ShopOrder
@embedded
class OrderItem {
String? productName;
double? price;
int? quantity;
}
Lembre-se de rodar o comando: dart run build_runner build
🛒 Passo 3: A Tela de Checkout (Resumo do Pedido)
O Checkout é a tela onde o usuário revisa o que está no carrinho (CartService), vê o endereço de entrega e confirma.
Crie lib/modules/checkout/checkout_screen.dart:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:shop_app/data/services/cart_service.dart';
import 'package:shop_app/data/models/shop_order.dart';
import 'package:shop_app/main.dart'; // Acesso ao isar global
class CheckoutScreen extends StatefulWidget {
const CheckoutScreen({super.key});
@override
State<CheckoutScreen> createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State<CheckoutScreen> {
final CartService _cart = CartService();
bool _isProcessing = false;
Future<void> _finalizePurchase() async {
setState(() => _isProcessing = true);
// 1. Cria a estrutura do Pedido
final newOrder = ShopOrder()
..orderDate = DateTime.now()
..totalAmount = _cart.total
..customerEmail = 'cliente@teste.com' // Idealmente viria do seu AuthService
..deliveryAddress = 'Rua das Flores, 123 - Centro'
..items = _cart.items.map((cartItem) {
return OrderItem()
..productName = cartItem.product.name
..price = cartItem.product.price
..quantity = cartItem.quantity;
}).toList();
// 2. Salva no Banco de Dados Isar
await isar.writeTxn(() async {
await isar.shopOrders.put(newOrder);
});
// 3. Limpa o carrinho
_cart.clear();
// 4. Mostra sucesso e volta para a Home
setState(() => _isProcessing = false);
_showSuccessDialog();
}
void _showSuccessDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
icon: const Icon(Icons.check_circle, color: Colors.green, size: 64),
title: const Text('Pedido Confirmado!'),
content: const Text('Sua compra foi finalizada com sucesso. Agradecemos a preferência!'),
actions: [
FilledButton(
onPressed: () {
Navigator.pop(ctx); // Fecha o dialog
context.go('/home'); // Volta pra loja
},
child: const Text('VOLTAR PARA A LOJA'),
)
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Checkout')),
body: _cart.items.isEmpty
? const Center(child: Text('Seu carrinho está vazio.'))
: Column(
children: [
Expanded(
child: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _cart.items.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, index) {
final item = _cart.items[index];
return ListTile(
title: Text(item.product.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('Qtd: ${item.quantity}'),
trailing: Text('R\$ ${(item.product.price * item.quantity).toStringAsFixed(2)}'),
);
},
),
),
// Card de Resumo e Pagamento (Bottom)
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.vertical(top: Radius.circular(32)),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Total a Pagar', style: Theme.of(context).textTheme.titleMedium),
Text('R\$ ${_cart.total.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary
)),
],
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _isProcessing ? null : _finalizePurchase,
icon: _isProcessing
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Icon(Icons.lock),
label: Text(_isProcessing ? 'PROCESSANDO...' : 'FINALIZAR COMPRA SEGURA', style: const TextStyle(fontSize: 16)),
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
),
),
],
),
),
)
],
),
);
}
}
🛠️ Passo 4: A Área Administrativa Restrita
Lembra do Login no Passo 1? Se o usuário for administrador (admin@shop.com), ele é redirecionado para a tela de gerenciamento de produtos. Vamos criar um painel simples para ele.
Crie lib/modules/admin/admin_dashboard_screen.dart:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class AdminDashboardScreen extends StatelessWidget {
const AdminDashboardScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Painel do Administrador'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
tooltip: 'Sair (Logout)',
onPressed: () {
// Limpa o estado de auth e volta pro login
context.go('/auth/login');
},
)
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildAdminCard(
context,
icon: Icons.add_box,
title: 'Cadastrar Novo Produto',
subtitle: 'Adicionar fotos, preços e descrições',
onTap: () => context.push('/admin/product-form'), // Usa a tela criada na Parte 2
),
const SizedBox(height: 16),
_buildAdminCard(
context,
icon: Icons.inventory,
title: 'Meus Produtos',
subtitle: 'Editar e remover produtos existentes',
onTap: () => context.push('/admin/product-list'), // Usa a tela criada na Parte 2
),
const SizedBox(height: 16),
_buildAdminCard(
context,
icon: Icons.receipt_long,
title: 'Pedidos Realizados',
subtitle: 'Verifique as compras dos clientes',
onTap: () {
// Uma tela futura para listar os 'ShopOrder' do banco Isar
},
),
],
),
);
}
Widget _buildAdminCard(BuildContext context, {required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
return Card(
elevation: 2,
child: ListTile(
contentPadding: const EdgeInsets.all(16),
leading: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer, shape: BoxShape.circle),
child: Icon(icon, color: Theme.of(context).colorScheme.primary),
),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
),
);
}
}
✅ Conclusão do E-commerce
Nesta jornada de quatro artigos, você evoluiu de um projeto em branco para um aplicativo de Comércio Eletrônico Completo.
Recapitulando tudo que implementamos:
-
Design System & UX: Usamos o poder visual do Material 3, temas customizados e animações Hero.
-
Arquitetura de Dados: Aprendemos a salvar imagens localmente e gerenciar dados robustos offline com o Isar Database.
-
Gerenciamento de Estado: Criamos um carrinho de compras reativo e limpo.
-
Segurança e Fluxos: Separamos a visão do Comprador e do Administrador com fluxos de Autenticação e Checkout simulado de pedidos.
Com essa base, você está perfeitamente equipado para integrar APIs de pagamento reais (como Stripe ou Mercado Pago) e trocar o banco local por um backend em nuvem (como Firebase ou Supabase) no futuro.