Skip to content

Offline Routing

In this guide, you will learn how to implement offline routing functionality using the gem_kit package. This example demonstrates how to download a map for offline use, disable internet access, and calculate routes offline.

offline_routing - example flutter screenshot

Setup

First, get an API key token, see the Getting Started guide.

Prerequisites

Make sure you completed the Environment Setup - Flutter Examples guide before starting this guide.

Build and Run

Start a terminal/command prompt and navigate to the offline_routing directory within the Flutter examples directory. This is the name of the example project.

Note - the gem_kit directory containing the Maps SDK for Flutter should be in the plugins directory of the example, e.g. example_pathname/plugins/gem_kit - see the environment setup guide above. Replace example_pathname with the actual example path name, such as address_search

Download project dependencies:

example flutter upgrade screenshot

flutter upgrade

example flutter clean screenshot

run the following terminal commands in the project directory, where the pubspec.yaml file is located:

flutter clean

example flutter pub get screenshot

flutter pub get

Run the example:

First, verify that the ANDROID_SDK_ROOT environment variable is set to the root path of your android SDK.

In android/build.gradle add the maven {} block as shown, within the allprojects {} block, for both debug and release builds, without the line numbers, those are for reference:

1allprojects {
2    repositories {
3        google()
4        mavenCentral()
5        maven {
6           url "${rootDir}/../plugins/gem_kit/android/build"
7        }
8    }
9}

in android/app/build.gradle within the android {} block, in the defaultConfig {} block, the android SDK version minSdk must be set as shown below.

Additionally, for release builds, in android/app/build.gradle, within the android {} block, add the buildTypes {} block as shown:

Replace example_pathname with the actual example pathname, such as center_coordinates

 1android {
 2    defaultConfig {
 3        applicationId "com.magiclane.gem_kit.examples.example_pathname"
 4        minSdk 21
 5        targetSdk flutter.targetSdk
 6        versionCode flutterVersionCode.toInteger()
 7        versionName flutterVersionName
 8    }
 9    buildTypes {
10        release {
11            minifyEnabled false
12            shrinkResources false
13
14            // TODO: Add your own signing config for the release build.
15            // Signing with the debug keys for now, so `flutter run --release` works.
16            signingConfig signingConfigs.debug
17        }
18    }
19}

Then run the project:

flutter run --debug
or
flutter run --release

In the ios/Podfile configuration text file, at the top, set the minimum ios platform to 13 like this:

platform :ios, '13.0'

Then run the project:

flutter run --debug
or
flutter run --release

How it Works

This example demonstrates how to use the gem_kit package to download a map for offline use, disable internet access, and calculate routes offline.

offline_routing - example flutter screenshot

Import Necessary Packages

First, import the required packages in your Dart code.

1import 'package:gem_kit/core.dart';
2import 'package:gem_kit/map.dart';
3import 'package:gem_kit/routing.dart';
4import 'package:flutter/material.dart' hide Route;

Initialize GemKit

In the main function, initialize GemKit with your project API token.

1Future<void> main() async {
2  const projectApiToken = String.fromEnvironment('GEM_TOKEN');
3  await GemKit.initialize(appAuthorization: projectApiToken);
4  runApp(const MyApp());
5}

Build the Main Application

Define the main application widget, MyApp.

 1class MyApp extends StatelessWidget {
 2  const MyApp({super.key});
 3
 4  @override
 5  Widget build(BuildContext context) {
 6    return const MaterialApp(
 7        title: 'Offline Routing',
 8        debugShowCheckedModeBanner: false,
 9        home: MyHomePage());
10  }
11}

Handle Maps and Routes in the Stateful Widget

offline_routing - example flutter screenshot

Create the stateful widget, MyHomePage, which will handle the map and routing functionality.

1class MyHomePage extends StatefulWidget {
2  const MyHomePage({super.key});
3
4  @override
5  State<MyHomePage> createState() => _MyHomePageState();
6}

Define State Variables and Methods

Within _MyHomePageState, define the necessary state variables and methods to interact with the map and manage routes.

  1class _MyHomePageState extends State<MyHomePage> {
  2  late GemMapController _mapController;
  3  bool _areRoutesBuilt = false;
  4  TaskHandler? _routingHandler;
  5  bool _isDownloaded = false;
  6  double _downloadProgress = 0;
  7
  8  @override
  9  void dispose() {
 10    GemKit.release();
 11    super.dispose();
 12  }
 13
 14  @override
 15  Widget build(BuildContext context) {
 16    return Scaffold(
 17      appBar: AppBar(
 18        backgroundColor: Colors.deepPurple[900],
 19        title: const Text(
 20          'Offline Routing',
 21          style: TextStyle(color: Colors.white),
 22        ),
 23        actions: [
 24          if (!_isDownloaded && _downloadProgress != 0)
 25            Container(
 26              width: 20,
 27              height: 20,
 28              margin: const EdgeInsets.only(right: 10.0),
 29              child: const Center(
 30                child: CircularProgressIndicator(color: Colors.white),
 31              ),
 32            ),
 33          if (!_isDownloaded && _downloadProgress == 0)
 34            IconButton(
 35              onPressed: _setOfflineMap,
 36              icon: const Icon(
 37                Icons.download,
 38                color: Colors.white,
 39              ),
 40            ),
 41          if (_routingHandler == null && !_areRoutesBuilt && _isDownloaded)
 42            IconButton(
 43              onPressed: () => _onBuildRouteButtonPressed(context),
 44              icon: const Icon(
 45                Icons.route,
 46                color: Colors.white,
 47              ),
 48            ),
 49          if (_routingHandler != null)
 50            IconButton(
 51              onPressed: _onCancelRouteButtonPressed,
 52              icon: const Icon(
 53                Icons.stop,
 54                color: Colors.white,
 55              ),
 56            ),
 57          if (_areRoutesBuilt)
 58            IconButton(
 59              onPressed: _onClearRoutesButtonPressed,
 60              icon: const Icon(
 61                Icons.clear,
 62                color: Colors.white,
 63              ),
 64            ),
 65        ],
 66      ),
 67      body: GemMap(onMapCreated: _onMapCreated),
 68    );
 69  }
 70
 71  void _onMapCreated(GemMapController controller) async {
 72    _mapController = controller;
 73    SdkSettings.setAllowOffboardServiceOnExtraChargedNetwork(
 74        ServiceGroupType.contentService, true);
 75  }
 76
 77  void _onBuildRouteButtonPressed(BuildContext context) {
 78    final departureLandmark =
 79        Landmark.withLatLng(latitude: 42.49720, longitude: 1.50498);
 80    final destinationLandmark =
 81        Landmark.withLatLng(latitude: 42.51003, longitude: 1.53400);
 82    final routePreferences = RoutePreferences();
 83
 84    _showSnackBar(context, message: "The route is being calculated.");
 85
 86    _routingHandler = RoutingService.calculateRoute(
 87        [departureLandmark, destinationLandmark], routePreferences,
 88        (err, routes) {
 89      _routingHandler = null;
 90      ScaffoldMessenger.of(context).clearSnackBars();
 91
 92      if (err == GemError.success) {
 93        final routesMap = _mapController.preferences.routes;
 94        for (final route in routes!) {
 95          routesMap.add(route, route == routes.first, label: route.getMapLabel());
 96        }
 97        _mapController.centerOnRoutes(routes);
 98        setState(() {
 99          _areRoutesBuilt = true;
100        });
101      }
102    });
103
104    setState(() {});
105  }
106
107  void _onClearRoutesButtonPressed() {
108    _mapController.preferences.routes.clear();
109    setState(() {
110      _areRoutesBuilt = false;
111    });
112  }
113
114  void _onCancelRouteButtonPressed() {
115    if (_routingHandler != null) {
116      RoutingService.cancelRoute(_routingHandler!);
117      setState(() {
118        _routingHandler = null;
119      });
120    }
121  }
122
123  Future<List<ContentStoreItem>> _getMaps() async {
124    Completer<List<ContentStoreItem>> mapsList =
125        Completer<List<ContentStoreItem>>();
126
127    ContentStore.asyncGetStoreContentList(ContentType.roadMap,
128        (err, items, isCached) {
129      if (err == GemError.success && items != null) {
130        mapsList.complete(items);
131      }
132    });
133    return mapsList.future;
134  }
135
136  void _setOfflineMap() {
137    final localMaps = ContentStore.getLocalContentList(ContentType.roadMap);
138
139    if (localMaps.where((map) => map.name == 'Andorra').isNotEmpty) {
140      setState(() {
141        _isDownloaded = true;
142      });
143      SdkSettings.setAllowConnection(false);
144      return;
145    }
146
147    _getMaps().then((maps) {
148      _downloadProgress = maps[4].downloadProgress.toDouble();
149      _downloadMap(maps[4]);
150    });
151  }
152
153  void _downloadMap(ContentStoreItem map) {
154    map.asyncDownload(_onMapDownloadFinished,
155        onProgressCallback: _onMapDownloadProgressUpdated,
156        allowChargedNetworks: true);
157  }
158
159  void _onMapDownloadProgressUpdated(int progress) {
160    setState(() => _downloadProgress = progress.toDouble());
161  }
162
163  void _onMapDownloadFinished(GemError err) {
164    if (err == GemError.success) {
165      SdkSettings.setAllowConnection(false);
166      setState(() => _isDownloaded = true);
167    }
168  }
169
170  void _showSnackBar(BuildContext context,
171      {required String message, Duration duration = const Duration(hours: 1)}) {
172    final snackBar = SnackBar(
173      content: Text(message),
174      duration: duration,
175    );
176
177    ScaffoldMessenger.of(context).showSnackBar(snackBar);
178  }
179}

Define Extension for Route Label

Define an extension to format the route label displayed on the map.

 1extension RouteExtension on Route {
 2  String getMapLabel() {
 3    final totalDistance = getTimeDistance().unrestrictedDistanceM +
 4        getTimeDistance().restrictedDistanceM;
 5    final totalDuration =
 6        getTimeDistance().unrestrictedTimeS + getTimeDistance().restrictedTimeS;
 7
 8    return '${_convertDistance(totalDistance)} \n${_convertDuration(totalDuration)}';
 9  }
10
11  String _convertDistance(int meters) {
12    if (meters >= 1000) {
13      double kilometers = meters / 1000;
14      return '${kilometers.toStringAsFixed(1)} km';
15    }
16    return '$meters m';
17  }
18
19  String _convertDuration(int seconds) {
20    if (seconds >= 3600) {
21      int hours = seconds ~/ 3600;
22      int minutes = (seconds % 3600) ~/ 60;
23      return '${hours}h ${minutes}m';
24    } else if (seconds >= 60) {
25      int minutes = seconds ~/ 60;
26      return '${minutes}m';
27    }
28    return '${seconds}s';
29  }
30}

Flutter Examples

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