Construindo um App de Controle de Veículos com Flutter: Estado, Banco de Dados e Navegação (Parte 2)
Na primeira parte desta série, criamos a estrutura básica do nosso app “CarControl”. Agora, vamos transformar aquela interface estática em uma aplicação funcional, capaz de salvar dados no dispositivo e navegar entre telas de forma profissional.
Neste artigo, vamos cobrir:
-
Gerenciamento de Estado: Usando o pacote
providerpara atualizar a UI automaticamente. -
Persistência de Dados: Implementando um banco de dados SQLite com o pacote
sqflite. -
Navegação Declarativa: Configurando rotas modernas com
go_router.
📦 Passo 1: Adicionando as Dependências
Abra o seu arquivo pubspec.yaml e adicione as seguintes bibliotecas na seção dependencies. Estamos usando as versões mais estáveis e populares do ecossistema Flutter.
dependencies:
flutter:
sdk: flutter
# Gerenciamento de Estado
provider: ^6.1.2
# Banco de Dados Local (SQLite)
sqflite: ^2.4.1
path: ^1.9.0
# Navegação
go_router: ^14.2.0
Após salvar, rode o comando:
flutter pub get
💾 Passo 2: A Camada de Dados (Banco de Dados)
Vamos criar um serviço para gerenciar nosso banco de dados SQLite. Isso permitirá que os dados dos carros persistam mesmo se o usuário fechar o aplicativo.
Crie o arquivo lib/data/db_helper.dart:
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import '../models/carro.dart';
class DBHelper {
// Singleton: garante que só exista uma instância do banco aberta
static final DBHelper _instance = DBHelper._internal();
factory DBHelper() => _instance;
DBHelper._internal();
Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB();
return _database!;
}
Future<Database> _initDB() async {
String path = join(await getDatabasesPath(), 'car_control.db');
return await openDatabase(
path,
version: 1,
onCreate: (db, version) {
return db.execute('''
CREATE TABLE carros(
id TEXT PRIMARY KEY,
modelo TEXT,
marca TEXT,
ano INTEGER,
placa TEXT
)
''');
},
);
}
// Métodos CRUD (Create, Read, Delete)
Future<void> insertCarro(Carro carro) async {
final db = await database;
// Precisamos converter nosso objeto Carro para Map (vamos adicionar isso no model)
await db.insert('carros', carro.toMap(), conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<List<Carro>> getCarros() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query('carros');
return List.generate(maps.length, (i) => Carro.fromMap(maps[i]));
}
Future<void> deleteCarro(String id) async {
final db = await database;
await db.delete('carros', where: 'id = ?', whereArgs: [id]);
}
}
Atualização no Model (lib/models/carro.dart): Adicione os métodos toMap e fromMap para facilitar a conversão.
class Carro {
// ... campos existentes ...
Map<String, dynamic> toMap() {
return {
'id': id,
'modelo': modelo,
'marca': marca,
'ano': ano,
'placa': placa,
};
}
factory Carro.fromMap(Map<String, dynamic> map) {
return Carro(
id: map['id'],
modelo: map['modelo'],
marca: map['marca'],
ano: map['ano'],
placa: map['placa'],
);
}
}
⚡ Passo 3: O Gerenciador de Estado (Provider)
O Provider será a ponte entre o Banco de Dados e a Interface. Ele vai notificar a tela sempre que a lista de carros mudar.
Crie o arquivo lib/providers/carro_provider.dart:
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart'; // Adicione uuid: ^4.5.1 ao pubspec se quiser IDs únicos fáceis
import '../models/carro.dart';
import '../data/db_helper.dart';
class CarroProvider with ChangeNotifier {
List<Carro> _carros = [];
final DBHelper _dbHelper = DBHelper();
List<Carro> get carros => _carros;
// Carregar dados iniciais do banco
Future<void> loadCarros() async {
_carros = await _dbHelper.getCarros();
notifyListeners(); // Avisa a UI para redesenhar
}
Future<void> addCarro(String modelo, String marca, int ano, String placa) async {
final novoCarro = Carro(
id: DateTime.now().toString(), // ID simples por enquanto
modelo: modelo,
marca: marca,
ano: ano,
placa: placa,
);
await _dbHelper.insertCarro(novoCarro);
_carros.add(novoCarro);
notifyListeners();
}
Future<void> removeCarro(String id) async {
await _dbHelper.deleteCarro(id);
_carros.removeWhere((carro) => carro.id == id);
notifyListeners();
}
}
🧭 Passo 4: Navegação e Injeção de Dependência
Vamos configurar o go_router para gerenciar nossas rotas e envolver o app com o ChangeNotifierProvider para que o estado seja acessível globalmente.
Atualize o lib/main.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'providers/carro_provider.dart';
import 'screens/home_screen.dart';
import 'screens/add_carro_screen.dart'; // Criaremos esta tela em breve
void main() {
runApp(
// Injetando o Provider no topo da árvore
ChangeNotifierProvider(
create: (context) => CarroProvider()..loadCarros(),
child: const CarControlApp(),
),
);
}
// Configuração de Rotas
final _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/add',
builder: (context, state) => const AddCarroScreen(),
),
],
);
class CarControlApp extends StatelessWidget {
const CarControlApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Car Control',
routerConfig: _router, // Conectando o GoRouter
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
);
}
}
📱 Passo 5: Conectando a UI (Consumindo Dados)
Agora vamos atualizar a HomeScreen para exibir a lista real vinda do Provider e criar o botão que navega para a tela de cadastro.
Atualize lib/screens/home_screen.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import '../providers/carro_provider.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
// Acessando o estado
final carroProvider = Provider.of<CarroProvider>(context);
final carros = carroProvider.carros;
return Scaffold(
appBar: AppBar(title: const Text('Meus Veículos')),
body: carros.isEmpty
? const Center(child: Text('Nenhum carro cadastrado.'))
: ListView.builder(
itemCount: carros.length,
itemBuilder: (ctx, i) {
final carro = carros[i];
return ListTile(
leading: const Icon(Icons.directions_car),
title: Text(carro.modelo),
subtitle: Text('${carro.marca} - ${carro.placa}'),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
// Removendo item via Provider
carroProvider.removeCarro(carro.id);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.push('/add'), // Navegação simples com GoRouter
child: const Icon(Icons.add),
),
);
}
}
🚀 Conclusão da Parte 2
Neste artigo, transformamos um protótipo visual em uma aplicação completa com:
-
Persistência: Seus dados agora sobrevivem ao fechamento do app graças ao SQLite.
-
Reatividade: A tela atualiza sozinha quando um dado muda, graças ao Provider.
-
Navegação Moderna: Usamos URLs e rotas declarativas com o GoRouter.
No próximo artigo (Parte 3), vamos focar em Refinamento de UI e Validação de Formulários, criando a tela de cadastro (AddCarroScreen) com validações profissionais e inputs customizados.