Building a navigation app

VERSION 1.20.0
PUBLIC PREVIEW

Navigation SDK for Android is only available upon request. Contact us to get started.

This tutorial guides you through building a navigation application using the TomTom Navigation SDK for Android. You’ll learn to display a map, show the user’s location, calculate and display routes, and enable turn-by-turn navigation using built-in UI components.

Note that this tutorial uses simplified code snippets and doesn’t address configuration changes, error handling, or other best practices, which you’ll want to include in your application for production readiness.


Project setup

This tutorial is validated with the targetSDK set to Android API level 33. If you want to use a different API level, you may need to adjust the code accordingly.

  1. Configure the project as described in the Project setup guide.

  2. Add the following dependencies to your app’s build.gradle.kts file and synchronize the project.

    1val version = "1.20.0"
    2implementation("com.tomtom.sdk.location:provider-default:$version")
    3implementation("com.tomtom.sdk.location:provider-map-matched:$version")
    4implementation("com.tomtom.sdk.location:provider-simulation:$version")
    5implementation("com.tomtom.sdk.maps:map-display:$version")
    6implementation("com.tomtom.sdk.datamanagement:navigation-tile-store:$version")
    7implementation("com.tomtom.sdk.navigation:navigation-online:$version")
    8implementation("com.tomtom.sdk.navigation:ui:$version")
    9implementation("com.tomtom.sdk.routing:route-planner-online:$version")
  3. This tutorial requires supportFragmentManager, so ensure that your activity extends AppCompatActivity.

    class MainActivity : AppCompatActivity() {
  4. Retrieve the TomTom API key from the BuildConfig field and store in a variable:

    1// Note: BuildConfig.TOMTOM_API_KEY could potentially be null, ensure it's not null before use.
    2// TomTom APIs will not work without a valid API key. Navigation SDK is only avaialble upon request.
    3// Use the API key provided by TomTom to start using the SDK.
    4private val apiKey = BuildConfig.TOMTOM_API_KEY
  5. Make sure to import the following classes, which will be used throughout this tutorial:

    Imports
    1import android.os.Bundle
    2import androidx.appcompat.app.AppCompatActivity
    3import com.tomtom.sdk.datamanagement.navigationtile.NavigationTileStore
    4import com.tomtom.sdk.location.LocationProvider
    5import com.tomtom.sdk.location.OnLocationUpdateListener
    6import com.tomtom.sdk.map.display.TomTomMap
    7import com.tomtom.sdk.map.display.ui.MapFragment
    8import com.tomtom.sdk.navigation.TomTomNavigation
    9import com.tomtom.sdk.navigation.ui.NavigationFragment
    10import com.tomtom.sdk.routing.RoutePlanner
    11import com.tomtom.sdk.routing.options.RoutePlanningOptions
    12import com.tomtom.sdk.routing.route.Route
  6. Declare the members of the MainActivity class for later use in the guide:

    1private lateinit var mapFragment: MapFragment
    2private lateinit var tomTomMap: TomTomMap
    3private lateinit var navigationTileStore: NavigationTileStore
    4private lateinit var locationProvider: LocationProvider
    5private lateinit var onLocationUpdateListener: OnLocationUpdateListener
    6private lateinit var routePlanner: RoutePlanner
    7private var route: Route? = null
    8private lateinit var routePlanningOptions: RoutePlanningOptions
    9private lateinit var tomTomNavigation: TomTomNavigation
    10private lateinit var navigationFragment: NavigationFragment
  7. In the onCreate function, the following SDK components will be enabled through the following steps:

    1 override fun onCreate(savedInstanceState: Bundle?) {
    2 super.onCreate(savedInstanceState)
    3 setContentView(R.layout.activity_main)
    4
    5// initMap()
    6// initNavigationTileStore()
    7// initLocationProvider()
    8// initRouting()
    9// initNavigation()
    10 }

With the dependencies and API key set up, you can proceed to the next step and start displaying the map.

Displaying a map

Step 1: Creating map UI

To host the map component, configure the FragmentContainerView in your layout XML file, which can be either the activity layout or a fragment layout. The map content will be displayed within this designated container.

1<androidx.fragment.app.FragmentContainerView
2 android:id="@+id/map_container"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent" />

Now, you can initialize the MapFragment and add it to the FragmentContainerView programmatically.

Step 2: Displaying the map

To initialize the map, you’ll need the following components:

Here’s the code snippet for the map initialization:

1private fun initMap() {
2 val mapOptions = MapOptions(mapKey = apiKey)
3 mapFragment = MapFragment.newInstance(mapOptions)
4 supportFragmentManager.beginTransaction()
5 .replace(R.id.map_container, mapFragment)
6 .commit()
7 mapFragment.getMapAsync { map ->
8 tomTomMap = map
9 // Place the code here to enable/show user location and setup map listeners.
10 // as explained in the following sections.
11 }
12}

Once you have the TomTomMap object, you can perform various actions such as adding markers or drawing routes. You can learn more about map configuration in the Map configuration guide.

Build and run your application. Upon execution, the application will display a globe map.

center

Showing user location

In this section, you learn to display the user’s location on the map and adjust the camera to focus on the user’s position. To enable this functionality, use the following steps:

Step 1: Enabling location services

Before proceeding with code changes, make sure to enable location services for your application. You can declare the below permissions in the Manifest file of your application. Refer to the Android documentation for more details.

1<uses-permission android:name="android.permission.INTERNET"/>
2<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
3<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Firstly, you must verify if location permissions have been granted at runtime:

1/**
2 * Method to verify permissions:
3 * - [Manifest.permission.ACCESS_FINE_LOCATION]
4 * - [Manifest.permission.ACCESS_COARSE_LOCATION]
5 */
6private fun areLocationPermissionsGranted() = ContextCompat.checkSelfPermission(
7 this,
8 Manifest.permission.ACCESS_FINE_LOCATION,
9) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
10 this,
11 Manifest.permission.ACCESS_COARSE_LOCATION,
12) == PackageManager.PERMISSION_GRANTED

If the permissions are not granted, proceed to request them:

1private val locationPermissionRequest =
2 registerForActivityResult(
3 ActivityResultContracts.RequestMultiplePermissions(),
4 ) { permissions ->
5 if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true &&
6 permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true
7 ) {
8 // showUserLocation()
9 } else {
10 Toast.makeText(
11 this,
12 getString(R.string.location_permission_denied),
13 Toast.LENGTH_SHORT,
14 ).show()
15 }
16 }
1private fun requestLocationPermissions() {
2 locationPermissionRequest.launch(
3 arrayOf(
4 Manifest.permission.ACCESS_FINE_LOCATION,
5 Manifest.permission.ACCESS_COARSE_LOCATION,
6 ),
7 )
8}

Combining all the steps may look like this:

1/**
2 * In order to show the user’s location, the application must use the device’s location services,
3 * which requires the appropriate permissions.
4 */
5private fun enableUserLocation() {
6 if (areLocationPermissionsGranted()) {
7 // showUserLocation()
8 } else {
9 requestLocationPermissions()
10 }
11}

You can call this method right after the map is ready to use:

1mapFragment.getMapAsync { map ->
2 tomTomMap = map
3 enableUserLocation()
4}

Step 2: Obtaining location updates

The SDK uses the LocationProvider interface for location updates. In this use case, we use the default LocationProvider, which relies on Android’s system location services.

To retrieve location updates, you need to invoke the LocationProvider.enable() method. For more details, consult the Location quickstart guide.

1private fun initLocationProvider() {
2 locationProvider = DefaultLocationProviderFactory.create(context = applicationContext)
3 locationProvider.enable()
4}

Step 3: Displaying the user’s location on the map

The LocationProvider only reports location changes. It does not interact internally with the map or navigation. Therefore, to show the user’s location on the map, you must set the LocationProvider to the TomTomMap. You also have to manually enable the location indicator. It can be configured using the LocationMarkerOptions class. Read more about user location on the map in the Showing User Location document.

1private fun showUserLocation() {
2 locationProvider.enable()
3 // zoom to current location at city level
4 onLocationUpdateListener =
5 OnLocationUpdateListener { location ->
6 tomTomMap.moveCamera(CameraOptions(location.position, zoom = 8.0))
7 locationProvider.removeOnLocationUpdateListener(onLocationUpdateListener)
8 }
9 locationProvider.addOnLocationUpdateListener(onLocationUpdateListener)
10 tomTomMap.setLocationProvider(locationProvider)
11 val locationMarker = LocationMarkerOptions(type = LocationMarkerOptions.Type.Pointer)
12 tomTomMap.enableLocationMarker(locationMarker)
13}
center

Generating a route

This section describes how to calculate a route between two locations and display it on the map. You can find more details on adjusting planning criteria and accommodating vehicle profiles in the routing documentation.

Step 1: Setting up the route planner

The route planner is your access point to the routing service through the Routing API. Initialize it with the default TomTom implementation based on the Routing API:

1private fun initRouting() {
2 routePlanner = OnlineRoutePlanner.create(context = applicationContext, apiKey = apiKey)
3}

Step 2: Calculating a route

You can learn to plan a route using the RoutePlanningOptions. This step specifies the origin, destination, guidance options, and vehicle profile. The calculated route is passed to a callback for further handling:

1private fun calculateRouteTo(destination: GeoPoint) {
2 val userLocation =
3 tomTomMap.currentLocation?.position ?: return
4 val itinerary = Itinerary(origin = userLocation, destination = destination)
5 routePlanningOptions =
6 RoutePlanningOptions(
7 itinerary = itinerary,
8 guidanceOptions = GuidanceOptions(),
9 vehicle = Vehicle.Car(),
10 )
11 routePlanner.planRoute(routePlanningOptions, routePlanningCallback)
12}

Step 3: Displaying the route on the map

Now that you successfully planned a route, you can access the first route by processing the results of the RoutePlanningResponse:

1private val routePlanningCallback =
2 object : RoutePlanningCallback {
3 override fun onSuccess(result: RoutePlanningResponse) {
4 route = result.routes.first()
5 route?.let { drawRoute(it) }
6 }
7
8 override fun onFailure(failure: RoutingFailure) {
9 Toast.makeText(this@MainActivity, failure.message, Toast.LENGTH_SHORT).show()
10 }
11
12 override fun onRoutePlanned(route: Route) = Unit
13 }

Displaying the route involves constructing a series of instructions from the route’s legs and then using the RouteOptions to draw the route geometry and maneuver instructions. To keep the reference of the planned route to the route drawn on the map, the RouteOptions.tag property is used. It also manages zoom levels for the route overview.

It is crucial to pass the correct route offsets for the route. The property RouteOptions.routeOffset determines where instructions and route progress are rendered. Although this field is optional, we highly recommend passing the route offset directly to the RouteOptions. Ensure that the route offsets are synchronized with the route instructions offset to avoid misplaced instructions or inaccurate progress. If you use products like TomTom Routing API, we recommend using route offsets directly from the API.

1private fun drawRoute(
2 route: Route,
3 color: Int = RouteOptions.DEFAULT_COLOR,
4 withDepartureMarker: Boolean = true,
5 withZoom: Boolean = true,
6) {
7 val instructions =
8 route.legs
9 .flatMap { routeLeg -> routeLeg.instructions }
10 .map {
11 Instruction(
12 routeOffset = it.routeOffset,
13 )
14 }
15 val routeOptions =
16 RouteOptions(
17 geometry = route.geometry,
18 destinationMarkerVisible = true,
19 departureMarkerVisible = withDepartureMarker,
20 instructions = instructions,
21 routeOffset = route.routePoints.map { it.routeOffset },
22 color = color,
23 tag = route.id.toString(),
24 )
25 tomTomMap.addRoute(routeOptions)
26 if (withZoom) {
27 tomTomMap.zoomToRoutes(ZOOM_TO_ROUTE_PADDING)
28 }
29}
30companion object {
31 private const val ZOOM_TO_ROUTE_PADDING = 100
32}

Step 4: User interaction

Learn how to interact with the map to set the destination and initiate route calculation. This step clears any existing map elements, calculates the route to the selected location, and sets up a MapLongClickListener to handle destination selection:

1private fun clearMap() {
2 tomTomMap.clear()
3}
4
5private val mapLongClickListener =
6 MapLongClickListener { geoPoint ->
7 clearMap()
8 calculateRouteTo(geoPoint)
9 true
10 }
11
12private fun setUpMapListeners() {
13 tomTomMap.addMapLongClickListener(mapLongClickListener)
14}

Finally, ensure these actions are executed when the map instance is ready to be used:

1private fun initMap() {
2 val mapOptions = MapOptions(mapKey = apiKey)
3 mapFragment = MapFragment.newInstance(mapOptions)
4 supportFragmentManager.beginTransaction()
5 .replace(R.id.map_container, mapFragment)
6 .commit()
7 mapFragment.getMapAsync { map ->
8 tomTomMap = map
9 enableUserLocation()
10 setUpMapListeners()
11 }
12}
Route instruction

Setting up turn-by-turn navigation

Step 1: Initializing navigation

First create the NavigationTileStore using your TomTom API key. This is required, as the navigation component relies on navigation tile data.

1private fun initNavigationTileStore() {
2 navigationTileStore =
3 NavigationTileStore.create(
4 context = applicationContext,
5 navigationTileStoreConfig = NavigationTileStoreConfiguration(apiKey),
6 )
7}

