Construindo um App de Controle de Veículos com Flutter: Autenticação, Testes Unitários e Animações (Final)
Neste artigo, vamos implementar um fluxo de login protegido com GoRouter, garantir a qualidade do código com bloc_test e dar aquele toque de classe com animações Hero.
🔐 Passo 1: Autenticação e Proteção de Rotas
Vamos criar um AuthBloc para gerenciar se o usuário está logado ou não e configurar o GoRouter para redirecionar usuários não autenticados para a tela de Login.
1.1 O AuthBloc
Primeiro, criamos a lógica de autenticação. Para este exemplo, faremos uma autenticação simulada (mas a estrutura é idêntica para Firebase ou API).
Crie lib/blocs/auth/auth_bloc.dart:
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
// Eventos
abstract class AuthEvent extends Equatable {
@override
List<Object> get props => [];
}
class AuthLoginRequested extends AuthEvent {
final String email;
final String password;
AuthLoginRequested(this.email, this.password);
}
class AuthLogoutRequested extends AuthEvent {}
// Estados
abstract class AuthState extends Equatable {
@override
List<Object> get props => [];
}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final String username;
AuthAuthenticated(this.username);
}
class AuthUnauthenticated extends AuthState {}
// BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(AuthUnauthenticated()) {
on<AuthLoginRequested>((event, emit) async {
emit(AuthLoading());
await Future.delayed(const Duration(seconds: 1)); // Simula API
if (event.email == 'admin' && event.password == '123') {
emit(AuthAuthenticated('Administrador'));
} else {
emit(AuthUnauthenticated()); // Poderia ser um estado de Erro
}
});
on<AuthLogoutRequested>((event, emit) {
emit(AuthUnauthenticated());
});
}
}
1.2 Protegendo Rotas com GoRouter
Agora, vamos ensinar o roteador a “ouvir” o BLoC. Se o estado mudar para AuthUnauthenticated, ele chuta o usuário para o Login.
Atualize lib/router/app_router.dart:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/auth/auth_bloc.dart';
import '../screens/login_screen.dart'; // Crie esta tela simples depois
import '../screens/dashboard_screen.dart';
class AppRouter {
final AuthBloc authBloc;
AppRouter(this.authBloc);
late final router = GoRouter(
initialLocation: '/dashboard',
refreshListenable: StreamListenable(authBloc.stream), // O Segredo!
redirect: (context, state) {
final isLoggedIn = authBloc.state is AuthAuthenticated;
final isLoggingIn = state.uri.toString() == '/login';
if (!isLoggedIn && !isLoggingIn) return '/login';
if (isLoggedIn && isLoggingIn) return '/dashboard';
return null; // Nenhuma ação necessária
},
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/dashboard',
builder: (context, state) => const DashboardScreen(),
// ... suas sub-rotas anteriores
),
],
);
}
// Classe utilitária para transformar Stream em Listenable
class StreamListenable extends ChangeNotifier {
late final StreamSubscription _subscription;
StreamListenable(Stream stream) {
_subscription = stream.listen((_) => notifyListeners());
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
Não esqueça de injetar o AuthBloc no main.dart e passar para o AppRouter.
🧪 Passo 2: Testes Unitários de BLoC
Código sem teste é dívida técnica. Vamos testar se nosso DashboardBloc carrega os dados corretamente.
Adicione ao pubspec.yaml (dev_dependencies):
bloc_test: ^9.1.0 mocktail: ^1.0.0
Crie test/blocs/dashboard_bloc_test.dart:
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:car_control/blocs/dashboard/dashboard_bloc.dart';
import 'package:car_control/repositories/isar_repository.dart';
import 'package:car_control/models/carro.dart';
// 1. Criar o Mock do Repositório
class MockIsarRepository extends Mock implements IsarRepository {}
void main() {
late MockIsarRepository mockRepo;
late DashboardBloc dashboardBloc;
setUp(() {
mockRepo = MockIsarRepository();
dashboardBloc = DashboardBloc(mockRepo);
});
// Dados falsos para teste
final mockCarros = [Carro(modelo: 'Fusca', marca: 'VW', ano: 1970, placa: 'ABC')];
group('DashboardBloc', () {
test('estado inicial deve ser DashboardLoading', () {
expect(dashboardBloc.state, DashboardLoading());
});
blocTest<DashboardBloc, DashboardState>(
'deve emitir [DashboardLoading, DashboardLoaded] quando os dados carregam com sucesso',
build: () {
// Ensinando o Mock a responder
when(() => mockRepo.getCarros()).thenAnswer((_) async => mockCarros);
return dashboardBloc;
},
act: (bloc) => bloc.add(LoadDashboardData()),
expect: () => [
DashboardLoading(), // Primeiro emite carregando
DashboardLoaded(carrosDestaque: mockCarros, marcas: [], categorias: []), // Depois sucesso
],
);
});
}
Rode flutter test e veja a mágica (e os checks verdes) acontecerem!
✨ Passo 3: Animações Hero (Polimento de UI)
Vamos fazer a imagem do carro “voar” do card do Dashboard para a tela de Detalhes. Isso é chamado de Hero Animation.
3.1 No Card do Dashboard (dashboard_screen.dart)
Envolva a imagem do carro (ou ícone) com o widget Hero. O segredo é a tag, que deve ser única (usaremos o ID do carro).
// Dentro do seu ListView de destaques
Hero(
tag: 'carro_img_${carro.id}', // Tag Única
child: Container(
// Sua imagem ou ícone aqui
child: Image.file(File(carro.imagemPath!)),
),
)
3.2 Na Tela de Detalhes (car_detail_screen.dart)
Envolva a imagem de destino com a mesma tag.
// No cabeçalho da tela de detalhes
SizedBox(
height: 250,
width: double.infinity,
child: Hero(
tag: 'carro_img_${widget.carro.id}', // Mesma Tag!
child: Image.file(
File(widget.carro.imagemPath!),
fit: BoxFit.cover
),
),
)
Agora, ao navegar, a imagem fará uma transição fluida entre as telas, dando um aspecto super profissional ao app.
📥 Conclusão e Código Fonte
Chegamos ao fim da nossa jornada CarControl! 🏁
Começamos com um arquivo em branco e construímos uma aplicação completa com:
-
Arquitetura Limpa e Repository Pattern.
-
Banco de Dados NoSQL de alta performance (Isar).
-
Gerenciamento de Estado Sênior com BLoC.
-
Hardware Real (Câmera e Scanner).
-
Segurança e Testes.
Você agora tem em mãos um template poderoso que pode ser a base para freelancers, projetos de TCC ou portfólio para vagas Sênior.
Com base em toda a explicação mostro um projeto mais completo com os mesmos ideias e mais avançado.
🔗 Baixe o Projeto Completo que usei para montar os artigos
O código fonte completo, com todas as 6 partes integradas, está disponível no GitHub:
👉 github.com/caneto/Catalago_colecionadores