Construindo um App de Gestão de Corridas (F1) com Flutter 3.41: SQLite, Home e Exportação de Dados (Parte 1)

Gerenciar dados de campeonatos de corrida exige precisão. Neste artigo, daremos o pontapé inicial na construção de um aplicativo focado em Controle de Pontuação e Dados de Corrida.

Vamos configurar uma arquitetura local robusta utilizando SQLite (banco de dados relacional), implementar uma funcionalidade crítica de Exportação de Banco de Dados (para backup ou análise externa) e construir a interface principal (Home e Drawer) seguindo rigorosamente as diretrizes do Material 3.

🏎️ 1. Arquitetura de Pastas (Feature-First)

Para um projeto que vai escalar (tabelas de pilotos, equipes, corridas, pontuações), a organização é fundamental. Estruture seu projeto lib da seguinte forma:

lib/
├── core/
│   ├── database/    # Configuração e Helper do SQLite
│   └── theme/       # Design System (Material 3)
├── data/
│   └── models/      # Modelos de dados (Piloto, Pista, etc.)
├── modules/
│   ├── home/        # Telas da Home e Dashboard
│   ├── drivers/     # Telas de Gestão de Pilotos
│   └── tracks/      # Telas de Pistas e Calendário
└── main.dart

📦 2. Configurando o Arsenal (Dependências)

Para lidar com SQL nativo, caminhos de sistema e exportação de arquivos, adicione os seguintes pacotes ao seu pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.3.0       # O motor de banco de dados
  path: ^1.9.0          # Manipulação de caminhos de arquivo
  path_provider: ^2.1.2 # Acesso aos diretórios do Android/iOS
  share_plus: ^7.2.0    # Para exportar (compartilhar) o banco .db
  google_fonts: ^6.1.0  # Tipografia

(Execute flutter pub get).

🧬 3. Modelagem de Dados Relacional (Entidades)

Diferente do NoSQL, no SQLite precisamos pensar em tabelas e chaves estrangeiras. Vamos começar modelando nossa base principal: o Piloto (Driver).

Crie lib/data/models/driver.dart:

class Driver {
  final int? id;
  final String name;
  final String team;
  final int number;

  Driver({this.id, required this.name, required this.team, required this.number});

  // Converte objeto para Mapa (Para o SQLite)
  Map<String, dynamic> toMap() {
    return {
      if (id != null) 'id': id,
      'name': name,
      'team': team,
      'number': number,
    };
  }

  // Converte Mapa do SQLite para Objeto
  factory Driver.fromMap(Map<String, dynamic> map) {
    return Driver(
      id: map['id'] as int?,
      name: map['name'] as String,
      team: map['team'] as String,
      number: map['number'] as int,
    );
  }
}

💾 4. O Coração do App: Configuração do SQLite

Aqui criamos o Singleton que gerencia a conexão com o banco e executa as migrations (criação de tabelas).

Crie lib/core/database/database_helper.dart:

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static final DatabaseHelper instance = DatabaseHelper._init();
  static Database? _database;

  DatabaseHelper._init();

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDB('racing_data.db');
    return _database!;
  }

  Future<Database> _initDB(String filePath) async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, filePath);

    return await openDatabase(
      path,
      version: 1,
      onCreate: _createDB,
    );
  }

  Future _createDB(Database db, int version) async {
    const idType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
    const textType = 'TEXT NOT NULL';
    const integerType = 'INTEGER NOT NULL';

    // Tabela de Pilotos
    await db.execute('''
    CREATE TABLE drivers (
      id $idType,
      name $textType,
      team $textType,
      number $integerType
    )
    ''');

    // Futuras tabelas (Pistas, Categorias) seriam criadas aqui
    await db.execute('''
    CREATE TABLE tracks (
      id $idType,
      name $textType,
      country $textType,
      length REAL NOT NULL
    )
    ''');
  }

  Future<void> close() async {
    final db = await instance.database;
    db.close();
  }
}

📤 5. Engenharia de Dados: Exportando o Banco

Um recurso muito avançado e requisitado. O usuário pode querer exportar o banco .db para ler em um software externo (como o DB Browser for SQLite) ou fazer backup.

Adicione este método na classe DatabaseHelper:

import 'dart:io';
import 'package:share_plus/share_plus.dart';

Future<void> exportDatabase() async {
  final dbPath = await getDatabasesPath();
  final path = join(dbPath, 'racing_data.db');

  final file = File(path);
  if (await file.exists()) {
    // Usa o share_plus para abrir o menu de compartilhamento do SO
    await Share.shareXFiles([XFile(path)], text: 'Backup do Banco de Dados de Corridas');
  } else {
    throw Exception("Banco de dados não encontrado.");
  }
}

🎨 6. Design System e Identidade Visual (Material 3)

Automobilismo remete a cores fortes (Vermelho, Preto, Carbono). Vamos usar o esquema tonal do M3 para gerar essa paleta.

