Skip to main content
GuidesAPI ReferenceExamplesFAQ

Recorder

Estimated reading time: 5 minutes

This example demonstrates how to build a Flutter app using the Maps SDK to record and display the user's track.

How It Works

The example app highlights the following features:

  • Initializing a map.
  • Configuring the map to use live data from the device's GPS.
  • Starting and stopping a recording while displaying the track on the map.
hello_maphello_maphello_map
Initial mapRecording startedStopped recording

UI and Map Integration

The following code builds the UI with a GemMap widget and an app bar that includes buttons for starting/stopping recording and following the user's position.

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


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

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


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

class _MyHomePageState extends State<MyHomePage> {
late GemMapController _mapController;
late Recorder _recorder;

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

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


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple[900],
title: const Text('Recorder', style: TextStyle(color: Colors.white)),
actions: [
if (_hasLiveDataSource && _isRecording == false)
IconButton(
onPressed: _onRecordButtonPressed,
icon: Icon(Icons.radio_button_on, color: Colors.white),
),
if (_isRecording)
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,
),
],
),
);
}

Requesting Location Permission

The following code centers the camera on the user's current position if location permission is granted. Otherwise, it requests the necessary permission.

  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.
_locationPermissionStatus = PermissionStatus.granted;
} 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(() {});
}
}

Starting and Stopping Recording

  Future<void> _onRecordButtonPressed() async {
// Helper function that returns path to the Tracks directory
final logsDir = await getDirectoryPath("Tracks");

final recorder = Recorder.create(
RecorderConfiguration(
dataSource: DataSource.createLiveDataSource()!,
logsDir: logsDir,
recordedTypes: [DataType.position],
minDurationSeconds: 0,
),
);

setState(() {
_isRecording = true;
_recorder = recorder;
});

await _recorder.startRecording();

// Clear displayed paths
_mapController.preferences.paths.clear();
_mapController.deactivateAllHighlights();
}

Future<void> _onStopRecordingButtonPressed() async {
final endErr = await _recorder.stopRecording();

if (endErr == GemError.success) {
await _presentRecordedRoute();
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Recording failed: $endErr'),
duration: Duration(seconds: 5),
),
);
}
}

setState(() {
_isRecording = false;
});
}

Presenting the Recorded Track on the Map

This code loads the last recorded track from device memory, retrieves the coordinates, builds a Path entity, and adds it to the MapViewPathCollection.

  Future<void> _presentRecordedRoute() async {
// The recorded tracks are stored in /Data/Tracks directory
final logsDir = await getDirectoryPath("Tracks");

// It loads all .gm and .mp4 files at logsDir
final bookmarks = RecorderBookmarks.create(logsDir);

// Get all recordings path
final logList = bookmarks?.logsList;

// Get the LogMetadata to obtain details about recorded session
LogMetadata meta = bookmarks!.getLogMetadata(logList!.last);
final recorderCoordinates = meta.preciseRoute;
final duration = convertDuration(meta.durationMillis);

// Create a path entity from coordinates
final path = Path.fromCoordinates(recorderCoordinates);

Landmark beginLandmark = Landmark.withCoordinates(recorderCoordinates.first);
Landmark endLandmark = Landmark.withCoordinates(recorderCoordinates.last);

beginLandmark.setImageFromIcon(GemIcon.waypointStart);
endLandmark.setImageFromIcon(GemIcon.waypointFinish);

HighlightRenderSettings renderSettings = HighlightRenderSettings(
options: {HighlightOptions.showLandmark},
);

_mapController.activateHighlight([beginLandmark, endLandmark], renderSettings: renderSettings, highlightId: 1);

// Show the path immediately after stopping recording
_mapController.preferences.paths.add(path);

// Center on recorder path
_mapController.centerOnAreaRect(
path.area,
viewRc: RectType(
x: _mapController.viewport.width ~/ 3,
y: _mapController.viewport.height ~/ 3,
width: _mapController.viewport.width ~/ 3,
height: _mapController.viewport.height ~/ 3,
),
);

if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Duration: $duration'),
duration: Duration(seconds: 5),
),
);
}
}

Utility Functions

The getDirectoryPath function retrieves the root directory path for the app and returns the desired directory path inside the "Data" folder.

import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:path/path.dart' as path;

import 'dart:io';

Future<String> getDirectoryPath(String dirName) async {
final docDirectory =
Platform.isAndroid
? await path_provider.getExternalStorageDirectory()
: await path_provider.getApplicationDocumentsDirectory();

String absPath = docDirectory!.path;

final expectedPath = path.joinAll([absPath, "Data", dirName]);
return expectedPath;
}

// Utility function to convert the seconds duration into a suitable format
String convertDuration(int milliseconds) {
int seconds = (milliseconds / 1000).floor();
int hours = seconds ~/ 3600; // Number of whole hours
int minutes = (seconds % 3600) ~/ 60; // Number of whole minutes

String hoursText = (hours > 0) ? '$hours h ' : ''; // Hours text
String minutesText = (minutes > 0) ? '$minutes min ' : ''; // Minutes text
String secondsText = '$seconds sec';

return hoursText + minutesText + secondsText;
}

Flutter Examples

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