Use the built-in navigation UI and location simulation engine to integrate turn-by-turn navigation into your application. The built-in UI displays essential information such as upcoming maneuvers, remaining distance, ETA, current speed, and speed limits. You can use default UI components or customize your own.

To start, create a TomTomNavigation object:

1private fun initNavigation() {
2 tomTomNavigation =
3 OnlineTomTomNavigationFactory.create(
4 Configuration(
5 context = applicationContext,
6 navigationTileStore = navigationTileStore,
7 locationProvider = locationProvider,
8 routePlanner = routePlanner,
9 ),
10 )
11 tomTomNavigation.preferredLanguage = Locale.US
12}

Step 2: Creating navigation UI

To host a navigation component, add another FragmentContainerView to your layout XML file. If you want to position the navigation fragment at the bottom of the map container (assuming you have a map container with the ID "map_container"), you can use the app:layout_constraintBottom_toBottomOf attribute and set its value to @+id/map_container. This aligns the navigation fragment’s bottom edge with map container’s bottom edge.

1<androidx.fragment.app.FragmentContainerView
2 android:id="@+id/navigation_fragment_container"
3 android:layout_width="match_parent"
4 android:layout_height="wrap_content"
5 app:layout_constraintBottom_toBottomOf="@+id/map_container" />

Now, you can initialize the NavigationFragment and add it to the FragmentContainerView programmatically. Note that you must manually handle the disposal of the TomTomNavigation instance.

1private fun initNavigationFragment() {
2 if (!::navigationFragment.isInitialized) {
3 val navigationUiOptions =
4 NavigationUiOptions(
5 keepInBackground = true,
6 )
7 navigationFragment = NavigationFragment.newInstance(navigationUiOptions)
8 }
9 supportFragmentManager.beginTransaction()
10 .add(R.id.navigation_fragment_container, navigationFragment)
11 .commitNow()
12}

Step 3: Starting navigation

Initialize the navigation process by passing a Route object and RoutePlanningOptions. Before using it, ensure that you have associated the TomTomNavigation object with the NavigationFragment.

1private fun startNavigation(route: Route) {
2 initNavigationFragment()
3 navigationFragment.setTomTomNavigation(tomTomNavigation)
4 val routePlan = RoutePlan(route, routePlanningOptions)
5 navigationFragment.startNavigation(routePlan)
6 navigationFragment.addNavigationListener(navigationListener)
7 tomTomNavigation.addProgressUpdatedListener(progressUpdatedListener)
8 tomTomNavigation.addRouteAddedListener(routeAddedListener)
9 tomTomNavigation.addRouteRemovedListener(routeRemovedListener)
10 tomTomNavigation.addActiveRouteChangedListener(activeRouteChangedListener)
11}
12
13private val navigationListener =
14 object : NavigationFragment.NavigationListener {
15 override fun onStarted() {
16 tomTomMap.addCameraChangeListener(cameraChangeListener)
17 tomTomMap.cameraTrackingMode = CameraTrackingMode.FollowRouteDirection
18 tomTomMap.enableLocationMarker(LocationMarkerOptions(LocationMarkerOptions.Type.Chevron))
19 route?.let {
20 setSimulationLocationProvider(it)
21 } ?: setMapMatchedLocationProvider()
22 setMapNavigationPadding()
23 }
24
25 override fun onStopped() {
26 stopNavigation()
27 }
28 }
29
30private val progressUpdatedListener =
31 ProgressUpdatedListener {
32 tomTomMap.routes.first().progress = it.distanceAlongRoute
33 }

Step 4: User interaction

Users can trigger navigation by tapping on a route, if navigation is not already running. Add a RouteClickListener to the map view:

1private fun isNavigationRunning(): Boolean = tomTomNavigation.navigationState != NavigationState.Idle
2
3private val routeClickListener =
4 RouteClickListener {
5 if (!isNavigationRunning()) {
6 route?.let { route ->
7 mapFragment.currentLocationButton.visibilityPolicy = VisibilityPolicy.Invisible
8 startNavigation(route)
9 }
10 }
11 }
12
13private fun setUpMapListeners() {
14 tomTomMap.addMapLongClickListener(mapLongClickListener)
15 tomTomMap.addRouteClickListener(routeClickListener)
16}

Step 5: Updating navigation states

Respond to navigation state updates by using the implemented listeners:

