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 chaveNSCameraUsageDescription.
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:
-
Arquitetura Limpa: Implementamos o Repository Pattern, desacoplando a lógica de negócios da implementação do banco de dados.
-
Isar Database: Migramos para um banco NoSQL de alta performance, mantendo o código limpo.
-
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! 🚀💙