Building a navigation app

Request access

Navigation SDK for Android is only available upon request. Contact Sales 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.

This tutorial on GitHub

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.

    val version = "1.26.4"
    implementation("com.tomtom.sdk.location:provider-default:$version")
    implementation("com.tomtom.sdk.location:provider-map-matched:$version")
    implementation("com.tomtom.sdk.location:provider-simulation:$version")
    implementation("com.tomtom.sdk.maps:map-display:$version")
    implementation("com.tomtom.sdk.datamanagement:navigation-tile-store:$version")
    implementation("com.tomtom.sdk.navigation:navigation-online:$version")
    implementation("com.tomtom.sdk.navigation:ui:$version")
    implementation("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:

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

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

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

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // initMap()
    // initNavigationTileStore()
    // initLocationProvider()
    // initRouting()
    // initNavigation()
    }

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.

<androidx.fragment.app.FragmentContainerView
android:id="@+id/map_container"
android:layout_width="match_parent"
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:

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

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.

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

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

/**
* Method to verify permissions:
* - [Manifest.permission.ACCESS_FINE_LOCATION]
* - [Manifest.permission.ACCESS_COARSE_LOCATION]
*/
private fun areLocationPermissionsGranted() = ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION,
) == PackageManager.PERMISSION_GRANTED

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

private val locationPermissionRequest =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
) { permissions ->
if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true &&
permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true
) {
// showUserLocation()
} else {
Toast.makeText(
this,
getString(R.string.location_permission_denied),
Toast.LENGTH_SHORT,
).show()
}
}
private fun requestLocationPermissions() {
locationPermissionRequest.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
),
)
}

Define the location_permission_denied string resource in res/values/strings.xml.

<string name="location_permission_denied">Location permissions denied</string>

Combining all the steps may look like this:

/**
* In order to show the user’s location, the application must use the device’s location services,
* which requires the appropriate permissions.
*/
private fun enableUserLocation() {
if (areLocationPermissionsGranted()) {
// showUserLocation()
} else {
requestLocationPermissions()
}
}

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

mapFragment.getMapAsync { map ->
tomTomMap = map
enableUserLocation()
}

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 .

private fun initLocationProvider() {
locationProvider = DefaultLocationProviderFactory.create(context = applicationContext)
locationProvider.enable()
}

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.

private fun showUserLocation() {
locationProvider.enable()
// zoom to current location at city level
onLocationUpdateListener =
OnLocationUpdateListener { location ->
tomTomMap.moveCamera(CameraOptions(location.position, zoom = 8.0))
locationProvider.removeOnLocationUpdateListener(onLocationUpdateListener)
}
locationProvider.addOnLocationUpdateListener(onLocationUpdateListener)
tomTomMap.setLocationProvider(locationProvider)
val locationMarker = LocationMarkerOptions(type = LocationMarkerOptions.Type.Pointer)
tomTomMap.enableLocationMarker(locationMarker)
}

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 :

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

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:

private fun calculateRouteTo(destination: GeoPoint) {
val userLocation =
tomTomMap.currentLocation?.position ?: return
val itinerary = Itinerary(origin = userLocation, destination = destination)
routePlanningOptions =
RoutePlanningOptions(
itinerary = itinerary,
guidanceOptions = GuidanceOptions(),
vehicle = Vehicle.Car(),
)
routePlanner.planRoute(routePlanningOptions, routePlanningCallback)
}

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 :

private val routePlanningCallback =
object : RoutePlanningCallback {
override fun onSuccess(result: RoutePlanningResponse) {
route = result.routes.first()
route?.let { drawRoute(it) }
}
override fun onFailure(failure: RoutingFailure) {
Toast.makeText(this@MainActivity, failure.message, Toast.LENGTH_SHORT).show()
}
}

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.

private fun drawRoute(
route: Route,
color: Int = RouteOptions.DEFAULT_COLOR,
withDepartureMarker: Boolean = true,
withZoom: Boolean = true,
) {
val instructions =
route.legs
.flatMap { routeLeg -> routeLeg.instructions }
.map {
Instruction(
routeOffset = it.routeOffset,
)
}
val routeOptions =
RouteOptions(
geometry = route.geometry,
destinationMarkerVisible = true,
departureMarkerVisible = withDepartureMarker,
instructions = instructions,
routeOffset = route.routePoints.map { it.routeOffset },
color = color,
tag = route.id.toString(),
)
tomTomMap.addRoute(routeOptions)
if (withZoom) {
tomTomMap.zoomToRoutes(ZOOM_TO_ROUTE_PADDING)
}
}
companion object {
private const val ZOOM_TO_ROUTE_PADDING = 100
}

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:

private fun clearMap() {
tomTomMap.clear()
}
private val mapLongClickListener =
MapLongClickListener { geoPoint ->
clearMap()
calculateRouteTo(geoPoint)
true
}
private fun setUpMapListeners() {
tomTomMap.addMapLongClickListener(mapLongClickListener)
}

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

private fun initMap() {
val mapOptions = MapOptions(mapKey = apiKey)
mapFragment = MapFragment.newInstance(mapOptions)
supportFragmentManager.beginTransaction()
.replace(R.id.map_container, mapFragment)
.commit()
mapFragment.getMapAsync { map ->
tomTomMap = map
enableUserLocation()
setUpMapListeners()
}
}

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.

private fun initNavigationTileStore() {
navigationTileStore =
NavigationTileStore.create(
context = applicationContext,
navigationTileStoreConfig = NavigationTileStoreConfiguration(apiKey),
)
}

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:

private fun initNavigation() {
tomTomNavigation =
OnlineTomTomNavigationFactory.create(
Configuration(
context = applicationContext,
navigationTileStore = navigationTileStore,
locationProvider = locationProvider,
routePlanner = routePlanner,
),
)
tomTomNavigation.preferredLanguage = Locale.US
}

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.

<androidx.fragment.app.FragmentContainerView
android:id="@+id/navigation_fragment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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.

private fun initNavigationFragment() {
if (!::navigationFragment.isInitialized) {
val navigationUiOptions =
NavigationUiOptions(
keepInBackground = true,
)
navigationFragment = NavigationFragment.newInstance(navigationUiOptions)
}
supportFragmentManager.beginTransaction()
.add(R.id.navigation_fragment_container, navigationFragment)
.commitNow()
}

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 .

private fun startNavigation(route: Route) {
initNavigationFragment()
navigationFragment.setTomTomNavigation(tomTomNavigation)
val routePlan = RoutePlan(route, routePlanningOptions)
navigationFragment.startNavigation(routePlan)
navigationFragment.addNavigationListener(navigationListener)
tomTomNavigation.addProgressUpdatedListener(progressUpdatedListener)
tomTomNavigation.addRouteAddedListener(routeAddedListener)
tomTomNavigation.addRouteRemovedListener(routeRemovedListener)
tomTomNavigation.addActiveRouteChangedListener(activeRouteChangedListener)
}
private val navigationListener =
object : NavigationFragment.NavigationListener {
override fun onStarted() {
tomTomMap.addCameraChangeListener(cameraChangeListener)
tomTomMap.cameraTrackingMode = CameraTrackingMode.FollowRouteDirection
tomTomMap.enableLocationMarker(LocationMarkerOptions(LocationMarkerOptions.Type.Chevron))
route?.let {
setSimulationLocationProvider(it)
} ?: setMapMatchedLocationProvider()
setMapNavigationPadding()
}
override fun onStopped() {
stopNavigation()
}
}
private val progressUpdatedListener =
ProgressUpdatedListener {
tomTomMap.routes.first().progress = it.distanceAlongRoute
}

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:

private fun isNavigationRunning(): Boolean = tomTomNavigation.navigationState != NavigationState.Idle
private val routeClickListener =
RouteClickListener {
if (!isNavigationRunning()) {
route?.let { route ->
mapFragment.currentLocationButton.visibilityPolicy = VisibilityPolicy.Invisible
startNavigation(route)
}
}
}
private fun setUpMapListeners() {
tomTomMap.addMapLongClickListener(mapLongClickListener)
tomTomMap.addRouteClickListener(routeClickListener)
}

Step 5: Updating navigation states

Respond to navigation state updates by using the implemented listeners:

private val routeAddedListener by lazy {
RouteAddedListener { route, _, routeAddedReason ->
if (routeAddedReason !is RouteAddedReason.NavigationStarted) {
drawRoute(
route = route,
color = Color.GRAY,
withDepartureMarker = false,
withZoom = false,
)
}
}
}
private val routeRemovedListener by lazy {
RouteRemovedListener { route, _ ->
tomTomMap.routes.find { it.tag == route.id.toString() }?.remove()
}
}
private val activeRouteChangedListener by lazy {
ActiveRouteChangedListener { route ->
tomTomMap.routes.forEach {
if (it.tag == route.id.toString()) {
it.color = RouteOptions.DEFAULT_COLOR
} else {
it.color = Color.GRAY
}
}
}
}
private val cameraChangeListener by lazy {
CameraChangeListener {
val cameraTrackingMode = tomTomMap.cameraTrackingMode
if (cameraTrackingMode == CameraTrackingMode.FollowRouteDirection) {
navigationFragment.navigationView.showSpeedView()
} else {
navigationFragment.navigationView.hideSpeedView()
}
}
}
private fun setSimulationLocationProvider(route: Route) {
val routeGeoLocations = route.geometry.map { GeoLocation(it) }
val simulationStrategy = InterpolationStrategy(routeGeoLocations)
val oldLocationProvider = tomTomNavigation.locationProvider
locationProvider = SimulationLocationProvider.create(strategy = simulationStrategy)
tomTomNavigation.locationProvider = locationProvider
tomTomMap.setLocationProvider(locationProvider)
oldLocationProvider.close()
locationProvider.enable()
}

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.

private fun setMapMatchedLocationProvider() {
val mapMatchedLocationProvider = MapMatchedLocationProviderFactory.create(tomTomNavigation)
tomTomMap.setLocationProvider(mapMatchedLocationProvider)
mapMatchedLocationProvider.enable()
}

Step 7: Adjusting map display

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

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

Step 8: Stopping navigation

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

private fun stopNavigation() {
navigationFragment.stopNavigation()
mapFragment.currentLocationButton.visibilityPolicy =
VisibilityPolicy.InvisibleWhenRecentered
tomTomMap.removeCameraChangeListener(cameraChangeListener)
tomTomMap.cameraTrackingMode = CameraTrackingMode.None
tomTomMap.enableLocationMarker(LocationMarkerOptions(LocationMarkerOptions.Type.Pointer))
tomTomMap.setPadding(Padding(0, 0, 0, 0))
navigationFragment.removeNavigationListener(navigationListener)
tomTomNavigation.removeProgressUpdatedListener(progressUpdatedListener)
tomTomNavigation.removeRouteAddedListener(routeAddedListener)
tomTomNavigation.removeRouteRemovedListener(routeRemovedListener)
tomTomNavigation.removeActiveRouteChangedListener(activeRouteChangedListener)
clearMap()
initLocationProvider()
enableUserLocation()
}

Step 9: Clean Up

Remember to release resources when navigation is no longer needed:

override fun onDestroy() {
tomTomMap.setLocationProvider(null)
if (::navigationFragment.isInitialized) {
supportFragmentManager.beginTransaction().remove(navigationFragment)
.commitNowAllowingStateLoss()
}
tomTomNavigation.close()
routePlanner.close()
locationProvider.close()
navigationTileStore.close()
super.onDestroy()
}

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

This tutorial on GitHub

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: