Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.bookovia.com/llms.txt

Use this file to discover all available pages before exploring further.

Bookovia Flutter SDK

The official Flutter SDK for the Bookovia Telematics API provides a cross-platform Dart implementation optimized for Flutter applications with null safety, isolate support, and state management integration.

Features

  • Cross-Platform - Single codebase for iOS, Android, and Web
  • Null Safety - Full null safety support with Dart 2.18+
  • Isolate Support - Background processing with Dart isolates
  • State Management - Integration with Provider, Riverpod, and BLoC
  • Offline-First Architecture - SQLite integration with 60+ enterprise sensor fields
  • Enterprise Telematics - Gyroscope, magnetometer, barometer, activity recognition, OBD-II
  • Multi-Sensor Crash Detection - 99% accurate crash detection with sensor fusion
  • Platform Channels - Native integration for device-specific features

Installation

Add to your pubspec.yaml:
dependencies:
  bookovia: ^2.1.0
  
  # Optional: for offline support
  bookovia_offline: ^2.1.0
  
  # Optional: for background location
  bookovia_background: ^2.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter
Then run:
flutter pub get
Requirements:
  • Flutter 3.0+
  • Dart 2.18+
  • Android SDK 21+
  • iOS 13.0+

Setup

Android Configuration

Add permissions to android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Network permissions -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
    <!-- Location permissions -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    
    <!-- Background processing -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    
    <application>
        <!-- Location service -->
        <service
            android:name="com.bookovia.flutter.LocationService"
            android:exported="false" />
    </application>
</manifest>

iOS Configuration

Add to ios/Runner/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to track trips</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>  
<string>This app needs background location for trip tracking</string>

<key>UIBackgroundModes</key>
<array>
    <string>location</string>
    <string>background-fetch</string>
</array>

SDK Initialization

import 'package:bookovia/bookovia.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize Bookovia SDK
  await Bookovia.initialize(
    apiKey: 'bkv_live_your_api_key',
    config: BookoviaConfig(
      baseUrl: 'https://api.bookovia.com/v1',
      enableOfflineMode: true,
      enableDebugLogging: true,
    ),
  );
  
  runApp(MyApp());
}

Quick Start

Basic Client Usage

import 'package:bookovia/bookovia.dart';

class TripService {
  final BookoviaClient _client = Bookovia.instance;
  
  Future<Trip> startTrip({
    required String vehicleId,
    String? driverId,
  }) async {
    try {
      final request = StartTripRequest(
        vehicleId: vehicleId,
        driverId: driverId,
        startLocation: const Location(
          latitude: 40.7128,
          longitude: -74.0060,
        ),
        metadata: {
          'purpose': 'delivery',
          'platform': 'flutter',
        },
      );
      
      final trip = await _client.trips.start(request);
      return trip;
    } catch (e) {
      print('Failed to start trip: $e');
      rethrow;
    }
  }
  
  Future<Trip> stopTrip(String tripId) async {
    try {
      final trip = await _client.trips.stop(
        tripId: tripId,
        request: const StopTripRequest(),
      );
      return trip;
    } catch (e) {
      print('Failed to stop trip: $e');
      rethrow;
    }
  }
}

State Management with Provider

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:bookovia/bookovia.dart';

class TripProvider extends ChangeNotifier {
  final BookoviaClient _client = Bookovia.instance;
  
  Trip? _currentTrip;
  bool _isLoading = false;
  String? _error;
  
  Trip? get currentTrip => _currentTrip;
  bool get isLoading => _isLoading;
  String? get error => _error;
  bool get hasActiveTrip => _currentTrip?.status == TripStatus.active;
  
  Future<void> startTrip(String vehicleId, [String? driverId]) async {
    _setLoading(true);
    _clearError();
    
    try {
      final request = StartTripRequest(
        vehicleId: vehicleId,
        driverId: driverId,
      );
      
      final trip = await _client.trips.start(request);
      _currentTrip = trip;
      
      notifyListeners();
    } catch (e) {
      _setError('Failed to start trip: $e');
    } finally {
      _setLoading(false);
    }
  }
  
  Future<void> stopTrip() async {
    if (_currentTrip == null) return;
    
    _setLoading(true);
    _clearError();
    
    try {
      final stoppedTrip = await _client.trips.stop(
        tripId: _currentTrip!.tripId,
      );
      
      _currentTrip = stoppedTrip;
      notifyListeners();
    } catch (e) {
      _setError('Failed to stop trip: $e');
    } finally {
      _setLoading(false);
    }
  }
  
