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:

  1. Novas Propriedades (latitude e longitude): Adicionamos os dois campos como double (números de ponto flutuante), que é o formato exigido pelo pacote Maps_flutter para a classe LatLng.

  2. 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), definimos 0.0 como 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.

  3. Segurança no fromMap com .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 um REAL. Mas se você salvar 0.0, ele pode otimizar e retornar um INTEGER (0). Se tentarmos forçar um 0 inteiro para dentro de uma variável double no Dart, o aplicativo lança uma exceção fatal de TypeError. Ao envolver o mapeamento com (map['latitude'] ?? 0.0).toDouble(), nós garantimos que o Dart sempre receberá um double, 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:

  1. Fundação: Banco de Dados SQLite puro (sqflite), otimizado com índices, FOREIGN KEYS e suporte a transações em lote para salvar os grids de largada instantaneamente.

  2. Clean Architecture: Separação estrita usando o padrão Repository (CRUD) isolado da lógica de negócio.

  3. Core Matemático: Regras densas da FIA (Provas normais, Sprints e Bandeiras Vermelhas com cálculos percentuais) isoladas em utilitários estáticos puros.

  4. Gerenciamento de Estado: Utilizamos BLoC (Business Logic Component) com eventos de Pattern Matching do Dart 3, mantendo a UI totalmente reativa e sem setState desnecessários.

  5. 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).

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