Construindo um E-commerce com Flutter: Splash Screen, BLoC e Atualizações (Parte Final)

Este ultimo capitulo irá mostrar como criar um projeto voltado a Comércio de Vendas Online, usando o Flutter como Framework e sua Linguagem Dart. A partir disso, irei demonstrar os novos passos e atualizações, como a Tela de Splash Screen (Primeira Tela), a atualização das telas com novos códigos baseados a partir do Flutter 3.41, a refatoração para utilizar o BLoC (Business Logic Component) em todos os fluxos, e uma revisão geral do projeto, utilizando o poder do Material 3.

Vamos finalizar esse projeto com chave de ouro!


📦 Passo 1: Atualizando Dependências e Preparando o Terreno

Para a nossa refatoração, vamos substituir o gerenciamento de estado simples pelo ecossistema do BLoC. Adicione ao seu pubspec.yaml:

dependencies:
  # ... dependências anteriores (isar, go_router, etc.)
  flutter_bloc: ^8.1.5
  equatable: ^2.0.5
  flutter_animate: ^4.5.0 # Para animações fluidas na Splash Screen

Rode flutter pub get para atualizar.


✨ Passo 2: A Tela de Splash Screen (A Primeira Impressão)

A Splash Screen é a primeira tela que o usuário vê. Ela serve para carregar dependências iniciais (como inicializar o Isar ou verificar se o usuário está logado) enquanto exibe a marca do app.

Crie lib/modules/splash/splash_screen.dart:

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart';

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

  @override
  State<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  @override
  void initState() {
    super.initState();
    _initializeApp();
  }

  Future<void> _initializeApp() async {
    // Simula o tempo de carregamento de dependências, banco Isar, tokens, etc.
    await Future.delayed(const Duration(seconds: 3));
    
    // Após carregar, navega para a Home (ou Login, dependendo do estado da autenticação)
    if (mounted) {
      context.go('/home');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.primary,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.shopping_bag, size: 100, color: Colors.white)
                .animate()
                .scale(duration: 600.ms, curve: Curves.easeOutBack)
                .then()
                .shimmer(duration: 1200.ms, color: Colors.white54),
            const SizedBox(height: 24),
            Text(
              'Flutter Shop',
              style: Theme.of(context).textTheme.displaySmall?.copyWith(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                  ),
            ).animate().fadeIn(delay: 400.ms).slideY(begin: 0.5, end: 0),
          ],
        ),
      ),
    );
  }
}

Nota para o app_router.dart: Atualize a initialLocation para '/splash' e crie a rota correspondente para esta tela.


🧠 Passo 3: Refatorando para BLoC (Padrão Ouro do Mercado)

Na Parte 3, usamos um ChangeNotifier simples para o Carrinho. Com o crescimento do app, o BLoC garante uma separação estrita entre UI e Lógica de Negócios, facilitando testes e escalabilidade.

Vamos refatorar o Carrinho de Compras.

3.1 Eventos do Carrinho (cart_event.dart)

O que pode acontecer no carrinho?

import 'package:equatable/equatable.dart';
import '../../data/models/product.dart';

sealed class CartEvent extends Equatable {
  const CartEvent();
  @override
  List<Object> get props => [];
}

class AddProductToCart extends CartEvent {
  final Product product;
  const AddProductToCart(this.product);
  @override
  List<Object> get props => [product];
}

class RemoveProductFromCart extends CartEvent {
  final int productId;
  const RemoveProductFromCart(this.productId);
  @override
  List<Object> get props => [productId];
}

class ClearCart extends CartEvent {}

3.2 Estados do Carrinho (cart_state.dart)

Como a UI deve reagir? Usando as facilidades do Dart moderno, tornamos o estado imutável.

import 'package:equatable/equatable.dart';
import '../../data/models/product.dart';

// Modelo auxiliar para os itens
class CartItem extends Equatable {
  final Product product;
  final int quantity;

  const CartItem({required this.product, this.quantity = 1});

  CartItem copyWith({int? quantity}) {
    return CartItem(product: product, quantity: quantity ?? this.quantity);
  }

  @override
  List<Object> get props => [product, quantity];
}

class CartState extends Equatable {
  final List<CartItem> items;
  final double total;

  const CartState({this.items = const [], this.total = 0.0});

  CartState copyWith({List<CartItem>? items, double? total}) {
    return CartState(
      items: items ?? this.items,
      total: total ?? this.total,
    );
  }

  @override
  List<Object> get props => [items, total];
}

3.3 A Lógica do BLoC (cart_bloc.dart)

import 'package:flutter_bloc/flutter_bloc.dart';
import 'cart_event.dart';
import 'cart_state.dart';

class CartBloc extends Bloc<CartEvent, CartState> {
  CartBloc() : super(const CartState()) {
    on<AddProductToCart>(_onAddProduct);
    on<RemoveProductFromCart>(_onRemoveProduct);
    on<ClearCart>((event, emit) => emit(const CartState()));
  }

  void _onAddProduct(AddProductToCart event, Emitter<CartState> emit) {
    final List<CartItem> updatedItems = List.from(state.items);
    final index = updatedItems.indexWhere((i) => i.product.id == event.product.id);

    if (index >= 0) {
      updatedItems[index] = updatedItems[index].copyWith(quantity: updatedItems[index].quantity + 1);
    } else {
      updatedItems.add(CartItem(product: event.product));
    }

    final newTotal = _calculateTotal(updatedItems);
    emit(state.copyWith(items: updatedItems, total: newTotal));
  }

  void _onRemoveProduct(RemoveProductFromCart event, Emitter<CartState> emit) {
    final updatedItems = state.items.where((i) => i.product.id != event.productId).toList();
    emit(state.copyWith(items: updatedItems, total: _calculateTotal(updatedItems)));
  }

  double _calculateTotal(List<CartItem> items) {
    return items.fold(0, (sum, item) => sum + (item.product.price * item.quantity));
  }
}

⚡ Passo 4: Atualizações Flutter 3.41+ e Integração da UI

As versões mais recentes do Flutter e Dart trazem otimizações no motor gráfico (Impeller) e sintaxes mais curtas (como o Pattern Matching com switch expressions). Vamos atualizar nossa main.dart para injetar o BLoC globalmente usando o Material 3.

Atualizando o main.dart:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'core/theme/app_theme.dart';
import 'router/app_router.dart';
import 'blocs/cart/cart_bloc.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Inicialização do Isar ocorreria aqui...

  runApp(
    // MultiBlocProvider permite adicionar mais BLoCs no futuro (ex: AuthBloc, ThemeBloc)
    MultiBlocProvider(
      providers: [
        BlocProvider<CartBloc>(create: (_) => CartBloc()),
      ],
      child: const ShopApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter E-commerce',
      theme: AppTheme.lightTheme, // Nosso Design System Material 3
      routerConfig: appRouter,
      debugShowCheckedModeBanner: false,
    );
  }
}

Atualizando a StoreHomeScreen (Ouvindo o BLoC):

Em vez de usar o antigo ListenableBuilder, utilizamos os poderosos widgets do flutter_bloc. O Flutter moderno preza por código declarativo e limpo.

// Na StoreHomeScreen, atualize a seção da SliverAppBar:

SliverAppBar.large(
  title: const Text('Discover Shop'),
  actions: [
    // BlocBuilder reconstrói apenas o ícone do carrinho quando o estado muda
    BlocBuilder<CartBloc, CartState>(
      builder: (context, state) {
        return Badge(
          label: Text('${state.items.length}'),
          isLabelVisible: state.items.isNotEmpty,
          child: IconButton(
            icon: const Icon(Icons.shopping_cart_outlined),
            onPressed: () => context.push('/checkout'),
          ),
        );
      },
    ),
    IconButton(
      icon: const Icon(Icons.person_outline),
      onPressed: () => context.push('/auth/login'),
    ),
  ],
),

Atualizando a Ação de Compra (Exemplo no Detalhe do Produto):

// Dentro do ProductDetailScreen, ao clicar em "Adicionar ao Carrinho":
FilledButton.icon(
  onPressed: () {
    // Dispara o Evento para o BLoC
    context.read<CartBloc>().add(AddProductToCart(product));
    
    // SnackBar nativo do M3
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('${product.name} adicionado ao carrinho!'),
        behavior: SnackBarBehavior.floating, // Estilo moderno
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
      ),
    );
  },
  icon: const Icon(Icons.shopping_bag),
  label: const Text("ADICIONAR AO CARRINHO"),
)

🏁 Revisão Geral do Projeto (Finalizando)

Chegamos ao fim da nossa série de E-commerce. Vamos olhar para a arquitetura sólida que construímos:

  1. Fundação: Usamos Isar para um banco de dados local ultrarrápido (que pode ser facilmente espelhado para a nuvem futuramente).

  2. Roteamento: Implementamos o GoRouter, lidando com Deep Links, abas persistentes (Menu Lateral) e passagens de rotas complexas.

  3. Interface de Usuário: Aplicamos rigorosamente as diretrizes do Material 3, utilizando Slivers, animações Hero, e uma tipografia unificada com Google Fonts.

  4. Autenticação e Permissões: Separamos a visão do lojista (Admin) da visão do consumidor final.

  5. Gerenciamento de Estado Sênior: Finalizamos migrando toda a lógica de negócio para o BLoC, padrão amplamente exigido em vagas de nível Pleno e Sênior no mercado.

Próximos Passos (Para Você)

O código está pronto e estruturado. Se você deseja evoluir este projeto para seu portfólio ou para produção, os próximos passos ideais seriam:

  • Integrar uma API de Pagamento Real (Stripe ou Mercado Pago).

  • Adicionar chamadas HTTP (usando o pacote dio) se quiser separar o catálogo em um backend Node.js, Python ou Supabase.

  • Escrever testes unitários para o CartBloc usando a biblioteca bloc_test.

Desenvolver em Flutter é como montar um quebra-cabeça perfeito, onde a performance nativa encontra um design espetacular.

Obrigado por acompanhar esta série técnica! Qual será o seu próximo grande aplicativo? 🚀

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