  void _setLoading(bool loading) {
    _isLoading = loading;
    notifyListeners();
  }
  
  void _setError(String error) {
    _error = error;
    notifyListeners();
  }
  
  void _clearError() {
    _error = null;
  }
}

// Usage in main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => TripProvider()),
        ChangeNotifierProvider(create: (_) => LocationProvider()),
      ],
      child: MaterialApp(
        title: 'Bookovia Flutter',
        home: TripScreen(),
      ),
    );
  }
}

Location Tracking

import 'dart:async';
import 'package:geolocator/geolocator.dart';
import 'package:bookovia/bookovia.dart';

class LocationProvider extends ChangeNotifier {
  final BookoviaClient _client = Bookovia.instance;
  
  StreamSubscription<Position>? _positionSubscription;
  Position? _currentPosition;
  String? _currentTripId;
  bool _isTracking = false;
  
  Position? get currentPosition => _currentPosition;
  bool get isTracking => _isTracking;
  
  Future<bool> requestPermission() async {
    bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      return false;
    }
    
    LocationPermission permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
      if (permission == LocationPermission.denied) {
        return false;
      }
    }
    
    if (permission == LocationPermission.deniedForever) {
      return false;
    }
    
    return true;
  }
  
  Future<void> startTracking(String tripId) async {
    if (!await requestPermission()) {
      throw Exception('Location permission not granted');
    }
    
    _currentTripId = tripId;
    _isTracking = true;
    notifyListeners();
    
    const LocationSettings locationSettings = LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 10, // meters
    );
    
    _positionSubscription = Geolocator.getPositionStream(
      locationSettings: locationSettings,
    ).listen(
      _onLocationUpdate,
      onError: _onLocationError,
    );
  }
  
  void stopTracking() {
    _positionSubscription?.cancel();
    _positionSubscription = null;
    _currentTripId = null;
    _isTracking = false;
    notifyListeners();
  }
  
  Future<void> _onLocationUpdate(Position position) async {
    _currentPosition = position;
    notifyListeners();
    
    if (_currentTripId != null) {
      try {
        await _client.locations.upload(
          LocationUploadRequest(
            tripId: _currentTripId!,
            latitude: position.latitude,
            longitude: position.longitude,
            timestamp: DateTime.fromMillisecondsSinceEpoch(
              position.timestamp?.millisecondsSinceEpoch ?? 
              DateTime.now().millisecondsSinceEpoch,
            ),
            speed: position.speed,
            heading: position.heading,
            accuracy: position.accuracy,
          ),
        );
      } catch (e) {
        print('Failed to upload location: $e');
        // Store locally for later sync
        await _storeLocationOffline(position);
      }
    }
  }
  
  void _onLocationError(Object error) {
    print('Location error: $error');
  }
  
  Future<void> _storeLocationOffline(Position position) async {
    // Implement offline storage using SQLite
    // This will be synced when connection is restored
  }
}

API Reference

Trip Service

// Trip Query Methods (DataSyncManager)
class DataSyncManager {
  // User Trip Queries
  Future<Map<String, dynamic>> getUserActiveTrips(String userId);
  Future<Map<String, dynamic>> getUserTripHistory(String userId, {String? status});
  
  // Organization Trip Queries
  Future<Map<String, dynamic>> getOrgActiveTrips(String orgId);
  Future<Map<String, dynamic>> getOrgTripHistory(String orgId, {String? status});
  
  // Vehicle Trip Queries
  Future<Map<String, dynamic>> getVehicleActiveTrips(String vehicleId);
  Future<Map<String, dynamic>> getVehicleTripHistory(String vehicleId, {String? status});
}

// Usage
final tripService = Bookovia.instance.trips;

// Start trip
final trip = await tripService.start(
  StartTripRequest(
    vehicleId: 'vehicle_123',
    driverId: 'driver_456',
  ),
);

// List trips
final trips = await tripService.list(
  TripFilters(
    status: TripStatus.active,
    limit: 50,
  ),
);

Location Service

abstract class LocationService {
  Future<void> upload(LocationUploadRequest request);
  Future<BatchUploadResponse> batchUpload(BatchUploadRequest request);
  Future<RouteResponse> getRoute(String tripId);
  Future<List<NearbyLocation>> findNearby(NearbySearchRequest request);
}

// Batch upload locations
final locations = [
  LocationPoint(
    latitude: 40.7128,
    longitude: -74.0060,
    timestamp: DateTime.now(),
    speed: 35.0,
    heading: 180.0,
  ),
];

