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?
-
Deep Linking: Se o usuário abrir um link
myapp://dashboard/add-carro, ele cai direto na tela certa. -
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:
-
Dashboard Completo: Criamos uma tela rica com seções horizontais e verticais.
-
BLoC Pattern: Implementamos uma arquitetura escalável onde a UI apenas reage a estados (
Loading,Loaded,Error), sem lógica misturada no widget. -
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
GoRoutereRedirect. -
Testes Unitários: Como testar nosso
DashboardBloce 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! 🚀