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.
Setup¶
Prerequisites¶
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:
flutter upgrade
run the following terminal commands in the project directory,
where the pubspec.yaml
file is located:
flutter clean
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
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
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.
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¶
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}