Skip to main content
GuidesAPI ReferenceExamplesFAQ

Driver Behaviour

Estimated reading time: 5 minutes

This guide will teach you how to start analyzing driver behaviour using Maps SDK for Flutter.

How It Works

The example app demonstrates the following key features:

  • Initializing a map.
  • Configuring the map to use live data from the device's GPS.
  • Starting and stopping a driver behaviour analysis.
  • Viewing recorded driver behaviour sessions.
hello_maphello_maphello_map
Initial screenFinished recordingBehaviour analysis page

UI and Map Integration

The following code demonstrates how to build a user interface featuring a GemMap widget and an app bar. The app bar includes buttons for starting and stopping recordings, as well as following the user's position. After successfully completing a recording, a View Analysis button appears at the bottom of the screen. Clicking this button navigates the user to the AnalysesPage.

const projectApiToken = String.fromEnvironment('GEM_TOKEN');

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Driver Behaviour',
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});


State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
late GemMapController _mapController;
late DriverBehaviour _driverBehaviour;

DriverBehaviourAnalysis? _recordedAnalysis;

PermissionStatus _locationPermissionStatus = PermissionStatus.denied;
bool _hasLiveDataSource = false;
bool _isAnalizing = false;

void dispose() {
GemKit.release();
super.dispose();
}


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple[900],
title: const Text('Driver Behaviour', style: TextStyle(color: Colors.white)),
actions: [
if (_hasLiveDataSource && _isAnalizing == false)
IconButton(
onPressed: _onRecordButtonPressed,
icon: Icon(Icons.radio_button_on, color: Colors.white),
),
if (_isAnalizing)
IconButton(
onPressed: _onStopRecordingButtonPressed,
icon: Icon(Icons.stop_circle, color: Colors.white),
),
IconButton(
onPressed: _onFollowPositionButtonPressed,
icon: const Icon(
Icons.location_searching_sharp,
color: Colors.white,
),
),
],
),
body: Stack(
children: [
GemMap(
key: ValueKey("GemMap"),
onMapCreated: (controller) => _onMapCreated(controller),
appAuthorization: projectApiToken,
),
if (_recordedAnalysis != null)
Positioned(
bottom: 10.0,
left: 0.0,
right: 0.0,
child: ElevatedButton(
onPressed: () {
final analyses = _driverBehaviour.getAllDriverBehaviourAnalyses();
Navigator.of(context).push(MaterialPageRoute<void>(builder: (context) {
return AnalysesPage(behaviourAnalyses: analyses);
}));
},
child: Text("View Analysis")))
],
),
);
}

// The callback for when map is ready to use.
void _onMapCreated(GemMapController controller) async {
// Save controller for further usage.
_mapController = controller;
}

Future<void> _onFollowPositionButtonPressed() async {
if (kIsWeb) {
// On web platform permission are handled differently than other platforms.
// The SDK handles the request of permission for location.
final locationPermssionWeb = await PositionService.requestLocationPermission;
if (locationPermssionWeb == true) {
_locationPermissionStatus = PermissionStatus.granted;
} else {
_locationPermissionStatus = PermissionStatus.denied;
}
} else {
// For Android & iOS platforms, permission_handler package is used to ask for permissions.
_locationPermissionStatus = await Permission.locationWhenInUse.request();
}

if (_locationPermissionStatus == PermissionStatus.granted) {
// After the permission was granted, we can set the live data source (in most cases the GPS).
// The data source should be set only once, otherwise we'll get -5 error.
if (!_hasLiveDataSource) {
PositionService.instance.setLiveDataSource();
_hasLiveDataSource = true;
}

// Optionally, we can set an animation
final animation = GemAnimation(type: AnimationType.linear);

// Calling the start following position SDK method.
_mapController.startFollowingPosition(animation: animation);

setState(() {});
}
}

void _onRecordButtonPressed() {
final liveDataSource = DataSource.createLiveDataSource();

if (liveDataSource == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Creating a data source failed.'),
duration: Duration(seconds: 5),
),
);
return;
}

