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.
Run: flutter pub get
Configure the native parts:
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:
1 allprojects {
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 project pathname
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
orflutter 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/content_store.dart';
2import 'package:gem_kit/core.dart';
3import 'package:gem_kit/map.dart';
4import 'package:gem_kit/routing.dart';
5import '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
4 bool _areRoutesBuilt = false;
5
6 // We use the handler to cancel the route calculation.
7 TaskHandler? _routingHandler;
8
9 bool _isDownloaded = false;
10 double _downloadProgress = 0;
11
12 @override
13 void dispose() {
14 GemKit.release();
15 super.dispose();
16 }
17
18 @override
19 Widget build(BuildContext context) {
20 return Scaffold(
21 appBar: AppBar(
22 backgroundColor: Colors.deepPurple[900],
23 title: const Text(
24 'Offline Routing',
25 style: TextStyle(color: Colors.white),
26 ),
27 actions: [
28 // Map is downloading.
29 if (_isDownloaded == false && _downloadProgress != 0)
30 Container(
31 width: 20,
32 height: 20,
33 margin: const EdgeInsets.only(right: 10.0),
34 child: const Center(
35 child: CircularProgressIndicator(color: Colors.white),
36 ),
37 ),
38 // Map is not downloaded.
39 if (_isDownloaded == false && _downloadProgress == 0)
40 IconButton(
41 onPressed: () => _setOfflineMap(),
42 icon: const Icon(
43 Icons.download,
44 color: Colors.white,
45 ),
46 ),
47 // Routes are not built.
48 if (_routingHandler == null &&
49 _areRoutesBuilt == false &&
50 _isDownloaded == true)
51 IconButton(
52 onPressed: () => _onBuildRouteButtonPressed(context),
53 icon: const Icon(
54 Icons.route,
55 color: Colors.white,
56 ),
57 ),
58 // Routes calculating is in progress.
59 if (_routingHandler != null)
60 IconButton(
61 onPressed: () => _onCancelRouteButtonPressed(),
62 icon: const Icon(
63 Icons.stop,
64 color: Colors.white,
65 ),
66 ),
67 // Routes calculating is finished.
68 if (_areRoutesBuilt == true)
69 IconButton(
70 onPressed: () => _onClearRoutesButtonPressed(),
71 icon: const Icon(
72 Icons.clear,
73 color: Colors.white,
74 ),
75 ),
76 ],
77 ),
78 body: GemMap(onMapCreated: _onMapCreated),
79 );
80 }
81
82 void _onMapCreated(GemMapController controller) async {
83 // Save controller for further usage.
84 _mapController = controller;
85
86 SdkSettings.setAllowOffboardServiceOnExtraChargedNetwork(
87 ServiceGroupType.contentService, true);
88 }
Define Route Calculation Logic¶
Implement methods to build routes based on predefined waypoints and manage the download of maps.
1void _onBuildRouteButtonPressed(BuildContext context) {
2 // Define the departure.
3 final departureLandmark =
4 Landmark.withLatLng(latitude: 42.49720, longitude: 1.50498);
5
6 // Define the destination.
7 final destinationLandmark =
8 Landmark.withLatLng(latitude: 42.51003, longitude: 1.53400);
9
10 // Define the route preferences.
11 final routePreferences = RoutePreferences();
12
13 _showSnackBar(context, message: "The route is being calculated.");
14
15 // Calling the calculateRoute SDK method.
16 _routingHandler = RoutingService.calculateRoute(
17 [departureLandmark, destinationLandmark], routePreferences,
18 (err, routes) {
19 _routingHandler = null;
20 ScaffoldMessenger.of(context).clearSnackBars();
21
22 if (err == GemError.success) {
23 final routesMap = _mapController.preferences.routes;
24 for (final route in routes!) {
25 routesMap.add(route, route == routes.first,
26 label: route.getMapLabel());
27 }
28 _mapController.centerOnRoutes(routes);
29 setState(() {
30 _areRoutesBuilt = true;
31 });
32 }
33 });
34
35 setState(() {});
36}
37
38void _onClearRoutesButtonPressed() {
39 _mapController.preferences.routes.clear();
40 setState(() {
41 _areRoutesBuilt = false;
42 });
43}
44
45void _onCancelRouteButtonPressed() {
46 if (_routingHandler != null) {
47 RoutingService.cancelRoute(_routingHandler!);
48 setState(() {
49 _routingHandler = null;
50 });
51 }
52}
Define Map Downloading Logic¶
Implement methods for downloading and managing the offline map.
1Future<List<ContentStoreItem>> _getMaps() async {
2 Completer<List<ContentStoreItem>> mapsList =
3 Completer<List<ContentStoreItem>>();
4
5 ContentStore.asyncGetStoreContentList(ContentType.roadMap,
6 (err, items, isCached) {
7 if (err == GemError.success && items != null) {
8 mapsList.complete(items);
9 }
10 });
11 return mapsList.future;
12}
13
14void _setOfflineMap() {
15 final localMaps = ContentStore.getLocalContentList(ContentType.roadMap);
16
17 if (localMaps.where((map) => map.name == 'Andorra').isNotEmpty) {
18 setState(() {
19 _isDownloaded = true;
20 });
21
22 SdkSettings.setAllowConnection(false);
23 return;
24 }
25
26 _getMaps().then((maps) {
27 _downloadProgress = maps[4].downloadProgress.toDouble();
28 _downloadMap(maps[4]);
29 });
30}
31
32void _downloadMap(ContentStoreItem map) {
33 map.asyncDownload(_onMapDownloadFinished,
34 onProgressCallback: _onMapDownloadProgressUpdated,
35 allowChargedNetworks: true);
36}
37
38void _onMapDownloadProgressUpdated(int progress) {
39 setState(() => _downloadProgress = progress.toDouble());
40}
41
42void _onMapDownloadFinished(GemError err) {
43 if (err == GemError.success) {
44 SdkSettings.setAllowConnection(false);
45 setState(() => _isDownloaded = true);
46 }
47}
Show SnackBar for User Feedback¶
Implement a method to show a SnackBar for providing feedback to the user.
1void _showSnackBar(BuildContext context,
2 {required String message, Duration duration = const Duration(hours: 1)}) {
3 final snackBar = SnackBar(
4 content: Text(message),
5 duration: duration,
6 );
7
8 ScaffoldMessenger.of(context).showSnackBar(snackBar);
9}
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}