Construindo um App de Gestão de Corridas com Flutter 3.41: Mapas, Telemetria e Produção (Final)
Neste encerramento, vamos mapear o campeonato globalmente usando o Google Maps, criar um painel de telemetria visual (Gráficos de Linha) para acompanhar a evolução do campeonato, otimizar nosso motor SQLite para lidar com milhares de consultas em milissegundos e preparar o aplicativo para o lançamento (Deploy).
🌍 1. Evolução do Banco de Dados: Geolocalização das Provas
Para exibirmos as corridas no mapa, nossa tabela races precisa armazenar as coordenadas geográficas (latitude e longitude).
Atualização do DatabaseHelper (Migration no onUpgrade): No SQLite, não podemos simplesmente alterar uma tabela complexa facilmente, mas podemos adicionar colunas.
// Em lib/core/database/database_helper.dart
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
return await openDatabase(
join(dbPath, filePath),
version: 2, // Subimos a versão para 2
onCreate: _createDB,
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
// Adicionando as colunas de geolocalização
await db.execute('ALTER TABLE races ADD COLUMN latitude REAL DEFAULT 0.0');
await db.execute('ALTER TABLE races ADD COLUMN longitude REAL DEFAULT 0.0');
}
},
);
}
O Modelo Atualizado com Geolocalização (race.dart)
class Race {
final int? id;
final String name;
final String date;
final bool isCompleted;
final bool isSprint;
final double completedPercentage;
// Novos campos para o Google Maps
final double latitude;
final double longitude;
Race({
this.id,
required this.name,
required this.date,
this.isCompleted = false,
this.isSprint = false,
this.completedPercentage = 100.0,
// Definimos 0.0 como padrão para retrocompatibilidade com corridas antigas
this.latitude = 0.0,
this.longitude = 0.0,
});
// Converte o objeto Race para um Map (Formato aceito pelo SQLite)
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'name': name,
'date': date,
'is_completed': isCompleted ? 1 : 0,
'is_sprint': isSprint ? 1 : 0,
'completed_percentage': completedPercentage,
// Salvando as coordenadas no banco
'latitude': latitude,
'longitude': longitude,
};
}
// Converte o Map vindo do SQLite de volta para um objeto Race no Dart
factory Race.fromMap(Map<String, dynamic> map) {
return Race(
id: map['id'] as int?,
name: map['name'] as String,
date: map['date'] as String,
isCompleted: map['is_completed'] == 1,
isSprint: map['is_sprint'] == 1,
completedPercentage: (map['completed_percentage'] ?? 100.0).toDouble(),
// Lendo as coordenadas com segurança
// Usamos .toDouble() pois o SQLite pode retornar um int se o valor salvo for redondo (ex: 0 em vez de 0.0)
latitude: (map['latitude'] ?? 0.0).toDouble(),
longitude: (map['longitude'] ?? 0.0).toDouble(),
);
}
}
💡 Explicação Técnica das Atualizações:
-
Novas Propriedades (
latitudeelongitude): Adicionamos os dois campos comodouble(números de ponto flutuante), que é o formato exigido pelo pacoteMaps_flutterpara a classeLatLng. -
Construtor com Valores Padrão (
0.0): Como o banco de dados já possuía corridas salvas antes da nossa atualização (Parte 1 a 4), definimos0.0como valor padrão. Assim, se o aplicativo ler uma corrida antiga que ainda não tem coordenadas cadastradas, o código não vai “quebrar” (Crash) por variáveis nulas. A corrida simplesmente apareceria no “ponto zero” do mapa até ser editada. -
Segurança no
fromMapcom.toDouble(): Este é um detalhe de nível Sênior no Flutter com SQLite. O SQLite possui uma tipagem dinâmica flexível (Dynamic Typing). Se você salvar a latitude-23.0, ele salva como umREAL. Mas se você salvar0.0, ele pode otimizar e retornar umINTEGER(0). Se tentarmos forçar um0inteiro para dentro de uma variáveldoubleno Dart, o aplicativo lança uma exceção fatal deTypeError. Ao envolver o mapeamento com(map['latitude'] ?? 0.0).toDouble(), nós garantimos que o Dart sempre receberá umdouble, não importa como o SQLite tenha otimizado o dado.
⚡ 2. Otimizações Finais de Performance no SQLite
Em produção, o cálculo do campeonato (que varre todos os resultados) pode ficar lento se a base de dados crescer muito. Precisamos criar Índices (INDEX) para as chaves estrangeiras.
Adicione no método _createDB:
-- Criação de Índices para acelerar consultas (JOINs lógicos e Filtros) CREATE INDEX idx_results_driver_id ON results (driver_id); CREATE INDEX idx_results_race_id ON results (race_id); CREATE INDEX idx_drivers_team_id ON drivers (team_id);
Além disso, sempre que fechar o app, podemos rodar um comando VACUUM opcional no banco para desfragmentar o arquivo SQLite, garantindo leituras em disco mais rápidas.
🗺️ 3. Configurando o Google Maps no Flutter 3.41
Com o motor gráfico Impeller ativado por padrão no iOS e Android no Flutter 3.41, a renderização de mapas está mais fluida do que nunca.
Dependências no pubspec.yaml:
google_maps_flutter: ^2.5.3
Nota de Produção: Lembre-se de adicionar a sua API Key do Google Maps no AndroidManifest.xml (Android) e no AppDelegate.swift (iOS) de forma segura (idealmente usando o pacote flutter_dotenv para não expor a chave no GitHub).
📍 4. Tela do Mapa do Campeonato (UI com Material 3)
Vamos criar uma tela interativa que exibe todos os locais onde ocorrerão as corridas do campeonato.
Arquivo: lib/modules/map/championship_map_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import '../../blocs/race/race_bloc.dart';
import '../../blocs/race/race_state.dart';
class ChampionshipMapScreen extends StatefulWidget {
const ChampionshipMapScreen({super.key});
@override
State<ChampionshipMapScreen> createState() => _ChampionshipMapScreenState();
}
class _ChampionshipMapScreenState extends State<ChampionshipMapScreen> {
late GoogleMapController mapController;
final LatLng _center = const LatLng(20.0, 0.0); // Visão global
void _onMapCreated(GoogleMapController controller) {
mapController = controller;
// Opcional: Aplicar um estilo escuro ao mapa para combinar com a telemetria
// mapController.setMapStyle(darkMapStyleString);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Mapa do Campeonato')),
body: BlocBuilder<RaceBloc, RaceState>(
builder: (context, state) {
if (state is RaceLoaded) {
final markers = state.races.where((r) => r.latitude != 0.0).map((race) {
return Marker(
markerId: MarkerId(race.id.toString()),
position: LatLng(race.latitude, race.longitude),
infoWindow: InfoWindow(title: race.name, snippet: race.date),
icon: BitmapDescriptor.defaultMarkerWithHue(
race.isCompleted ? BitmapDescriptor.hueGreen : BitmapDescriptor.hueRed,
),
);
}).toSet();
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(target: _center, zoom: 2.0),
markers: markers,
myLocationButtonEnabled: false,
);
}
return const Center(child: CircularProgressIndicator());
},
),
);
}
}
📈 5. Motor de Telemetria: Lógica de Evolução de Pontos
Para o gráfico, precisamos saber quantos pontos um piloto tinha na Corrida 1, Corrida 2, etc. Precisamos de uma Soma Cumulativa.
Adição no ChampionshipBloc ou em um TelemetryBloc dedicado:
// Função para gerar o histórico de pontos de um piloto específico
List<int> calculatePointEvolution(Driver driver, List<Result> allResults, List<Race> sortedRaces) {
int accumulatedPoints = 0;
List<int> evolution = [];
for (var race in sortedRaces) {
if (!race.isCompleted) continue;
// Busca o resultado do piloto nesta corrida específica
final result = allResults.cast<Result?>().firstWhere(
(r) => r?.raceId == race.id && r?.driverId == driver.id,
orElse: () => null,
);
if (result != null) {
accumulatedPoints += ScoringSystem.calculatePoints(
result.position,
isSprint: race.isSprint,
completedPercentage: race.completedPercentage
);
}
evolution.add(accumulatedPoints);
}
return evolution;
}
📊 6. Gráficos de Telemetria (Evolução do Campeonato)
Usaremos o poderoso pacote fl_chart para renderizar uma linha temporal da pontuação do líder do campeonato versus o segundo colocado, utilizando cores vibrantes do Material 3.
Dependência: fl_chart: ^0.66.0
Arquivo: lib/modules/telemetry/telemetry_chart_widget.dart
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class TelemetryChartWidget extends StatelessWidget {
final List<int> driver1Evolution; // Ex: [25, 43, 68]
final List<int> driver2Evolution; // Ex: [18, 33, 48]
const TelemetryChartWidget({
super.key,
required this.driver1Evolution,
required this.driver2Evolution
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return AspectRatio(
aspectRatio: 1.5,
child: Padding(
padding: const EdgeInsets.all(16),
child: LineChart(
LineChartData(
gridData: const FlGridData(show: true, drawVerticalLine: false),
titlesData: const FlTitlesData(
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
lineBarsData: [
// Linha do Líder
LineChartBarData(
spots: driver1Evolution.asMap().entries.map((e) => FlSpot(e.key.toDouble(), e.value.toDouble())).toList(),
isCurved: true,
color: colorScheme.primary,
barWidth: 4,
isStrokeCapRound: true,
dotData: const FlDotData(show: true),
),
// Linha do Vice-Líder
LineChartBarData(
spots: driver2Evolution.asMap().entries.map((e) => FlSpot(e.key.toDouble(), e.value.toDouble())).toList(),
isCurved: true,
color: colorScheme.tertiary,
barWidth: 3,
dashArray: [5, 5], // Linha tracejada
dotData: const FlDotData(show: false),
),
],
),
),
),
);
}
}
🛠️ 7. Preparo para Produção (Build & Obfuscação)
Chegou a hora de compilar. Para um app de alta performance e seguro, não rodamos apenas flutter build apk. Precisamos otimizar os binários e proteger nosso código de engenharia reversa.
Comando de Build para Android (Release Profissional):
flutter build appbundle --obfuscate --split-debug-info=./debug_info
-
appbundle: O Google Play exigirá o formato AAB, que baixa apenas a arquitetura correta para o celular do usuário, diminuindo o tamanho do app drasticamente. -
--obfuscate: Renomeia classes e métodos, protegendo nossa lógica matemática de pontuação e chaves do banco. -
--split-debug-info: Guarda os mapas de erro localmente para podermos ler relatórios de crashlytics depois.
🏁 8. Revisão Final da Arquitetura do App
Construir software não é apenas juntar telas; é sobre fluxo de dados. Vamos revisar a obra de arte que construímos nas últimas 5 partes:
-
Fundação: Banco de Dados SQLite puro (
sqflite), otimizado com índices,FOREIGN KEYSe suporte a transações em lote para salvar os grids de largada instantaneamente. -
Clean Architecture: Separação estrita usando o padrão Repository (CRUD) isolado da lógica de negócio.
-
Core Matemático: Regras densas da FIA (Provas normais, Sprints e Bandeiras Vermelhas com cálculos percentuais) isoladas em utilitários estáticos puros.
-
Gerenciamento de Estado: Utilizamos BLoC (Business Logic Component) com eventos de Pattern Matching do Dart 3, mantendo a UI totalmente reativa e sem
setStatedesnecessários. -
Interface de Nível Mundial: GoRouter para roteamento profundo e animações, combinado com componentes tonais do Material 3,
ReorderableListView(Arrastar e Soltar) e agora, Google Maps e Fl_Chart para imersão total.
Fim da Temporada!
O que começou como um esboço de tabelas transformou-se em um aplicativo Enterprise robusto. Este modelo pode ser facilmente estendido para consumir APIs reais de F1 no futuro (substituindo o repositório SQLite por chamadas HTTP Dio).