Construindo um App de Controle de Veículos com Flutter: Dashboard, BLoC Pattern e Rotas com GoRouter (Parte 5)

Neste artigo, vamos transformar a estrutura do CarControl. Introduziremos um Dashboard completo, novas entidades (Marca e Categoria) e refatoraremos o coração do app usando BLoC e GoRouter.

📦 Passo 1: Novas Dependências

Adicione as bibliotecas essenciais para BLoC e comparação de objetos no pubspec.yaml:

dependencies:
  flutter_bloc: ^8.1.3
  equatable: ^2.0.5 # Ajuda a comparar estados do BLoC
  # ... isar, go_router (já adicionados anteriormente)

🏗️ Passo 2: Modelagem de Dados (Marca e Categoria)

Vamos expandir nosso banco de dados Isar criando as novas entidades solicitadas.

Crie/Atualize lib/models/marca.dart:

import 'package:isar/isar.dart';
part 'marca.g.dart';

@collection
class Marca {
  Id id = Isar.autoIncrement;

  late String nome;
  String? descricao;
  String? logoPath; // Caminho da imagem do logo
  int quantidadeVeiculos; // Campo calculado ou estático
  List<String>? imagensLoja; // Lista de caminhos de imagens

  Marca({
    required this.nome,
    this.descricao,
    this.logoPath,
    this.quantidadeVeiculos = 0,
    this.imagensLoja,
  });
}

Crie/Atualize lib/models/categoria.dart:

import 'package:isar/isar.dart';
part 'categoria.g.dart';

@collection
class Categoria {
  Id id = Isar.autoIncrement;

  late String nome; // Ex: SUV, Sedan, Hatch
  String? descricao;
  List<String>? imagensExemplo;

  Categoria({
    required this.nome,
    this.descricao,
    this.imagensExemplo,
  });
}

Não esqueça de rodar dart run build_runner build para gerar os arquivos .g.dart.


🧠 Passo 3: Gerenciamento de Estado com BLoC

O BLoC separa a UI da Lógica de Negócios através de Eventos (o que o usuário faz) e Estados (como a tela deve ficar).

Vamos criar o DashboardBloc para gerenciar a tela principal.

3.1 Os Eventos (dashboard_event.dart)

Definimos o que pode acontecer na Dashboard. Por enquanto, apenas o carregamento inicial.

part of 'dashboard_bloc.dart';

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

class LoadDashboardData extends DashboardEvent {}

3.2 Os Estados (dashboard_state.dart)

Definimos como a UI reage. Ela pode estar carregando, carregada com dados ou com erro.

part of 'dashboard_bloc.dart';

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

class DashboardLoading extends DashboardState {}

class DashboardLoaded extends DashboardState {
  final List<Marca> marcas;
  final List<Categoria> categorias;
  final List<Carro> carrosDestaque;

  const DashboardLoaded({
    this.marcas = const [],
    this.categorias = const [],
    this.carrosDestaque = const [],
  });

  @override
  List<Object> get props => [marcas, categorias, carrosDestaque];
}

class DashboardError extends DashboardState {
  final String message;
  const DashboardError(this.message);
}

3.3 A Lógica (dashboard_bloc.dart)

Aqui a mágica acontece. O BLoC recebe o evento LoadDashboardData, vai no repositório buscar os dados e emite o estado DashboardLoaded.

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../repositories/isar_repository.dart'; // Seu repositório
import '../models/marca.dart';
import '../models/categoria.dart';
import '../models/carro.dart';

part 'dashboard_event.dart';
part 'dashboard_state.dart';

class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
  final IsarRepository repository;

  DashboardBloc(this.repository) : super(DashboardLoading()) {
    on<LoadDashboardData>(_onLoadData);
  }

  Future<void> _onLoadData(LoadDashboardData event, Emitter<DashboardState> emit) async {
    emit(DashboardLoading());
    try {
      // Simulando busca em repositório (Você deve implementar esses métodos no IsarRepository)
      // final marcas = await repository.getMarcas(); 
      // final categorias = await repository.getCategorias();
      final carros = await repository.getCarros(); 
      
      // Mockando dados de Marcas/Categorias para o exemplo funcionar imediatamente
      final marcasMock = [Marca(nome: 'Toyota', quantidadeVeiculos: 12)];
      final categoriasMock = [Categoria(nome: 'SUV')];

      emit(DashboardLoaded(
        marcas: marcasMock,
        categorias: categoriasMock,
        carrosDestaque: carros,
      ));
    } catch (e) {
      emit(DashboardError("Erro ao carregar dashboard: $e"));
    }
  }
}

🧭 Passo 4: Implementando e Explicando o GoRouter

O GoRouter é a solução moderna de navegação. Ele permite controle total via URLs, redirecionamentos e rotas aninhadas.

Configure o lib/router/app_router.dart:

import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';
import '../screens/dashboard_screen.dart';
import '../screens/add_carro_screen.dart';
// import outras telas...

final appRouter = GoRouter(
  initialLocation: '/dashboard',
  routes: [
    // Rota Principal (Dashboard)
    GoRoute(
      path: '/dashboard',
      name: 'dashboard',
      builder: (context, state) => const DashboardScreen(),
      routes: [
        // Rota Filha: Cadastro de Carro
        // A URL será /dashboard/add-carro
        GoRoute(
          path: 'add-carro',
          name: 'add_carro',
          builder: (context, state) => const AddCarroScreen(),
        ),
        // Aqui viriam rotas para detalhes de Marca e Categoria
      ],
    ),
  ],
  errorBuilder: (context, state) => const Scaffold(
    body: Center(child: Text('Página não encontrada!')),
  ),
);

Por que usar isso?

  1. Deep Linking: Se o usuário abrir um link myapp://dashboard/add-carro, ele cai direto na tela certa.

  2. Organização: As rotas ficam centralizadas, não espalhadas em Navigator.push.

📊 Passo 5: A Nova Tela de Dashboard (UI)

Agora vamos criar a tela que consome o BLoC e exibe as 3 áreas solicitadas: Categorias, Destaques e Lista.

Crie lib/screens/dashboard_screen.dart:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../blocs/dashboard/dashboard_bloc.dart';

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

  @override
  State<DashboardScreen> createState() => _DashboardScreenState();
}

class _DashboardScreenState extends State<DashboardScreen> {
  @override
  void initState() {
    super.initState();
    // Dispara o evento de carregar dados assim que a tela abre
    context.read<DashboardBloc>().add(LoadDashboardData());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('CarControl Dashboard')),
      body: BlocBuilder<DashboardBloc, DashboardState>(
        builder: (context, state) {
          if (state is DashboardLoading) {
            return const Center(child: CircularProgressIndicator());
          } else if (state is DashboardError) {
            return Center(child: Text(state.message));
          } else if (state is DashboardLoaded) {
            return RefreshIndicator(
              onRefresh: () async => context.read<DashboardBloc>().add(LoadDashboardData()),
              child: SingleChildScrollView(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // 1. Área de Categorias
                    _buildSectionTitle('Categorias'),
                    SizedBox(
                      height: 100,
                      child: ListView.builder(
                        scrollDirection: Axis.horizontal,
                        itemCount: state.categorias.length,
                        itemBuilder: (ctx, i) => Card(
                          color: Colors.blue.shade100,
                          child: Container(
                            width: 100,
                            alignment: Alignment.center,
                            child: Text(state.categorias[i].nome, style: const TextStyle(fontWeight: FontWeight.bold)),
                          ),
                        ),
                      ),
                    ),
                    const SizedBox(height: 20),

                    // 2. Área de Destaques (Miniaturas)
                    _buildSectionTitle('Destaques do Mês'),
                    SizedBox(
                      height: 160,
                      child: ListView.builder(
                        scrollDirection: Axis.horizontal,
                        itemCount: state.carrosDestaque.take(5).length,
                        itemBuilder: (ctx, i) {
                          final carro = state.carrosDestaque[i];
                          return Card(
                            child: Column(
                              children: [
                                Expanded(
                                  child: Container(
                                    width: 140,
                                    color: Colors.grey.shade300,
                                    child: const Icon(Icons.car_rental, size: 50),
                                  ),
                                ),
                                Padding(
                                  padding: const EdgeInsets.all(8.0),
                                  child: Text(carro.modelo),
                                )
                              ],
                            ),
                          );
                        },
                      ),
                    ),
                    const SizedBox(height: 20),

                    // 3. Lista de Marcas e Quantidades
                    _buildSectionTitle('Marcas Parceiras'),
                    ...state.marcas.map((marca) => ListTile(
                      leading: CircleAvatar(child: Text(marca.nome[0])),
                      title: Text(marca.nome),
                      trailing: Chip(label: Text('${marca.quantidadeVeiculos} un.')),
                    )),
                  ],
                ),
              ),
            );
          }
          return const SizedBox.shrink();
        },
      ),
      floatingActionButton: FloatingActionButton(
        // Navegação via GoRouter
        onPressed: () => context.go('/dashboard/add-carro'), 
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildSectionTitle(String title) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Text(title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
    );
  }
}

🚀 Passo 6: Injeção de Dependência no main.dart

Para que o BLoC funcione, precisamos injetá-lo no topo da árvore de widgets.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'repositories/isar_repository.dart';
import 'blocs/dashboard/dashboard_bloc.dart';
import 'router/app_router.dart';

void main() {
  final repository = IsarRepository(); // Instancia o repositório

  runApp(
    MultiBlocProvider(
      providers: [
        // Injeta o DashboardBloc para o app todo
        BlocProvider(create: (context) => DashboardBloc(repository)),
      ],
      child: const CarControlApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Car Control Avançado',
      routerConfig: appRouter, // Usa a configuração do GoRouter
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
    );
  }
}

✅ Conclusão

Nesta Parte 5, modernizamos drasticamente o CarControl:

  1. Dashboard Completo: Criamos uma tela rica com seções horizontais e verticais.

  2. BLoC Pattern: Implementamos uma arquitetura escalável onde a UI apenas reage a estados (Loading, Loaded, Error), sem lógica misturada no widget.

  3. GoRouter: Configuramos rotas nomeadas e aninhadas, facilitando a navegação futura e deep linking.

🔮 Próximos Passos

Ainda temos muito o que evoluir! Nos próximos artigos:

  • Autenticação: Login e Proteção de Rotas com GoRouter e Redirect.

  • Testes Unitários: Como testar nosso DashboardBloc e garantir que ele emite os estados corretos.

  • Animações: Tornar a transição entre o Dashboard e os Detalhes mais fluida.

Fique ligado para continuar profissionalizando seu portfólio Flutter! 🚀

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