await Bookovia.instance.locations.batchUpload(
  BatchUploadRequest(
    tripId: 'trip_123',
    locations: locations,
  ),
);

Safety Analytics

abstract class SafetyService {
  Future<SafetyScore> getScore(SafetyScoreRequest request);
  Future<BehaviorAnalysis> analyzeBehavior(BehaviorAnalysisRequest request);
  Future<EventListResponse> getHarshEvents(EventFilters filters);
  Future<CrashRiskAssessment> getCrashRisk(CrashRiskRequest request);
}

// Get safety score
final score = await Bookovia.instance.safety.getScore(
  SafetyScoreRequest(
    driverId: 'driver_456',
    dateRange: DateRange(
      start: DateTime.now().subtract(const Duration(days: 30)),
      end: DateTime.now(),
    ),
  ),
);

print('Safety Score: ${score.score}/100 (Grade: ${score.grade})');

Data Models

Core Models

class Trip {
  final String tripId;
  final String organizationId;
  final String vehicleId;
  final String? driverId;
  final TripStatus status;
  final DateTime startTime;
  final DateTime? endTime;
  final Location? startLocation;
  final Location? endLocation;
  final TripAnalytics analytics;
  final Map<String, dynamic>? metadata;
  final DateTime createdAt;
  final DateTime updatedAt;
  
  const Trip({
    required this.tripId,
    required this.organizationId,
    required this.vehicleId,
    this.driverId,
    required this.status,
    required this.startTime,
    this.endTime,
    this.startLocation,
    this.endLocation,
    required this.analytics,
    this.metadata,
    required this.createdAt,
    required this.updatedAt,
  });
  
  factory Trip.fromJson(Map<String, dynamic> json) {
    return Trip(
      tripId: json['trip_id'] as String,
      organizationId: json['organization_id'] as String,
      vehicleId: json['vehicle_id'] as String,
      driverId: json['driver_id'] as String?,
      status: TripStatus.fromString(json['status'] as String),
      startTime: DateTime.parse(json['start_time'] as String),
      endTime: json['end_time'] != null 
          ? DateTime.parse(json['end_time'] as String) 
          : null,
      startLocation: json['start_location'] != null
          ? Location.fromJson(json['start_location'] as Map<String, dynamic>)
          : null,
      endLocation: json['end_location'] != null
          ? Location.fromJson(json['end_location'] as Map<String, dynamic>)
          : null,
      analytics: TripAnalytics.fromJson(json['analytics'] as Map<String, dynamic>),
      metadata: json['metadata'] as Map<String, dynamic>?,
      createdAt: DateTime.parse(json['created_at'] as String),
      updatedAt: DateTime.parse(json['updated_at'] as String),
    );
  }
  
  Map<String, dynamic> toJson() {
    return {
      'trip_id': tripId,
      'organization_id': organizationId,
      'vehicle_id': vehicleId,
      'driver_id': driverId,
      'status': status.name,
      'start_time': startTime.toIso8601String(),
      'end_time': endTime?.toIso8601String(),
      'start_location': startLocation?.toJson(),
      'end_location': endLocation?.toJson(),
      'analytics': analytics.toJson(),
      'metadata': metadata,
      'created_at': createdAt.toIso8601String(),
      'updated_at': updatedAt.toIso8601String(),
    };
  }
}

class Location {
  final double latitude;
  final double longitude;
  final String? address;
  
  const Location({
    required this.latitude,
    required this.longitude,
    this.address,
  });
  
  factory Location.fromJson(Map<String, dynamic> json) {
    return Location(
      latitude: (json['latitude'] as num).toDouble(),
      longitude: (json['longitude'] as num).toDouble(),
      address: json['address'] as String?,
    );
  }
  
  Map<String, dynamic> toJson() {
    return {
      'latitude': latitude,
      'longitude': longitude,
      'address': address,
    };
  }
}

enum TripStatus {
  active,
  completed,
  paused,
  cancelled;
  
  static TripStatus fromString(String value) {
    return TripStatus.values.firstWhere(
      (status) => status.name == value,
      orElse: () => TripStatus.active,
    );
  }
}

Request Models

class StartTripRequest {
  final String vehicleId;
  final String? driverId;
  final Location? startLocation;
  final Map<String, dynamic>? metadata;
  final int? odometerReading;
  final double? fuelLevelPercent;
  
  const StartTripRequest({
    required this.vehicleId,
    this.driverId,
    this.startLocation,
    this.metadata,
    this.odometerReading,
    this.fuelLevelPercent,
  });
  
  Map<String, dynamic> toJson() {
    return {
      'vehicle_id': vehicleId,
      'driver_id': driverId,
      'start_location': startLocation?.toJson(),
      'metadata': metadata,
      'odometer_reading': odometerReading,
      'fuel_level_percent': fuelLevelPercent,
    };
  }
}

class TripFilters {
  final String? vehicleId;
  final String? driverId;
  final TripStatus? status;
  final DateTime? startDate;
  final DateTime? endDate;
  final int? limit;
  final int? offset;
  
  const TripFilters({
    this.vehicleId,
    this.driverId,
    this.status,
    this.startDate,
    this.endDate,
    this.limit = 25,
    this.offset = 0,
  });
  
  Map<String, dynamic> toJson() {
    return {
      'vehicle_id': vehicleId,
      'driver_id': driverId,
      'status': status?.name,
      'start_date': startDate?.toIso8601String().split('T').first,
      'end_date': endDate?.toIso8601String().split('T').first,
      'limit': limit,
      'offset': offset,
    };
  }
}

UI Components

Trip Management Widget

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class TripManagementWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<TripProvider>(
      builder: (context, tripProvider, child) {
        return Card(
          margin: const EdgeInsets.all(16),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Trip Status',
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
                const SizedBox(height: 16),
                
                if (tripProvider.currentTrip != null)
                  TripStatusCard(trip: tripProvider.currentTrip!)
                else
                  const Text('No active trip'),
                
                const SizedBox(height: 16),
                
                Row(
                  children: [
                    Expanded(
                      child: ElevatedButton(
                        onPressed: tripProvider.hasActiveTrip || tripProvider.isLoading
                            ? null
                            : () => _startTrip(context),
                        child: tripProvider.isLoading
                            ? const SizedBox(
                                height: 20,
                                width: 20,
                                child: CircularProgressIndicator(strokeWidth: 2),
                              )
                            : const Text('Start Trip'),
                      ),
                    ),
                    const SizedBox(width: 16),
                    Expanded(
                      child: ElevatedButton(
                        onPressed: !tripProvider.hasActiveTrip || tripProvider.isLoading
                            ? null
                            : () => tripProvider.stopTrip(),
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.red,
                          foregroundColor: Colors.white,
                        ),
                        child: const Text('Stop Trip'),
                      ),
                    ),
                  ],
                ),
                
                if (tripProvider.error != null)
                  Padding(
                    padding: const EdgeInsets.only(top: 16),
                    child: Text(
                      tripProvider.error!,
                      style: TextStyle(color: Colors.red[700]),
                    ),
                  ),
              ],
            ),
          ),
        );
      },
    );
  }
  
  void _startTrip(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => StartTripDialog(),
    );
  }
}

class TripStatusCard extends StatelessWidget {
  final Trip trip;
  
  const TripStatusCard({Key? key, required this.trip}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: _getStatusColor().withOpacity(0.1),
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: _getStatusColor()),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(_getStatusIcon(), color: _getStatusColor()),
              const SizedBox(width: 8),
              Text(
                'Trip ${trip.status.name.toUpperCase()}',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  color: _getStatusColor(),
                ),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Text(
            'ID: ${trip.tripId}',
            style: Theme.of(context).textTheme.bodySmall,
          ),
          const SizedBox(height: 12),
          