Atualize o lib/main.dart:

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'modules/home/home_screen.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const RacingApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Racing Manager',
      theme: ThemeData(
        useMaterial3: true,
        // Cor base: Vermelho F1
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFFE10600),
          brightness: Brightness.dark, // Tema escuro combina com telemetria
        ),
        textTheme: GoogleFonts.rajdhaniTextTheme(ThemeData.dark().textTheme),
      ),
      home: const HomeScreen(),
    );
  }
}

🏠 7. O Dashboard: Tela Home com M3 Cards

A tela inicial será um Grid focado na usabilidade, levando o usuário para as áreas de gestão.

Crie lib/modules/home/home_screen.dart:

import 'package:flutter/material.dart';
import 'widgets/racing_drawer.dart'; // Criaremos no Passo 8

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

  @override
  Widget build(BuildContext context) {
    final menuItems = [
      {'title': 'Pilotos', 'icon': Icons.sports_motorsports},
      {'title': 'Pistas', 'icon': Icons.map},
      {'title': 'Categorias', 'icon': Icons.category},
      {'title': 'Pontuação', 'icon': Icons.leaderboard},
    ];

    return Scaffold(
      appBar: AppBar(
        title: const Text('Racing Manager', style: TextStyle(fontWeight: FontWeight.bold)),
        centerTitle: true,
      ),
      drawer: const RacingDrawer(), // Injeção do Menu Lateral
      body: GridView.builder(
        padding: const EdgeInsets.all(16),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 16,
          mainAxisSpacing: 16,
          childAspectRatio: 1.1,
        ),
        itemCount: menuItems.length,
        itemBuilder: (context, index) {
          final item = menuItems[index];
          return Card(
            // Material 3 Tonal Card
            elevation: 0,
            color: Theme.of(context).colorScheme.surfaceContainerHighest,
            clipBehavior: Clip.antiAlias,
            child: InkWell(
              onTap: () {
                // Navegação futura para as listas
              },
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(item['icon'] as IconData, size: 48, color: Theme.of(context).colorScheme.primary),
                  const SizedBox(height: 16),
                  Text(
                    item['title'] as String,
                    style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

🗄️ 8. Menu Drawer Moderno (NavigationDrawer M3)

No Material 3, o antigo Drawer foi substituído pelo componente NavigationDrawer, que já possui padding inteligente, itens com bordas de pílula e suporte a cabeçalhos nativos.

Crie lib/modules/home/widgets/racing_drawer.dart:

import 'package:flutter/material.dart';
import '../../../core/database/database_helper.dart';

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

  @override
  Widget build(BuildContext context) {
    return NavigationDrawer(
      onDestinationSelected: (index) async {
        // Controle de navegação do Drawer
        if (index == 2) { // Índice 2 é a Exportação
          try {
            await DatabaseHelper.instance.exportDatabase();
          } catch (e) {
            ScaffoldMessenger.of(context).showSnackBar(
               SnackBar(content: Text('Erro ao exportar: $e')),
            );
          }
        }
      },
      children: [
        Padding(
          padding: const EdgeInsets.fromLTRB(28, 40, 16, 10),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              CircleAvatar(
                radius: 30,
                backgroundColor: Theme.of(context).colorScheme.primary,
                child: const Icon(Icons.person, size: 35, color: Colors.white),
              ),
              const SizedBox(height: 12),
              Text('Diretor de Prova', style: Theme.of(context).textTheme.titleLarge),
              Text('admin@racing.com', style: Theme.of(context).textTheme.bodyMedium),
            ],
          ),
        ),
        const Padding(
          padding: EdgeInsets.symmetric(horizontal: 28),
          child: Divider(),
        ),
        const Padding(
          padding: EdgeInsets.fromLTRB(28, 16, 16, 10),
          child: Text('Navegação'),
        ),
        const NavigationDrawerDestination(
          icon: Icon(Icons.home_outlined),
          selectedIcon: Icon(Icons.home),
          label: Text('Início'),
        ),
        const NavigationDrawerDestination(
          icon: Icon(Icons.settings_outlined),
          selectedIcon: Icon(Icons.settings),
          label: Text('Configurações e Edição'),
        ),
        const Padding(
          padding: EdgeInsets.symmetric(horizontal: 28),
          child: Divider(),
        ),
        const Padding(
          padding: EdgeInsets.fromLTRB(28, 16, 16, 10),
          child: Text('Dados'),
        ),
        const NavigationDrawerDestination(
          icon: Icon(Icons.cloud_upload_outlined),
          label: Text('Exportar Banco de Dados'), // Nosso método de exportação
        ),
      ],
    );
  }
}

✅ Conclusão

Nesta Parte 1, construímos uma infraestrutura de alto nível.

Estabelecemos o SQLite como motor de dados estruturados (preparado para joins complexos de campeonato), criamos um design impactante e imersivo com Material 3 (Dark Mode e Tonal Palettes), e entregamos uma funcionalidade rara em tutoriais básicos: a extração física do banco de dados para compartilhamento externo.

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