final driverBehaviour = DriverBehaviour(dataSource: liveDataSource, useMapMatch: true);

setState(() {
_isAnalizing = true;
_driverBehaviour = driverBehaviour;
});

final err = _driverBehaviour.startAnalysis();

if (!err) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Starting analysis failed.'),
duration: Duration(seconds: 5),
),
);
}
}

void _onStopRecordingButtonPressed() {
final analysis = _driverBehaviour.stopAnalysis();

setState(() {
_isAnalizing = false;
_recordedAnalysis = analysis;
});
}
}

Analyses Page

import 'package:flutter/material.dart';
import 'package:gem_kit/driver_behaviour.dart';
import 'package:intl/intl.dart';
import 'package:driver_behaviour/utils.dart';

class AnalysesPage extends StatelessWidget {
final List<DriverBehaviourAnalysis> behaviourAnalyses;
const AnalysesPage({super.key, required this.behaviourAnalyses});


Widget build(BuildContext context) {
final fmt = DateFormat.yMMMd().add_jm();

return Scaffold(
appBar: AppBar(
title: const Text('Analyses', style: TextStyle(color: Colors.white)),
backgroundColor: Colors.deepPurple[900],
foregroundColor: Colors.white,
),
body: behaviourAnalyses.isEmpty
? const Center(child: Text('No analyses recorded'))
: ListView.builder(
itemCount: behaviourAnalyses.length,
itemBuilder: (_, i) {
final a = behaviourAnalyses[i];
if (!a.isValid) {
return const ListTile(title: Text('Invalid analysis'));
}
final start = DateTime.fromMillisecondsSinceEpoch(a.startTime).toLocal();
final end = DateTime.fromMillisecondsSinceEpoch(a.finishTime).toLocal();
final dur = end.difference(start);

// Build a list of simple Text rows
final rows = <Widget>[
_buildRow('Start', fmt.format(start)),
_buildRow('End', fmt.format(end)),
_buildRow('Duration', formatDuration(dur)),
_buildRow('Distance (km)', a.kilometersDriven.toStringAsFixed(2)),
_buildRow('Driving Time (min)', a.minutesDriven.toStringAsFixed(1)),
_buildRow('Total Elapsed (min)', a.minutesTotalElapsed.toStringAsFixed(1)),
_buildRow('Speeding (min)', a.minutesSpeeding.toStringAsFixed(1)),
_buildRow('Risk Mean Speed (%)', formatPercent(a.riskRelatedToMeanSpeed)),
_buildRow('Risk Speed Var (%)', formatPercent(a.riskRelatedToSpeedVariation)),
const SizedBox(height: 8),
const Text('Events:', style: TextStyle(fontWeight: FontWeight.bold)),
_buildRow('Harsh Accel', a.numberOfHarshAccelerationEvents.toString()),
_buildRow('Harsh Braking', a.numberOfHarshBrakingEvents.toString()),
_buildRow('Cornering', a.numberOfCorneringEvents.toString()),
_buildRow('Swerving', a.numberOfSwervingEvents.toString()),
_buildRow('Ignored Stops', a.numberOfIgnoredStopSigns.toString()),
_buildRow('Stop Signs', a.numberOfEncounteredStopSigns.toString()),
];

return ExpansionTile(
title: Text('Trip ${i + 1}'),
subtitle: Text(fmt.format(start)),
childrenPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: rows,
);
},
),
);
}

Widget _buildRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label),
Text(value),
],
),
);
}
}

Utility Functions

String formatDuration(Duration d) {
final hours = d.inHours;
final minutes = d.inMinutes % 60;
final seconds = d.inSeconds % 60;
return [if (hours > 0) '${hours}h', if (minutes > 0) '${minutes}m', '${seconds}s'].join(' ');
}

String formatPercent(double value) => '${value.toStringAsFixed(1)}%';

Flutter Examples

Maps SDK for Flutter Examples can be downloaded or cloned with Git.