Building a navigation app

VERSION 1.1.0
PUBLIC PREVIEW

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

This tutorial introduces you to the basic concepts of building a navigation application using the TomTom Navigation SDK for Android. The code snippets presented are simplified and may not cover aspects such as handling configuration changes, error handling, or implementing other best practices, which you should consider when implementing your application. This tutorial is not meant for production use.

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.


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.1.0"
    2implementation("com.tomtom.sdk.location:provider-android:$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 = registerForActivityResult(
2 ActivityResultContracts.RequestMultiplePermissions()
3) { permissions ->
4 if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true &&
5 permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true
6 ) {
7 // showUserLocation()
8 } else {
9 Toast.makeText(
10 this,
11 getString(R.string.location_permission_denied),
12 Toast.LENGTH_SHORT
13 ).show()
14 }
15}
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 AndroidLocationProvider, 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 = AndroidLocationProvider(context = this)
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 = OnLocationUpdateListener { location ->
5 tomTomMap.moveCamera(CameraOptions(location.position, zoom = 8.0))
6 locationProvider.removeOnLocationUpdateListener(onLocationUpdateListener)
7 }
8 locationProvider.addOnLocationUpdateListener(onLocationUpdateListener)
9 tomTomMap.setLocationProvider(locationProvider)
10 val locationMarker = LocationMarkerOptions(type = LocationMarkerOptions.Type.Pointer)
11 tomTomMap.enableLocationMarker(locationMarker)
12}
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 = this, 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 = RoutePlanningOptions(
6 itinerary = itinerary,
7 guidanceOptions = GuidanceOptions(),
8 vehicle = Vehicle.Car()
9 )
10 routePlanner.planRoute(routePlanningOptions, routePlanningCallback)
11}

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 = object : RoutePlanningCallback {
2 override fun onSuccess(result: RoutePlanningResponse) {
3 route = result.routes.first()
4 route?.let { drawRoute(it) }
5 }
6
7 override fun onFailure(failure: RoutingFailure) {
8 Toast.makeText(this@MainActivity, failure.message, Toast.LENGTH_SHORT).show()
9 }
10
11 override fun onRoutePlanned(route: Route) = Unit
12}

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.

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

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 = MapLongClickListener { geoPoint ->
6 clearMap()
7 calculateRouteTo(geoPoint)
8 true
9}
10
11private fun setUpMapListeners() {
12 tomTomMap.addMapLongClickListener(mapLongClickListener)
13}

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 = NavigationTileStore.create(
3 context = this,
4 navigationTileStoreConfig = NavigationTileStoreConfiguration(apiKey)
5 )
6}

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 = OnlineTomTomNavigationFactory.create(
3 Configuration(
4 context = this,
5 navigationTileStore = navigationTileStore,
6 locationProvider = locationProvider,
7 routePlanner = routePlanner
8 )
9 )
10 tomTomNavigation.preferredLanguage = Locale.US
11}

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 = NavigationUiOptions(
4 keepInBackground = true
5 )
6 navigationFragment = NavigationFragment.newInstance(navigationUiOptions)
7 }
8 supportFragmentManager.beginTransaction()
9 .add(R.id.navigation_fragment_container, navigationFragment)
10 .commitNow()
11}

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 = object : NavigationFragment.NavigationListener {
14 override fun onStarted() {
15 tomTomMap.addCameraChangeListener(cameraChangeListener)
16 tomTomMap.cameraTrackingMode = CameraTrackingMode.FollowRouteDirection
17 tomTomMap.enableLocationMarker(LocationMarkerOptions(LocationMarkerOptions.Type.Chevron))
18 setMapMatchedLocationProvider()
19 route?.let { setSimulationLocationProviderToNavigation(it) }
20 setMapNavigationPadding()
21 }
22
23 override fun onStopped() {
24 stopNavigation()
25 }
26}
27
28private val progressUpdatedListener = ProgressUpdatedListener {
29 tomTomMap.routes.first().progress = it.distanceAlongRoute
30}

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 = RouteClickListener {
4 if (!isNavigationRunning()) {
5 route?.let { route ->
6 mapFragment.currentLocationButton.visibilityPolicy = VisibilityPolicy.Invisible
7 startNavigation(route)
8 }
9 }
10}
11
12private fun setUpMapListeners() {
13 tomTomMap.addMapLongClickListener(mapLongClickListener)
14 tomTomMap.addRouteClickListener(routeClickListener)
15}

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 setSimulationLocationProviderToNavigation(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 oldLocationProvider.close()
48 locationProvider.enable()
49}

Step 6: Improving map-matching quality

To match raw location updates to the routes, use the MapMatchedLocationProvider and set it to the TomTomMap.

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

Step 6: 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 7: 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 8: 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: