Construindo um E-commerce com Flutter: Loja do Cliente, Carrinho e Material 3 (Parte 3)

Neste capitulo, vamos transformar nosso banco de dados em uma vitrine atraente. O que vamos implementar:

  1. Lógica do Carrinho: Um serviço para gerenciar o estado das compras.

  2. Tela Home (Vitrine): Carrossel de promoções e Grid de “Lançamentos”.

  3. Detalhes do Produto: Uso de animações Hero para transição de imagens e botão de compra.

  4. Cadastro de Cliente: Formulário completo com validação e estilo Material 3.


📦 Passo 1: Dependências e Lógica do Carrinho

Precisaremos de um pacote para o carrossel e uma forma simples de gerenciar o estado do carrinho. Adicione ao pubspec.yaml:

dependencies:
  # ... anteriores
  carousel_slider: ^4.2.1
  # Vamos usar ValueNotifier nativo para o estado, sem pacotes extras por enquanto.

Crie lib/data/services/cart_service.dart:

Vamos criar um Singleton para que o carrinho seja acessível em todo o app.

import 'package:flutter/material.dart';
import '../models/product.dart';

class CartItem {
  final Product product;
  int quantity;
  CartItem({required this.product, this.quantity = 1});
}

class CartService extends ChangeNotifier {
  // Singleton
  static final CartService _instance = CartService._internal();
  factory CartService() => _instance;
  CartService._internal();

  final List<CartItem> _items = [];

  List<CartItem> get items => _items;

  double get total => _items.fold(0, (sum, item) => sum + (item.product.price * item.quantity));

  void add(Product product) {
    final index = _items.indexWhere((i) => i.product.id == product.id);
    if (index >= 0) {
      _items[index].quantity++;
    } else {
      _items.add(CartItem(product: product));
    }
    notifyListeners(); // Avisa a UI para atualizar
  }

  void remove(int productId) {
    _items.removeWhere((item) => item.product.id == productId);
    notifyListeners();
  }
  
  void clear() {
    _items.clear();
    notifyListeners();
  }
}

🏠 Passo 2: Tela Home (A Vitrine)

Vamos criar uma tela rica visualmente. Usaremos CustomScrollView para ter um efeito de rolagem moderno.

Crie lib/modules/shop/store_home_screen.dart:

import 'dart:io';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:shop_app/data/models/product.dart';
import 'package:shop_app/data/services/product_service.dart';
import 'package:shop_app/data/services/cart_service.dart'; // Importe o carrinho
import 'package:shop_app/main.dart'; // isar global

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

  @override
  State<StoreHomeScreen> createState() => _StoreHomeScreenState();
}

class _StoreHomeScreenState extends State<StoreHomeScreen> {
  late ProductService _productService;
  List<Product> _products = [];
  final CartService _cartService = CartService();

  @override
  void initState() {
    super.initState();
    _productService = ProductService(isar);
    _loadProducts();
  }

  void _loadProducts() async {
    final data = await _productService.getAllProducts();
    setState(() => _products = data);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // 1. App Bar Flutuante
          SliverAppBar.large(
            title: const Text('Discover Shop'),
            actions: [
              // Ícone do Carrinho com Contador (AnimatedBuilder ouvinte)
              ListenableBuilder(
                listenable: _cartService,
                builder: (context, child) {
                  return Badge(
                    label: Text('${_cartService.items.length}'),
                    isLabelVisible: _cartService.items.isNotEmpty,
                    child: IconButton(
                      icon: const Icon(Icons.shopping_cart_outlined),
                      onPressed: () {
                         // Futuro: context.push('/cart');
                      },
                    ),
                  );
                },
              ),
              IconButton(
                icon: const Icon(Icons.person_outline),
                onPressed: () => context.push('/register'), // Vamos criar essa rota
              ),
            ],
          ),

          // 2. Carrossel de Promoções
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.symmetric(vertical: 16),
              child: CarouselSlider(
                options: CarouselOptions(height: 180.0, autoPlay: true, enlargeCenterPage: true),
                items: [1, 2, 3].map((i) {
                  return Builder(
                    builder: (BuildContext context) {
                      return Container(
                        width: MediaQuery.of(context).size.width,
                        margin: const EdgeInsets.symmetric(horizontal: 5.0),
                        decoration: BoxDecoration(
                          color: Theme.of(context).colorScheme.primaryContainer,
                          borderRadius: BorderRadius.circular(16),
                        ),
                        child: Center(
                            child: Text('Promoção $i\nAté 50% OFF', 
                            textAlign: TextAlign.center,
                            style: Theme.of(context).textTheme.headlineMedium)),
                      );
                    },
                  );
                }).toList(),
              ),
            ),
          ),

          // 3. Título da Seção
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text("Lançamentos", style: Theme.of(context).textTheme.titleLarge),
            ),
          ),

          // 4. Grid de Produtos
          SliverPadding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            sliver: SliverGrid(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                childAspectRatio: 0.75,
                mainAxisSpacing: 16,
                crossAxisSpacing: 16,
              ),
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  final product = _products[index];
                  return GestureDetector(
                    onTap: () => context.push('/product/${product.id}', extra: product),
                    child: Card(
                      elevation: 0, // Flat style (M3)
                      color: Theme.of(context).colorScheme.surfaceContainerHighest,
                      clipBehavior: Clip.antiAlias,
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Expanded(
                            child: Hero( // Animação Mágica
                              tag: 'product_${product.id}',
                              child: product.imagePath != null
                                  ? Image.file(File(product.imagePath!), fit: BoxFit.cover, width: double.infinity)
                                  : Container(color: Colors.grey, child: const Icon(Icons.image)),
                            ),
                          ),
                          Padding(
                            padding: const EdgeInsets.all(12.0),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(product.name, style: Theme.of(context).textTheme.titleMedium, maxLines: 1),
                                const SizedBox(height: 4),
                                Text(product.formattedPrice, 
                                     style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor)),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
                  );
                },
                childCount: _products.length,
              ),
            ),
          ),
          
          // Espaço extra no final
          const SliverToBoxAdapter(child: SizedBox(height: 40)),
        ],
      ),
    );
  }
}

✨ Passo 3: Detalhes do Produto (Animações e Carrinho)

Aqui usamos o Hero novamente com a mesma tag da Home. Isso faz a imagem “voar” de uma tela para a outra.

Crie lib/modules/shop/product_detail_screen.dart:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:shop_app/data/models/product.dart';
import 'package:shop_app/data/services/cart_service.dart';

class ProductDetailScreen extends StatelessWidget {
  final Product product;

  const ProductDetailScreen({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // AppBar transparente para a imagem subir
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        leading: const BackButton(style: ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.white54))),
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Imagem Gigante com Hero
            Hero(
              tag: 'product_${product.id}',
              child: Container(
                height: 400,
                width: double.infinity,
                decoration: BoxDecoration(
                  color: Colors.grey[200],
                  borderRadius: const BorderRadius.vertical(bottom: Radius.circular(32)),
                ),
                child: product.imagePath != null
                    ? ClipRRect(
                        borderRadius: const BorderRadius.vertical(bottom: Radius.circular(32)),
                        child: Image.file(File(product.imagePath!), fit: BoxFit.cover),
                      )
                    : const Icon(Icons.image, size: 100),
              ),
            ),
            
            Padding(
              padding: const EdgeInsets.all(24.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Expanded(
                        child: Text(product.name, style: Theme.of(context).textTheme.headlineMedium),
                      ),
                      Text(product.formattedPrice, style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor)),
                    ],
                  ),
                  const SizedBox(height: 16),
                  Text("Descrição", style: Theme.of(context).textTheme.titleMedium),
                  const SizedBox(height: 8),
                  Text(
                    product.description ?? "Sem descrição detalhada para este produto incrível.",
                    style: Theme.of(context).textTheme.bodyLarge,
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
      bottomNavigationBar: Container(
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Theme.of(context).colorScheme.surface,
          boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: const Offset(0, -5))],
        ),
        child: SafeArea(
          child: FilledButton.icon(
            onPressed: () {
              CartService().add(product);
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('${product.name} adicionado ao carrinho!')),
              );
            },
            icon: const Icon(Icons.shopping_bag),
            label: const Text("ADICIONAR AO CARRINHO", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
          ),
        ),
      ),
    );
  }
}

📝 Passo 4: Cadastro de Cliente (Material 3)

Um formulário bonito para capturar os dados do cliente.

Crie lib/modules/checkout/customer_register_screen.dart:

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Criar Conta')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text("Seus Dados", style: Theme.of(context).textTheme.headlineSmall),
            const SizedBox(height: 20),
            
            // Campos com Design Material 3
            _buildTextField(label: "Nome Completo", icon: Icons.person),
            const SizedBox(height: 16),
            _buildTextField(label: "E-mail", icon: Icons.email, type: TextInputType.emailAddress),
            const SizedBox(height: 16),
            _buildTextField(label: "CPF", icon: Icons.badge, type: TextInputType.number),
            
            const SizedBox(height: 32),
            Text("Endereço de Entrega", style: Theme.of(context).textTheme.headlineSmall),
            const SizedBox(height: 20),
            
            Row(
              children: [
                Expanded(child: _buildTextField(label: "CEP", icon: Icons.map)),
                const SizedBox(width: 16),
                Expanded(flex: 2, child: _buildTextField(label: "Cidade", icon: Icons.location_city)),
              ],
            ),
            const SizedBox(height: 16),
            _buildTextField(label: "Rua e Número", icon: Icons.home),
            
            const SizedBox(height: 40),
            SizedBox(
              width: double.infinity,
              child: FilledButton(
                onPressed: () {
                  // Lógica de salvar cliente (Isar)
                },
                style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
                child: const Text("CRIAR MINHA CONTA"),
              ),
            )
          ],
        ),
      ),
    );
  }

  Widget _buildTextField({required String label, required IconData icon, TextInputType? type}) {
    return TextFormField(
      keyboardType: type,
      decoration: InputDecoration(
        labelText: label,
        prefixIcon: Icon(icon),
        border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
        filled: true,
        // fillColor automático do M3
      ),
    );
  }
}

🧭 Passo 5: Atualizando as Rotas

Adicione as novas telas ao app_router.dart. Lembre-se que estas telas geralmente ficam fora do ShellRoute (Menu Lateral) se quisermos que elas cubram a tela inteira (Full Screen) ou tenham navegação própria de retorno.

No app_router.dart:

// ...
// Adicione fora do ShellRoute ou dentro, dependendo se quer manter o menu
GoRoute(
  path: '/product/:id',
  builder: (context, state) {
    final product = state.extra as Product; // Passamos o objeto direto
    return ProductDetailScreen(product: product);
  },
),
GoRoute(
  path: '/register',
  builder: (context, state) => const CustomerRegisterScreen(),
),
// Atualize a rota /home para usar a StoreHomeScreen em vez da HomeScreen antiga

✅ Conclusão

Nesta Parte 3, o app ganhou cara de loja profissional:

  1. Vitrine Atrativa: Carrossel e Grid com design limpo.

  2. User Experience (UX): A animação Hero ao clicar no produto cria uma sensação de continuidade muito agradável.

  3. Carrinho Global: O CartService gerencia as compras em memória.

  4. Formulários M3: O cadastro de cliente utiliza as diretrizes mais novas do Material Design.

Próximos Passos: Na parte final, implementaremos a tela de Checkout (Resumo do Pedido) e simularemos a finalização da compra gerando um pedido no banco de dados Isar! 🚀🛍️

Please follow and like us:
error0
fb-share-icon
Tweet 20
fb-share-icon20