Skip to content

Speed Watcher

In this guide, you will learn how to monitor speed using an interactive map, calculate a route based on GPS data, and visualize navigation instructions.

Speed Watcher - 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

Go to the speed_watcher directory within the Flutter examples directory.

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

Import Necessary Packages

Start by importing the required packages.

import 'package:gem_kit/core.dart';
import 'package:gem_kit/map.dart';
import 'package:gem_kit/navigation.dart';
import 'package:gem_kit/routing.dart';
import 'package:flutter/material.dart';

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 monitor speed while navigating using the gem_kit package.

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(
      debugShowCheckedModeBanner: false,
      title: 'Speed Watcher',
      home: MyHomePage(),
    );
  }
}

Handle Map and Route Functionality

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 manage the map and navigation.

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

  bool _areRoutesBuilt = false;
  bool _isSimulationActive = false;

  TaskHandler? _routingHandler;
  TaskHandler? _navigationHandler;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Speed Watcher", style: TextStyle(color: Colors.white)),
        backgroundColor: Colors.deepPurple[900],
        actions: [
          if (!_isSimulationActive && _areRoutesBuilt)
            IconButton(
              onPressed: _startSimulation,
              icon: const Icon(Icons.play_arrow, color: Colors.white),
            ),
          if (_isSimulationActive)
            IconButton(
              onPressed: _stopSimulation,
              icon: const Icon(Icons.stop, color: Colors.white),
            ),
          if (!_areRoutesBuilt)
            IconButton(
              onPressed: () => _onBuildRouteButtonPressed(context),
              icon: const Icon(Icons.route, color: Colors.white),
            ),
        ],
      ),
      body: Stack(children: [
        GemMap(onMapCreated: _onMapCreated, appAuthorization: projectApiToken),
        if (_isSimulationActive) const Align(alignment: Alignment.topCenter, child: SpeedIndicator()),
        if (_isSimulationActive) Align(alignment: Alignment.bottomCenter, child: FollowPositionButton(onTap: () => _mapController.startFollowingPosition())),
      ]),
      resizeToAvoidBottomInset: false,
    );
  }

  void _onMapCreated(GemMapController controller) {
    _mapController = controller;
  }

Custom Method for Building the Route

Define a method for building the route based on the departure and destination landmarks.

void _onBuildRouteButtonPressed(BuildContext context) {
  final departureLandmark = Landmark.withLatLng(latitude: 41.898499, longitude: 12.526655);
  final destinationLandmark = Landmark.withLatLng(latitude: 41.891037, longitude: 12.492692);
  final routePreferences = RoutePreferences();
  _showSnackBar(context, message: 'The route is calculating.');

  _routingHandler = RoutingService.calculateRoute(
      [departureLandmark, destinationLandmark], routePreferences,
      (err, routes) {
        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;
        });
      });
}

Starting the Simulation

Define the method for starting the navigation simulation.

void _startSimulation() {
  final routes = _mapController.preferences.routes;

  _navigationHandler = NavigationService.startSimulation(routes.mainRoute,
      (type, instruction) {
    if (type == NavigationEventType.destinationReached || type == NavigationEventType.error) {
      setState(() {
        _isSimulationActive = false;
        _cancelRoute();
      });
      return;
    }
    _isSimulationActive = true;

    if (instruction != null) {
      setState(() => currentInstruction = instruction);
    }
  });

  _mapController.startFollowingPosition(); // Follow position during navigation
}

Stopping the Simulation

Define the method for stopping the navigation simulation.

void _stopSimulation() {
  NavigationService.cancelNavigation(_navigationHandler!);
  _navigationHandler = null;

  _cancelRoute();

  setState(() => _isSimulationActive = false);
}

Canceling the Route

Define the method for canceling the route.

void _cancelRoute() {
  _mapController.preferences.routes.clear();

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

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

Showing Snack Bars

Define the method for displaying snack bars with messages.

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);
}

Class for the Follow Position Button

Define a button that allows the user to recenter the map on their position.

class FollowPositionButton extends StatelessWidget {
  const FollowPositionButton({super.key, required this.onTap});
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onTap,
      child: Container(
        height: 50,
        width: 125,
        margin: const EdgeInsets.only(bottom: 10),
        padding: const EdgeInsets.symmetric(horizontal: 10),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: const BorderRadius.all(Radius.circular(20)),
          boxShadow: [
            BoxShadow(color: Colors.grey.withOpacity(0.5), spreadRadius: 5, blurRadius: 7, offset: const Offset(0, 3)),
          ],
        ),
        child: const Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Icon(Icons.navigation),
            Text('Recenter', style: TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w600)),
          ],
        ),
      ),
    );
  }
}

Flutter Examples

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