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.

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:

:lineno-start: 1

    allprojects {
        repositories {
            google()
            mavenCentral()
            maven {
               url "${rootDir}/../plugins/gem_kit/android/build"
            }
        }
    }

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

:lineno-start: 1

android {
    defaultConfig {
        applicationId "com.magiclane.gem_kit.examples.example_pathname"
        minSdk 21
        targetSdk flutter.targetSdk
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
    buildTypes {
        release {
            minifyEnabled false
            shrinkResources false

            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}

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

platform :ios, '14.0'

We recommend you to run these commands after you copy the gem_kit into your project: |flutter clean |flutter pub get |and |cd ios |pod install

Then run the project:

flutter run --debug
or
flutter run --release

App entry and initialization

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

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

This code initializes the projectApiToken with the required authorization token and launches the app.

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.

Build the Main Application

Define the main application widget, MyApp.

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

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

Handle Maps and Routes in the Stateful Widget

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

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

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

Define State Variables and Methods

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

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

  bool _areRoutesBuilt = false;

  // We use the handler to cancel the route calculation.
  TaskHandler? _routingHandler;

  bool _isDownloaded = false;
  double _downloadProgress = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.deepPurple[900],
        title: const Text(
          'Offline Routing',
          style: TextStyle(color: Colors.white),
        ),
        actions: [
          // Map is downloading.
          if (_isDownloaded == false && _downloadProgress != 0)
            Container(
              width: 20,
              height: 20,
              margin: const EdgeInsets.only(right: 10.0),
              child: const Center(
                child: CircularProgressIndicator(color: Colors.white),
              ),
            ),
          // Map is not downloaded.
          if (_isDownloaded == false && _downloadProgress == 0)
            IconButton(
              onPressed: () => _setOfflineMap(),
              icon: const Icon(
                Icons.download,
                color: Colors.white,
              ),
            ),
          // Routes are not built.
          if (_routingHandler == null &&
              _areRoutesBuilt == false &&
              _isDownloaded == true)
            IconButton(
              onPressed: () => _onBuildRouteButtonPressed(context),
              icon: const Icon(
                Icons.route,
                color: Colors.white,
              ),
            ),
          // Routes calculating is in progress.
          if (_routingHandler != null)
            IconButton(
              onPressed: () => _onCancelRouteButtonPressed(),
              icon: const Icon(
                Icons.stop,
                color: Colors.white,
              ),
            ),
          // Routes calculating is finished.
          if (_areRoutesBuilt == true)
            IconButton(
              onPressed: () => _onClearRoutesButtonPressed(),
              icon: const Icon(
                Icons.clear,
                color: Colors.white,
              ),
            ),
        ],
      ),
      body: GemMap(onMapCreated: _onMapCreated, appAuthorization: projectApiToken),
    );
  }

  void _onMapCreated(GemMapController controller) async {
    // Save controller for further usage.
    _mapController = controller;

    SdkSettings.setAllowOffboardServiceOnExtraChargedNetwork(
        ServiceGroupType.contentService, true);
  }

offline_routing - example flutter screenshot

Define Route Calculation Logic

Implement methods to build routes based on predefined waypoints and manage the download of maps.

void _onBuildRouteButtonPressed(BuildContext context) {
  // Define the departure.
  final departureLandmark =
      Landmark.withLatLng(latitude: 42.49720, longitude: 1.50498);

  // Define the destination.
  final destinationLandmark =
      Landmark.withLatLng(latitude: 42.51003, longitude: 1.53400);

  // Define the route preferences.
  final routePreferences = RoutePreferences();

  _showSnackBar(context, message: "The route is being calculated.");

  // Calling the calculateRoute SDK method.
  _routingHandler = RoutingService.calculateRoute(
      [departureLandmark, destinationLandmark], routePreferences,
      (err, routes) {
    _routingHandler = null;
    ScaffoldMessenger.of(context).clearSnackBars();

    if (err == GemError.success) {
      final routesMap = _mapController.preferences.routes;
      for (final route in routes!) {
        routesMap.add(route, route == routes.first,
            label: route.getMapLabel());
      }
      _mapController.centerOnRoutes(routes);
      setState(() {
        _areRoutesBuilt = true;
      });
    }
  });

  setState(() {});
}

void _onClearRoutesButtonPressed() {
  _mapController.preferences.routes.clear();
  setState(() {
    _areRoutesBuilt = false;
  });
}

void _onCancelRouteButtonPressed() {
  if (_routingHandler != null) {
    RoutingService.cancelRoute(_routingHandler!);
    setState(() {
      _routingHandler = null;
    });
  }
}

Define Map Downloading Logic

Implement methods for downloading and managing the offline map.

Future<List<ContentStoreItem>> _getMaps() async {
  Completer<List<ContentStoreItem>> mapsList =
      Completer<List<ContentStoreItem>>();

  ContentStore.asyncGetStoreContentList(ContentType.roadMap,
      (err, items, isCached) {
    if (err == GemError.success && items != null) {
      mapsList.complete(items);
    }
  });
  return mapsList.future;
}

void _setOfflineMap() {
  final localMaps = ContentStore.getLocalContentList(ContentType.roadMap);

  if (localMaps.where((map) => map.name == 'Andorra').isNotEmpty) {
    setState(() {
      _isDownloaded = true;
    });

    SdkSettings.setAllowConnection(false);
    return;
  }

  _getMaps().then((maps) {
    _downloadProgress = maps[4].downloadProgress.toDouble();
    _downloadMap(maps[4]);
  });
}

void _downloadMap(ContentStoreItem map) {
  map.asyncDownload(_onMapDownloadFinished,
      onProgressCallback: _onMapDownloadProgressUpdated,
      allowChargedNetworks: true);
}

void _onMapDownloadProgressUpdated(int progress) {
  setState(() => _downloadProgress = progress.toDouble());
}

void _onMapDownloadFinished(GemError err) {
  if (err == GemError.success) {
    SdkSettings.setAllowConnection(false);
    setState(() => _isDownloaded = true);
  }
}

Show SnackBar for User Feedback

Implement a method to show a SnackBar for providing feedback to the user.

void _showSnackBar(BuildContext context,
    {required String message, Duration duration = const Duration(hours: 1)}) {
  final snackBar = SnackBar(
    content: Text(message),
    duration: duration,
  );

  ScaffoldMessenger.of(context).showSnackBar(snackBar);
}

offline_routing - example flutter screenshot

Define Extension for Route Label

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

extension RouteExtension on Route {
  String getMapLabel() {
    final totalDistance = getTimeDistance().unrestrictedDistanceM +
        getTimeDistance().restrictedDistanceM;
    final totalDuration =
        getTimeDistance().unrestrictedTimeS + getTimeDistance().restrictedTimeS;

    return '${_convertDistance(totalDistance)} \n${_convertDuration(totalDuration)}';
  }

  String _convertDistance(int meters) {
    if (meters >= 1000) {
      double kilometers = meters / 1000;
      return '${kilometers.toStringAsFixed(1)} km';
    }
    return '$meters m';
  }

  String _convertDuration(int seconds) {
    if (seconds >= 3600) {
      int hours = seconds ~/ 3600;
      int minutes = (seconds % 3600) ~/ 60;
      return '${hours}h ${minutes}m';
    } else if (seconds >= 60) {
      int minutes = seconds ~/ 60;
      return '${minutes}m';
    }
    return '${seconds}s';
  }
}

Flutter Examples

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