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 yourpubspec.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
flutter pub get
- Flutter 3.0+
- Dart 2.18+
- Android SDK 21+
- iOS 13.0+
Setup
Android Configuration
Add permissions toandroid/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 toios/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 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)
- 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)
- Mock GPS Detection (is_mocked = true flag)
- GPS Quality Score (satellite count + HDOP)
- Activity Mismatch (activity ≠ “driving” but speed > 20 km/h)
- 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
- GitHub: github.com/bookovia/flutter-sdk
- pub.dev: pub.dev/packages/bookovia
- Issues: GitHub Issues
- Email: flutter-support@bookovia.com
- Discord: Bookovia Flutter Community
Ready to build cross-platform telematics apps? Check out our quickstart guide and API reference for more examples.