Construindo um App de Controle de Veículos com Flutter: Arquitetura Limpa, Isar DB e Integração com Câmera (Parte 4)

Um aplicativo profissional precisa ser flexível. E se quisermos trocar o banco de dados amanhã? E se precisarmos escanear a placa do carro via QRCode em vez de digitar? Vamos resolver isso agora.


🏗️ Passo 1: Arquitetura Limpa (Repository Pattern)

Para tornar possível a troca do banco de dados (de SQLite para Isar) sem quebrar o app inteiro, precisamos desacoplar a lógica. Criaremos um Contrato (Interface). O nosso Provider deixará de conhecer o DBHelper e passará a conhecer apenas esse contrato.

Crie lib/repositories/i_carro_repository.dart:

import '../models/carro.dart';

abstract class ICarroRepository {
  Future<List<Carro>> getCarros();
  Future<void> addCarro(Carro carro);
  Future<void> removeCarro(String id);
  // Métodos de busca e update viriam aqui
}

Agora, qualquer banco de dados que quisermos usar (SQLite, Isar, Firebase, API) só precisa implementar essa classe.


🚀 Passo 2: Migrando para Isar Database (NoSQL)

O Isar é um banco de dados extremamente rápido, feito para Flutter, que trabalha com objetos reais, eliminando a necessidade de converter Maps para Objetos (como fazíamos no SQLite).

2.1 Dependências

Adicione ao pubspec.yaml:

dependencies:
  isar: ^3.1.0
  isar_flutter: ^3.1.0
  path_provider: ^2.0.15 # Necessário para achar a pasta
  image_picker: ^1.0.7   # Para fotos
  mobile_scanner: ^5.0.0 # Para ler códigos de barra/QR

dev_dependencies:
  isar_generator: ^3.1.0
  build_runner: ^2.4.8

2.2 O Modelo Isar

O Isar exige que o modelo tenha anotações. Vamos evoluir nosso modelo Carro.

Atualize lib/models/carro.dart:

import 'package:isar/isar.dart';

part 'carro.g.dart'; // O gerador criará este arquivo

@collection
class Carro {
  Id id = Isar.autoIncrement; // Isar gerencia o ID automaticamente
  
  late String modelo;
  late String marca;
  late int ano;
  late String placa;
  String? imagemPath; // Novo campo para a foto!

  Carro({
    required this.modelo,
    required this.marca,
    required this.ano,
    required this.placa,
    this.imagemPath,
  });
}

Rode o comando para gerar o código do banco:

dart run build_runner build

2.3 Implementando o Repositório Isar

Agora criamos a implementação que usa o Isar, respeitando nosso contrato.

Crie lib/repositories/isar_repository.dart:

import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import '../models/carro.dart';
import 'i_carro_repository.dart';

class IsarRepository implements ICarroRepository {
  late Future<Isar> db;

  IsarRepository() {
    db = _initDB();
  }

  Future<Isar> _initDB() async {
    final dir = await getApplicationDocumentsDirectory();
    if (Isar.instanceNames.isEmpty) {
      return await Isar.open(
        [CarroSchema], // Schema gerado automaticamente
        directory: dir.path,
      );
    }
    return Isar.getInstance()!;
  }

  @override
  Future<void> addCarro(Carro carro) async {
    final isar = await db;
    await isar.writeTxn(() async {
      await isar.carros.put(carro);
    });
  }

  @override
  Future<List<Carro>> getCarros() async {
    final isar = await db;
    return await isar.carros.where().findAll();
  }

  @override
  Future<void> removeCarro(String id) async {
    // Nota: O Isar usa Int para ID, aqui faríamos a conversão se necessário.
    // Para simplificar, assumiremos conversão de int no provider ou mudança da interface.
    final isar = await db;
    await isar.writeTxn(() async {
      await isar.carros.delete(int.parse(id)); 
    });
  }
}

Agora, basta ir no seu main.dart ou no Provider e trocar a instância do DBHelper (SQLite) pelo IsarRepository. O resto do app nem saberá que o banco mudou!


📸 Passo 3: Integração de Hardware (Câmera e Scanner)

Vamos adicionar duas features modernas: tirar foto do carro e escanear a placa (via QRCode ou Código de Barras).

3.1 Configuração Nativa (Essencial)

  • Android (AndroidManifest.xml): Adicione permissão de câmera. <uses-permission android:name="android.permission.CAMERA" />

  • iOS (Info.plist): Adicione a chave NSCameraUsageDescription.

3.2 Widget de Scanner

Vamos criar um widget que abre a câmera para ler códigos.

Crie lib/screens/scanner_screen.dart:

import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Escanear Código')),
      body: MobileScanner(
        onDetect: (capture) {
          final List<Barcode> barcodes = capture.barcodes;
          if (barcodes.isNotEmpty && barcodes.first.rawValue != null) {
            final String code = barcodes.first.rawValue!;
            Navigator.pop(context, code); // Retorna o código lido
          }
        },
      ),
    );
  }
}

🎨 Passo 4: Evolução da Tela de Cadastro (UI Avançada)

Vamos juntar tudo na nossa tela de cadastro. Adicionaremos o campo de foto e o botão de scan no campo da placa.

Atualize lib/screens/add_carro_screen.dart:

import 'dart:io'; // Para lidar com arquivos de imagem
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import '../providers/carro_provider.dart';
import 'scanner_screen.dart';

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

  @override
  State<AddCarroScreen> createState() => _AddCarroScreenState();
}

class _AddCarroScreenState extends State<AddCarroScreen> {
  // ... controllers e keys anteriores ...
  final _placaController = TextEditingController(); // Certifique-se de ter este controller
  File? _imagemSelecionada;

  // Função para tirar foto
  Future<void> _tirarFoto() async {
    final picker = ImagePicker();
    final pickedFile = await picker.pickImage(source: ImageSource.camera);

    if (pickedFile != null) {
      setState(() {
        _imagemSelecionada = File(pickedFile.path);
      });
    }
  }

  // Função para abrir o scanner
  Future<void> _escanearPlaca() async {
    // Navega para a tela de scanner e aguarda o resultado
    final resultado = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const ScannerScreen()),
    );

    if (resultado != null) {
      setState(() {
        _placaController.text = resultado; // Preenche o campo automaticamente
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Novo Veículo Premium')),
      body: SingleChildScrollView( // Importante para telas com teclado
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey, // Use a key definida anteriormente
          child: Column(
            children: [
              // --- ÁREA DA FOTO ---
              GestureDetector(
                onTap: _tirarFoto,
                child: Container(
                  height: 200,
                  width: double.infinity,
                  decoration: BoxDecoration(
                    color: Colors.grey[200],
                    borderRadius: BorderRadius.circular(16),
                    image: _imagemSelecionada != null
                        ? DecorationImage(
                            image: FileImage(_imagemSelecionada!),
                            fit: BoxFit.cover,
                          )
                        : null,
                    border: Border.all(color: Colors.grey.shade400),
                  ),
                  child: _imagemSelecionada == null
                      ? const Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(Icons.camera_alt, size: 50, color: Colors.grey),
                            Text('Toque para tirar foto'),
                          ],
                        )
                      : null,
                ),
              ),
              const SizedBox(height: 20),

              // ... Inputs de Modelo, Marca, Ano ...

              // --- INPUT DE PLACA COM SCANNER ---
              Row(
                children: [
                  Expanded(
                    child: TextFormField(
                      controller: _placaController,
                      decoration: const InputDecoration(
                        labelText: 'Placa ou Código',
                        border: OutlineInputBorder(),
                      ),
                    ),
                  ),
                  const SizedBox(width: 10),
                  IconButton.filledTonal(
                    onPressed: _escanearPlaca,
                    icon: const Icon(Icons.qr_code_scanner),
                    iconSize: 30,
                  ),
                ],
              ),
              
              const SizedBox(height: 30),
              
              // Botão Salvar
              FilledButton(
                onPressed: () {
                   // Ao salvar, passe _imagemSelecionada?.path para o Provider/Model
                   // Lembre-se de atualizar seu método addCarro para aceitar String? imagePath
                },
                child: const Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [Icon(Icons.save), SizedBox(width: 8), Text('SALVAR')],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Perfeito! Ajustei o final do artigo para deixar claro que a jornada continua e preparar o terreno para os tópicos avançados que você mencionou (BLoC, aprofundamento em GoRouter e novas telas).

Aqui está a nova seção final para substituir a “Conclusão da Série”:


✅ Conclusão

Nesta etapa, demos um salto gigantesco na qualidade da engenharia do nosso projeto CarControl. Deixamos de ser apenas um app funcional para nos tornarmos uma aplicação escalável e moderna.

O que consolidamos hoje:

  1. Arquitetura Limpa: Implementamos o Repository Pattern, desacoplando a lógica de negócios da implementação do banco de dados.

  2. Isar Database: Migramos para um banco NoSQL de alta performance, mantendo o código limpo.

  3. Integração de Hardware: Adicionamos capacidades reais de uso da Câmera e Scanner de QR Code/Código de Barras, elevando a experiência do usuário.

Mas o desenvolvimento de software é um processo de melhoria contínua, e ainda temos muito o que explorar para tornar este projeto uma referência de mercado.

🔮 Próximos Passos

Nos próximos artigos, vamos focar na arquitetura corporativa e navegação complexa:

  • Novas Telas e Dashboards: Vamos expandir a aplicação com telas de relatórios visuais e configurações de usuário.

  • Gerenciamento de Estado com BLoC: Vamos refatorar as telas principais para utilizar o padrão BLoC (Business Logic Component). Você entenderá na prática as diferenças e vantagens em relação ao Provider para fluxos complexos.

  • Domínio do GoRouter: Vamos aprofundar no roteamento, explorando parâmetros dinâmicos, rotas aninhadas (ShellRoute), redirecionamentos e proteção de rotas (Guards).

Fique ligado para a próxima parte, onde a arquitetura do nosso app ficará ainda mais robusta! 🚀💙

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