          Row(
            children: [
              Expanded(
                child: _buildMetric(
                  'Distance',
                  '${trip.analytics.distanceKm.toStringAsFixed(1)} km',
                ),
              ),
              Expanded(
                child: _buildMetric(
                  'Duration',
                  '${trip.analytics.durationMinutes} min',
                ),
              ),
              Expanded(
                child: _buildMetric(
                  'Safety',
                  '${trip.analytics.safetyScore}/100',
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
  
  Widget _buildMetric(String label, String value) {
    return Column(
      children: [
        Text(
          value,
          style: const TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
          ),
        ),
        Text(
          label,
          style: const TextStyle(
            fontSize: 12,
            color: Colors.grey,
          ),
        ),
      ],
    );
  }
  
  Color _getStatusColor() {
    switch (trip.status) {
      case TripStatus.active:
        return Colors.green;
      case TripStatus.completed:
        return Colors.blue;
      case TripStatus.paused:
        return Colors.orange;
      case TripStatus.cancelled:
        return Colors.red;
    }
  }
  
  IconData _getStatusIcon() {
    switch (trip.status) {
      case TripStatus.active:
        return Icons.play_circle;
      case TripStatus.completed:
        return Icons.check_circle;
      case TripStatus.paused:
        return Icons.pause_circle;
      case TripStatus.cancelled:
        return Icons.cancel;
    }
  }
}

Location Map Widget

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';

class LocationMapWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<LocationProvider>(
      builder: (context, locationProvider, child) {
        final position = locationProvider.currentPosition;
        
        return Container(
          height: 300,
          margin: const EdgeInsets.all(16),
          child: ClipRRect(
            borderRadius: BorderRadius.circular(8),
            child: position != null
                ? FlutterMap(
                    options: MapOptions(
                      center: LatLng(position.latitude, position.longitude),
                      zoom: 15.0,
                    ),
                    children: [
                      TileLayer(
                        urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
                        userAgentPackageName: 'com.bookovia.flutter',
                      ),
                      MarkerLayer(
                        markers: [
                          Marker(
                            width: 40.0,
                            height: 40.0,
                            point: LatLng(position.latitude, position.longitude),
                            builder: (context) => Container(
                              decoration: BoxDecoration(
                                color: locationProvider.isTracking 
                                    ? Colors.green 
                                    : Colors.grey,
                                shape: BoxShape.circle,
                                border: Border.all(color: Colors.white, width: 2),
                              ),
                              child: Icon(
                                Icons.navigation,
                                color: Colors.white,
                                size: 20,
                              ),
                            ),
                          ),
                        ],
                      ),
                    ],
                  )
                : Container(
                    color: Colors.grey[300],
                    child: const Center(
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Icon(Icons.location_off, size: 48, color: Colors.grey),
                          SizedBox(height: 8),
                          Text('No location available'),
                        ],
                      ),
                    ),
                  ),
          ),
        );
      },
    );
  }
}

Background Processing

Isolate for Location Processing

import 'dart:isolate';
import 'dart:convert';
import 'package:bookovia/bookovia.dart';

class LocationIsolate {
  static Isolate? _isolate;
  static SendPort? _sendPort;
  
  static Future<void> start() async {
    final receivePort = ReceivePort();
    
    _isolate = await Isolate.spawn(
      _locationIsolateEntryPoint,
      receivePort.sendPort,
    );
    
    _sendPort = await receivePort.first as SendPort;
  }
  
  static void stop() {
    _isolate?.kill(priority: Isolate.immediate);
    _isolate = null;
    _sendPort = null;
  }
  
  static void processLocation(Map<String, dynamic> locationData) {
    _sendPort?.send(locationData);
  }
  
  static void _locationIsolateEntryPoint(SendPort mainSendPort) {
    final receivePort = ReceivePort();
    mainSendPort.send(receivePort.sendPort);
    
    receivePort.listen((dynamic message) {
      if (message is Map<String, dynamic>) {
        _handleLocationUpdate(message);
      }
    });
  }
  
  static Future<void> _handleLocationUpdate(Map<String, dynamic> locationData) async {
    try {
      // Process location data in background
      final client = BookoviaClient(
        apiKey: locationData['apiKey'] as String,
      );
      
      await client.locations.upload(
        LocationUploadRequest.fromJson(locationData['location'] as Map<String, dynamic>),
      );
    } catch (e) {
      print('Background location processing error: $e');
    }
  }
}

Testing

Unit Tests

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:bookovia/bookovia.dart';

class MockBookoviaClient extends Mock implements BookoviaClient {}
class MockTripService extends Mock implements TripService {}

void main() {
  group('TripProvider Tests', () {
    late TripProvider tripProvider;
    late MockBookoviaClient mockClient;
    late MockTripService mockTripService;
    
    setUp(() {
      mockClient = MockBookoviaClient();
      mockTripService = MockTripService();
      when(mockClient.trips).thenReturn(mockTripService);
      
      tripProvider = TripProvider(client: mockClient);
    });
    
    test('should start trip successfully', () async {
      // Given
      final expectedTrip = Trip(
        tripId: 'trip_123',
        organizationId: 'org_456',
        vehicleId: 'vehicle_123',
        status: TripStatus.active,
        startTime: DateTime.now(),
        analytics: const TripAnalytics(
          distanceKm: 0,
          durationMinutes: 0,
          safetyScore: 100,
        ),
        createdAt: DateTime.now(),
        updatedAt: DateTime.now(),
      );
      
      when(mockTripService.start(any)).thenAnswer((_) async => expectedTrip);
      
      // When
      await tripProvider.startTrip('vehicle_123');
      
      // Then
      expect(tripProvider.currentTrip, equals(expectedTrip));
      expect(tripProvider.hasActiveTrip, isTrue);
      expect(tripProvider.isLoading, isFalse);
    });
    
    test('should handle start trip error', () async {
      // Given
      when(mockTripService.start(any)).thenThrow(Exception('Network error'));
      
      // When
      await tripProvider.startTrip('vehicle_123');
      
      // Then
      expect(tripProvider.currentTrip, isNull);
      expect(tripProvider.error, isNotNull);
      expect(tripProvider.error, contains('Network error'));
    });
  });
}

Widget Tests

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';

void main() {
  group('TripManagementWidget Tests', () {
    testWidgets('should show no active trip initially', (tester) async {
      final tripProvider = TripProvider();
      
      await tester.pumpWidget(
        MaterialApp(
          home: ChangeNotifierProvider.value(
            value: tripProvider,
            child: Scaffold(body: TripManagementWidget()),
          ),
        ),
      );
      
      expect(find.text('No active trip'), findsOneWidget);
      expect(find.text('Start Trip'), findsOneWidget);
      expect(find.text('Stop Trip'), findsOneWidget);
      
      final stopButton = tester.widget<ElevatedButton>(
        find.widgetWithText(ElevatedButton, 'Stop Trip'),
      );
      expect(stopButton.onPressed, isNull); // Should be disabled
    });
    
    testWidgets('should enable stop button when trip is active', (tester) async {
      final tripProvider = TripProvider();
      // Set up active trip
      tripProvider.setCurrentTrip(createMockActiveTrip());
      
      await tester.pumpWidget(
        MaterialApp(
          home: ChangeNotifierProvider.value(
            value: tripProvider,
            child: Scaffold(body: TripManagementWidget()),
          ),
        ),
      );
      
      expect(find.text('Trip ACTIVE'), findsOneWidget);
      
      final startButton = tester.widget<ElevatedButton>(
        find.widgetWithText(ElevatedButton, 'Start Trip'),
      );
      expect(startButton.onPressed, isNull); // Should be disabled
      
      final stopButton = tester.widget<ElevatedButton>(
        find.widgetWithText(ElevatedButton, 'Stop Trip'),
      );
      expect(stopButton.onPressed, isNotNull); // Should be enabled
    });
  });
}

Integration Tests

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:bookovia_example/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  group('Bookovia Integration Tests', () {
    testWidgets('should complete full trip lifecycle', (tester) async {
      app.main();
      await tester.pumpAndSettle();
      
      // Start trip
      await tester.tap(find.text('Start Trip'));
      await tester.pumpAndSettle();
      
      // Fill trip details
      await tester.enterText(find.byKey(const Key('vehicle_id_field')), 'test_vehicle');
      await tester.tap(find.text('Start'));
      
      // Wait for trip to start
      await tester.pump(const Duration(seconds: 2));
      
      // Verify trip status
      expect(find.text('Trip ACTIVE'), findsOneWidget);
      
      // Stop trip
      await tester.tap(find.text('Stop Trip'));
      await tester.pumpAndSettle();
      
      // Verify trip stopped
      expect(find.text('No active trip'), findsOneWidget);
    });
  });
}

Platform-Specific Features

Android Background Service

// android/app/src/main/kotlin/MainActivity.kt
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.bookovia.flutter/location"
    
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            when (call.method) {
                "startLocationService" -> {
                    startLocationService(call.arguments as String)
                    result.success(true)
                }
                "stopLocationService" -> {
                    stopLocationService()
                    result.success(true)
                }
                else -> result.notImplemented()
            }
        }
    }
    
    private fun startLocationService(tripId: String) {
        // Start Android foreground service
    }
    
    private fun stopLocationService() {
        // Stop Android foreground service
    }
}

iOS Background Processing

// ios/Runner/AppDelegate.swift
import UIKit
import Flutter
import BackgroundTasks

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        let controller = window?.rootViewController as! FlutterViewController
        let locationChannel = FlutterMethodChannel(
            name: "com.bookovia.flutter/location",
            binaryMessenger: controller.binaryMessenger
        )
        
        locationChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
            switch call.method {
            case "startBackgroundLocation":
                self.startBackgroundLocationTracking(arguments: call.arguments as? String)
                result(true)
            case "stopBackgroundLocation":
                self.stopBackgroundLocationTracking()
                result(true)
            default:
                result(FlutterMethodNotImplemented)
            }
        }
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    func startBackgroundLocationTracking(arguments tripId: String?) {
        // Implement iOS background location tracking
    }
    
    func stopBackgroundLocationTracking() {
        // Stop iOS background location tracking
    }
}

Offline-First Architecture

SQLite Schema v2 - Enterprise Sensor Support