1private val routeAddedListener by lazy {
2 RouteAddedListener { route, _, routeAddedReason ->
3 if (routeAddedReason !is RouteAddedReason.NavigationStarted) {
4 drawRoute(
5 route = route,
6 color = Color.GRAY,
7 withDepartureMarker = false,
8 withZoom = false,
9 )
10 }
11 }
12}
13
14private val routeRemovedListener by lazy {
15 RouteRemovedListener { route, _ ->
16 tomTomMap.routes.find { it.tag == route.id.toString() }?.remove()
17 }
18}
19
20private val activeRouteChangedListener by lazy {
21 ActiveRouteChangedListener { route ->
22 tomTomMap.routes.forEach {
23 if (it.tag == route.id.toString()) {
24 it.color = RouteOptions.DEFAULT_COLOR
25 } else {
26 it.color = Color.GRAY
27 }
28 }
29 }
30}
31private val cameraChangeListener by lazy {
32 CameraChangeListener {
33 val cameraTrackingMode = tomTomMap.cameraTrackingMode
34 if (cameraTrackingMode == CameraTrackingMode.FollowRouteDirection) {
35 navigationFragment.navigationView.showSpeedView()
36 } else {
37 navigationFragment.navigationView.hideSpeedView()
38 }
39 }
40}
41private fun setSimulationLocationProvider(route: Route) {
42 val routeGeoLocations = route.geometry.map { GeoLocation(it) }
43 val simulationStrategy = InterpolationStrategy(routeGeoLocations)
44 val oldLocationProvider = tomTomNavigation.locationProvider
45 locationProvider = SimulationLocationProvider.create(strategy = simulationStrategy)
46 tomTomNavigation.locationProvider = locationProvider
47 tomTomMap.setLocationProvider(locationProvider)
48 oldLocationProvider.close()
49 locationProvider.enable()
50}

Step 6: Improving map-matching quality

To match raw location updates to the routes, use the map-matched LocationProvider and set it to the TomTomMap.

Note that this does not apply to route simulation cases. In simulation mode, location data for both the map and navigation should be sourced exclusively from the simulation location provider.

1private fun setMapMatchedLocationProvider() {
2 val mapMatchedLocationProvider = MapMatchedLocationProviderFactory.create(tomTomNavigation)
3 tomTomMap.setLocationProvider(mapMatchedLocationProvider)
4 mapMatchedLocationProvider.enable()
5}

Step 7: Adjusting map display

You can set padding on the map to ensure proper visibility of the navigation UI elements:

1<dimen name="map_padding_bottom">263.0dp</dimen>
2
3private fun setMapNavigationPadding() {
4 val paddingBottom = resources.getDimensionPixelOffset(R.dimen.map_padding_bottom)
5 val padding = Padding(0, 0, 0, paddingBottom)
6 tomTomMap.setPadding(padding)
7}

Step 8: Stopping navigation

Properly handle the end of the navigation process, including UI and resource clean-up:

1private fun stopNavigation() {
2 navigationFragment.stopNavigation()
3 mapFragment.currentLocationButton.visibilityPolicy =
4 VisibilityPolicy.InvisibleWhenRecentered
5 tomTomMap.removeCameraChangeListener(cameraChangeListener)
6 tomTomMap.cameraTrackingMode = CameraTrackingMode.None
7 tomTomMap.enableLocationMarker(LocationMarkerOptions(LocationMarkerOptions.Type.Pointer))
8 tomTomMap.setPadding(Padding(0, 0, 0, 0))
9 navigationFragment.removeNavigationListener(navigationListener)
10 tomTomNavigation.removeProgressUpdatedListener(progressUpdatedListener)
11 tomTomNavigation.removeRouteAddedListener(routeAddedListener)
12 tomTomNavigation.removeRouteRemovedListener(routeRemovedListener)
13 tomTomNavigation.removeActiveRouteChangedListener(activeRouteChangedListener)
14 clearMap()
15 initLocationProvider()
16 enableUserLocation()
17}

Step 9: Clean Up

Remember to release resources when navigation is no longer needed:

1override fun onDestroy() {
2 tomTomMap.setLocationProvider(null)
3 if (::navigationFragment.isInitialized) {
4 supportFragmentManager.beginTransaction().remove(navigationFragment)
5 .commitNowAllowingStateLoss()
6 }
7 tomTomNavigation.close()
8 locationProvider.close()
9 navigationTileStore.close()
10 super.onDestroy()
11}

Run the application. You should see a globe showing the user’s location. Set the destination point with a long press. If you want to start navigation along the drawn route, tap it. Navigation should start with a guidance panel and voice instructions.

center

Next steps

The TomTom Navigation SDK allows you to customize the appearance of the map and its overlays, use your own Navigation UI components, or provide a custom implementation of certain navigation behaviors. See the following guides for more information: