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.