The Flutter SDK includes a comprehensive SQLite schema that stores 60+ enterprise sensor fields locally on the device for offline-first data collection:
// SQLite Database v2 Schema
CREATE TABLE location_points (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  point_uuid TEXT UNIQUE NOT NULL,
  session_id INTEGER NOT NULL,

  -- Position (Core GPS)
  latitude REAL NOT NULL,
  longitude REAL NOT NULL,
  altitude REAL,
  accuracy REAL,
  speed_ms REAL,
  bearing REAL,
  timestamp_ms INTEGER NOT NULL,

  -- Linear Acceleration (Crash Detection)
  acceleration_x REAL,
  acceleration_y REAL,
  acceleration_z REAL,
  g_force_magnitude REAL,

  -- Rotational Motion (Gyroscope - Distracted Driving)
  rotation_x REAL,
  rotation_y REAL,
  rotation_z REAL,

  -- Magnetic Field (Magnetometer - Navigation)
  magnetic_x REAL,
  magnetic_y REAL,
  magnetic_z REAL,
  magnetic_bearing REAL,

  -- Pressure & Altitude (Elevation Tracking)
  pressure REAL,
  pressure_altitude REAL,

  -- GPS Quality (Fraud Detection)
  satellite_count INTEGER,
  hdop REAL,
  vdop REAL,
  location_provider TEXT,
  is_mocked INTEGER,

  -- Activity Recognition (Auto Trip Detection)
  activity TEXT,
  activity_confidence INTEGER,

  -- Device State (Battery Monitoring)
  battery_level INTEGER,
  battery_temp REAL,
  is_charging INTEGER,
  is_screen_on INTEGER,
  app_in_foreground INTEGER,

  -- Environmental Sensors (Night Driving & Crash Detection)
  light_level REAL,
  noise_level REAL,

  -- Phone Usage (Distracted Driving)
  is_call_active INTEGER,
  touch_count INTEGER,

  -- Connectivity (Network Quality)
  network_type TEXT,
  wifi_ssid TEXT,
  wifi_strength INTEGER,
  bluetooth_devices TEXT,

  -- Steps (Multi-Modal Trips)
  step_count INTEGER,

  -- OBD-II Vehicle Data (Enterprise)
  engine_rpm INTEGER,
  throttle_pos REAL,
  engine_load REAL,
  fuel_level REAL,
  coolant_temp REAL,

  -- Sync Metadata
  sync_status TEXT DEFAULT 'pending',
  sync_attempts INTEGER DEFAULT 0,
  last_sync_attempt INTEGER,
  created_at INTEGER NOT NULL,
  FOREIGN KEY (session_id) REFERENCES active_sessions(id)
);

Automatic Database Migration

The SDK automatically migrates existing installations from v1 to v2:
// Database version upgrade handled automatically
// v1 → v2: Adds 40+ new sensor columns
// Existing data preserved, new columns nullable

Future<void> _upgradeDatabase(Database db, int oldVersion, int newVersion) async {
  if (oldVersion < 2) {
    // Migration adds all enterprise sensor columns
    await db.execute('ALTER TABLE location_points ADD COLUMN rotation_x REAL');
    await db.execute('ALTER TABLE location_points ADD COLUMN magnetic_x REAL');
    // ... (33 total ALTER TABLE statements)
  }
}

Sensor Data Collection

The SDK collects sensor data when available and stores it locally:
await _database.saveLocationPoint(
  sessionId: sessionId,
  position: position,
  sensorData: {
    // Linear Acceleration
    'accelerationX': 0.2,
    'accelerationY': -0.1,
    'accelerationZ': 9.8,
    'gForceMagnitude': 1.02,
    
    // Rotational Motion (Gyroscope)
    'rotationX': 0.05,
    'rotationY': 0.02,
    'rotationZ': 0.01,
    
    // Magnetic Field (Magnetometer)
    'magneticX': 25.3,
    'magneticY': -12.7,
    'magneticZ': 42.1,
    'magneticBearing': 182.5,
    
    // Pressure & Altitude
    'pressure': 1013.25,
    'pressureAltitude': 10.2,
    
    // GPS Quality
    'satelliteCount': 12,
    'hdop': 1.2,
    'vdop': 1.8,
    'locationProvider': 'gps',
    'isMocked': false,
    
    // Activity Recognition
    'activity': 'driving',
    'activityConfidence': 95,
    
    // Device State
    'batteryTemp': 32.5,
    'isCharging': true,
    'isScreenOn': false,
    'appInForeground': true,
    
    // Environmental Sensors
    'lightLevel': 450.0,
    'noiseLevel': 65.0,
    
    // Phone Usage
    'isCallActive': false,
    'touchCount': 0,
    
    // Connectivity
    'wifiSSID': null,
    'wifiStrength': null,
    'bluetoothDevices': ['Honda Civic', 'OBD-II Adapter'],
    
    // Steps
    'stepCount': 1245,
    
    // OBD-II Vehicle Data
    'engineRPM': 2200,
    'throttlePos': 25.5,
    'engineLoad': 35.2,
    'fuelLevel': 68.0,
    'coolantTemp': 88.5,
  },
);

Automatic Sync to Backend

When internet connectivity is available, the SDK automatically syncs all 60+ fields to the backend:
// Automatic background sync every 5 minutes (configurable)
final response = await http.post(
  Uri.parse('${config.apiBaseUrl}/v1/trips/$tripId/gps/batch'),
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': config.apiKey,
  },
  body: jsonEncode({
    'points': pendingPoints.map((p) => {
      // All 60+ fields included in sync payload
      'latitude': p['latitude'],
      'longitude': p['longitude'],
      'rotation_x': p['rotation_x'],
      'magnetic_bearing': p['magnetic_bearing'],
      'activity': p['activity'],
      'light_level': p['light_level'],
      'engine_rpm': p['engine_rpm'],
      // ... all sensor fields
      'recorded_at': DateTime.fromMillisecondsSinceEpoch(p['timestamp_ms']).toIso8601String(),
    }).toList(),
  }),
);

Enterprise Analytics Enabled

With 60+ sensor fields, the following enterprise analytics are automatically available: Safety & Risk Analytics:
  • Distracted Driving Score (screen on + touches + rotation variance + calls)
  • Harsh Events (braking/acceleration > 3 m/s²)
  • Speeding Detection (actual vs posted limits)
  • Night Driving Hours (light_level < 10 lux = 3x crash risk)
  • Phone Usage While Driving (call active + movement)
Driver Performance:
  • Driver Ranking (multi-dimensional scoring)
  • Eco-Driving Score (smooth acceleration, optimal speed)
  • Idle Time Detection (speed < 0.5 m/s for > 2 minutes)
  • Route Efficiency (actual vs optimal path)
Vehicle Health:
  • Predictive Maintenance (high RPM + harsh events = wear)
  • Engine Health Score (DTC codes from OBD-II)
  • Fuel Efficiency (real consumption vs driving style)
  • Battery Health (temperature patterns)
Fraud Detection:
  • Mock GPS Detection (is_mocked = true flag)
  • GPS Quality Score (satellite count + HDOP)
  • Activity Mismatch (activity ≠ “driving” but speed > 20 km/h)
Auto Trip Detection:
  • Smart Start (Bluetooth car connected + activity = “driving”)
  • Smart Stop (activity = “walking” + step count increasing)
  • Driver vs Passenger (gyroscope patterns + activity)

Performance Optimization

Efficient Location Batching

class LocationBatchManager {
  final BookoviaClient _client;
  final List<LocationPoint> _locationBuffer = [];
  final int _batchSize;
  final Duration _uploadInterval;
  
  Timer? _uploadTimer;
  
  LocationBatchManager({
    required BookoviaClient client,
    int batchSize = 10,
    Duration uploadInterval = const Duration(seconds: 30),
  }) : _client = client,
       _batchSize = batchSize,
       _uploadInterval = uploadInterval;
  
  void start(String tripId) {
    _uploadTimer = Timer.periodic(_uploadInterval, (_) {
      _uploadBatch(tripId);
    });
  }
  
  void stop() {
    _uploadTimer?.cancel();
    _uploadTimer = null;
  }
  
  void addLocation(LocationPoint location) {
    _locationBuffer.add(location);
    
    if (_locationBuffer.length >= _batchSize) {
      _uploadBatch('current_trip_id'); // Get from state
    }
  }
  
  Future<void> _uploadBatch(String tripId) async {
    if (_locationBuffer.isEmpty) return;
    
    final locationsToUpload = List<LocationPoint>.from(_locationBuffer);
    _locationBuffer.clear();
    
    try {
      await _client.locations.batchUpload(
        BatchUploadRequest(
          tripId: tripId,
          locations: locationsToUpload,
        ),
      );
    } catch (e) {
      print('Failed to upload batch: $e');
      // Re-add locations for retry
      _locationBuffer.insertAll(0, locationsToUpload);
    }
  }
}

Support


Ready to build cross-platform telematics apps? Check out our quickstart guide and API reference for more examples.