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:

  1. Gerenciamento de Estado: Usando o pacote provider para atualizar a UI automaticamente.

  2. Persistência de Dados: Implementando um banco de dados SQLite com o pacote sqflite.

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

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