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:

  1. Arquitetura Limpa e Repository Pattern.

  2. Banco de Dados NoSQL de alta performance (Isar).

  3. Gerenciamento de Estado Sênior com BLoC.

  4. Hardware Real (Câmera e Scanner).

  5. 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

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