Build a simple Android navigation app with Google Maps Platform Navigation SDK

1. Before you begin

This codelab teaches you to create a simple Android app that uses Google Maps Platform Navigation SDK to navigate to a pre-configured destination.

This is what your app will look like when you've finished.

b6c535afde7abd20.png

Prerequisites

  • Knowledge of basic Android App development in Kotlin
  • Some familiarity with basic Google Maps SDK concepts such as maps, locations, coordinates.

What you'll learn

  • How to create a simple Android app that uses Navigation SDK to navigate to a destination.
  • How to integrate the Navigation SDK from the remote Google Maven repository
  • How to manage location permissions and user agreement with the Navigation SDK end user terms
  • How to initialize the SDK
  • How to set a destination and start navigation guidance.

What you'll need

  • The latest stable version of Android Studio installed. This codelab was created using Android Studio Jellyfish. If you're using a different version, the appearance and layout of the interface and components may vary.
  • A Google Account and Project with billing enabled.
  • An Android device in Developer mode with USB debugging enabled, or an Android emulator. Whichever you choose it must meet the minimum requirements for Navigation SDK

2. Get set up

If you do not already have a Google Cloud Platform account and a project with billing enabled, set up your Google Cloud project following the Getting started with Google Maps Platform instructions https://developers.google.com/maps/gmp-get-started

Select your Google Cloud project in the console

In the Cloud Console, click the project drop-down menu and select the project that you want to use for this codelab.

The project selector drop-down menu in the Google Cloud console.

Enable Navigation SDK in your project

Enable the Google Maps Platform APIs and SDKs required for this codelab in the Google Cloud Marketplace.

Navigate to the APIs & Services > Library in Google Cloud Console and search for "Navigation SDK".

You should see one search result.

The API Library screen in the Google Cloud console, showing the Navigation SDK page.

Click the Navigation SDK result to open the Product Details page. Click the Enable button to enable the SDK on your project.

Repeat this process for the Google Maps SDK for Android.

Create an API key

Generate an API key in the Credentials page of Cloud Console. You can follow the steps in step 3 of the quickstart section in Getting started with Google Maps Platform. All requests to Google Maps Platform require an API key.

3. Get the sample project files

This section describes how to set up a basic empty Android Studio project by cloning files from the GitHub repository for this codelab. The Github repo contains before and after versions of the codelab code. The codelab will start with an empty project template and build up to the finished state. You can use the finished project in the repo as a reference if you get stuck.

Clone this Github repo to get the code for this codelab.

git clone https://github.com/googlemaps-samples/codelab-navigation-101-android-kotlin.git

If you don't have git installed, click this button to get the code:

To get you started as quickly as possible, the repo contains some starter code in the Starter folder to help you follow along with this codelab. The starter project provides a basic app UI and build configuration but does not have Navigation SDK added to it. There is also a finished Solution project in case you want to jump ahead or check your progress at any time.

Open the cloned repo in Android Studio

Once you have cloned the repo locally, use Android Studio to open the Starter folder as an existing project.

  1. From the Welcome to Android Studio dialog, click the Open button.
  2. Navigate to the folder where you saved the cloned repo and select the Starter folder inside the top level "codelab-navigation-101-android-kotlin" folder.
  3. Check that the project builds and runs.

Add a virtual device, or connect a hardware device

To connect an Android device to your computer, follow the Android Studio instructions on how to Run apps on a hardware device. Alternatively, you can configure a virtual device using the Android Virtual Device (AVD) Manager. When choosing an emulator, make sure you pick an image that includes the Google APIs.

In Android Studio, click the Run menu option or the play button icon. Choose a device as prompted.

4. Add the Navigation SDK to your app

Add the Navigation SDK library and your API key to your project

To add the Navigation SDK library to your app, you need to modify your app-level build.gradle.kts to fetch the Navigation SDK from the Google Maven repository and configure a version number.

Create a variable in your build config to store the Navigation SDK version number.

Set up a variable in your app-level build.gradle.kts to contain the value of the version of Navigation SDK used in your app, so it's easy to change to the latest version in future.

Check the Navigation SDK release notes for the latest version number.

val navSdkVersion by extra("6.0.0")

You can also modify the values of this and other variables using the dialog found at File > Project Structure > Variables:

668332736b67dc82.png

Add a dependency to the build configuration

Now add the following API dependency to the dependencies block in your app-level build.gradle.kts.The version used will be the value of ${navSdkVersion} which you have just set in your app-level build.gradle.kts:

dependencies {

   // Include the Google Navigation SDK.
   api("com.google.android.libraries.navigation:navigation:${navSdkVersion}")

...

Add your API key

Use the Secrets Gradle plugin to manage the API key

We recommend using the Secrets Gradle plugin to securely manage the API key in your app. The plugin has been added to the initial project template as a dependency in your top level build.gradle.kts file.

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" apply false
    //... other plugin definitions here
}

Open the secrets.properties file in your top-level directory, and then replace YOUR_API_KEY with your API key. Store your key in this file because secrets.properties is excluded from being checked into a version control system.

MAPS_API_KEY=YOUR_API_KEY

For more information on this topic, see Add the API key to your app in the Navigation SDK documentation.

Verify the contents of local.defaults.properties

The empty project also contains a local.defaults.properties file in your top-level directory, the same folder as the secrets.properties file. Open it and observe the following code.

MAPS_API_KEY=DEFAULT_API_KEY

This exists to provide a backup value for the MAPS_API_KEY property in case secrets.properties is not added to the project, so that builds don't fail. There is no need to edit this file. If the secrets.properties definition of MAPS_API_KEY is not found the default value will stop the app running at runtime, with an API key error.

Check that the Android Manifest is using the API key you specified

Open app/src/main/AndroidManifest.xml. You will notice that the MAPS_API_KEY property is used to set the API key for the application:

<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="${MAPS_API_KEY}" />

Open your app-level build.gradle.kts file and find the secrets property.

The propertiesFileName setting of the plugin should be set to secrets.properties, and defaultPropertiesFileName should read local.defaults.properties.

secrets {
    // Optionally specify a different file name containing your secrets.
    // The plugin defaults to "local.properties"
    propertiesFileName = "secrets.properties"

    // A properties file containing default secret values. This file can be
    // checked in version control.
    defaultPropertiesFileName = "local.defaults.properties"
}

Save all files and sync your project with Gradle.

5. Configure app permissions and add a basic UI

Request precise location permission

Navigation SDK depends on GPS signals in order to work, so your app will need to ask the user to grant access to precise location data. Add the permission to access precise location as a child of the <manifest> element in AndroidManifest.xml.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" >
   <uses-permission 
      android:name="android.permission.ACCESS_FINE_LOCATION"
   />
</manifest>

You can read more about Android location permissions in the Request location permissions section of the Android developer documentation.

To run your app on an Android 14 device, request the Foreground Service Location permission by adding the following uses-permission tag in the same location as the precise location access permission:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />

Add a launch activity with a basic UI

When your app runs, it will need code that executes during start up to check whether the user has granted permission to access their location, and handle each possible scenario, requesting permission if it has not been granted yet. To do this, add a basic user interface to your app. This codelab uses the UI that is created when you create a new, empty Views activity in Android Studio. You will adapt this to perform the location permission check before adding code to the activity for the navigation UI.

Open the file MainActivity.kt in the code editor and inspect the code, which shows a basic UI.

Request location access permissions at runtime

Your app will need to trigger the request to access precise location before the Navigation SDK is initialized.

To ensure this check happens when your app starts, add some code to your MainActivity class, in the overridden onCreate() method of your Activity.

The following code checks whether the user has granted fine location permission. If not, it requests the permission. Add this code inside your onCreate() method.

    val permissions =
      if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
        arrayOf(permission.ACCESS_FINE_LOCATION, permission.POST_NOTIFICATIONS)
      } else {
        arrayOf(permission.ACCESS_FINE_LOCATION)
      }

    if (permissions.any { !checkPermissionGranted(it) }) {

      if (permissions.any { shouldShowRequestPermissionRationale(it) }) {
        // Display a dialogue explaining the required permissions.
      }

      val permissionsLauncher =
        registerForActivityResult(
          RequestMultiplePermissions(),
          { permissionResults ->
            if (permissionResults.getOrDefault(permission.ACCESS_FINE_LOCATION, false)) {
              onLocationPermissionGranted()
            } else {
              finish()
            }
          },
        )

      permissionsLauncher.launch(permissions)
    } else {
      android.os.Handler(Looper.getMainLooper()).postDelayed({ onLocationPermissionGranted() }, SPLASH_SCREEN_DELAY_MILLIS)
    }
  }

  private fun checkPermissionGranted(permissionToCheck: String): Boolean =
    ContextCompat.checkSelfPermission(this, permissionToCheck) == PackageManager.PERMISSION_GRANTED

Add a new function to your MainActivity class, called onLocationPermissionGranted, which will handle the outcome when the user grants permission to share their location. In the next steps we will add code here to launch a new navigation activity.

private fun onLocationPermissionGranted() {
   //code to initialize Navigation SDK will go here
}

Build your project. If you have any build errors, find and fix them.

Run your project on a new virtual device. You should see the permission request dialog appear when the app installs and starts.

6. Add a navigation user interface

There are two ways to add a Navigation UI: SupportNavigationFragment or NavigationView.

For simplicity the codelab uses a NavigationView.

Edit the layout

Edit res/layout/activity_main.xml to add a layout for a NavigationView.

  1. Open the file and switch to Code view.
  2. Replace the entire contents of the file with a new layout of a NavigationView inside a RelativeLayout as in the example below. As you will just be adding a navigation view to the app, a simple layout will do.
  3. Give your NavigationView an ID of "@+id/navigation_view".
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
 <com.google.android.libraries.navigation.NavigationView
     android:id="@+id/navigation_view"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
      />
</RelativeLayout>

Set up the Navigation activity

In Android Studio, open the MainActivity.kt file in the editor.

Add some basic setup code to ensure the navigation experience functions correctly in your app. In the MainActivity.kt file, make the following changes:

  1. Declare a variable in your MainActivity class to reference your NavigationView:
private lateinit var navView: NavigationView
  1. Add some code to the onCreate() method to obtain a reference to your NavigationView:
navView = findViewById(R.id.navigation_view)
navView.onCreate(savedInstanceState)
  1. Add some code to the onCreate() method to ensure that the screen stays on during navigation guidance:
// Ensure the screen stays on during nav.
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
  1. Edit the code that calls ViewCompat.setOnApplyWindowInsetsListener to reference the id of your NavigationView.
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.navigation_view)) { v, insets ->
  val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
  v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
  insets
}
  1. Add a showToast() method to the class to show feedback to the user:
private fun showToast(errorMessage: String) {
   Toast.makeText(this@MainActivity, errorMessage, Toast.LENGTH_LONG).show()
}

7. Initialize the Navigation SDK

Now that you've completed the basic Navigation activity setup, you can initialize the Navigation SDK. To do this, add the following code to your MainActivity.kt file:

/** Starts the Navigation API, capturing a reference when ready. */
@SuppressLint("MissingPermission")
private fun initializeNavigationApi() {
   NavigationApi.getNavigator(
       this,
       object : NavigatorListener {
           override fun onNavigatorReady(navigator: Navigator) {
               // store a reference to the Navigator object
               mNavigator = navigator
               // code to start guidance will go here
           }

           override fun onError(@NavigationApi.ErrorCode errorCode: Int) {
               when (errorCode) {
                   NavigationApi.ErrorCode.NOT_AUTHORIZED -> {
                       // Note: If this message is displayed, you may need to check that
                       // your API_KEY is specified correctly in AndroidManifest.xml
                       // and is been enabled to access the Navigation API
                       showToast(
                           "Error loading Navigation API: Your API key is " +
                                   "invalid or not authorized to use Navigation."
                       )
                   }
                   NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED -> {
                       showToast(
                           "Error loading Navigation API: User did not " +
                                   "accept the Navigation Terms of Use."
                       )
                   }
                   else -> showToast("Error loading Navigation API: $errorCode")
               }
           }
       },
   )

}

This code creates a new method called initializeNavigationApi(). This method gets a reference to a Navigator object by calling NavigationApi.getNavigator() and implements a NavigatorListener to handle the callback.

Notice that when the Navigation API is initialized the NavigationListener.onNavigatorReady method will be invoked, with a Navigator object passed as a parameter. The code above will update the mNavigator variable you declared earlier with the initialized Navigator object that is passed to this method.

Finally, add a call to your initializeNavigationApi method from the onLocationPermissionGranted method.

private fun onLocationPermissionGranted() {
   initializeNavigationApi()
}

8. Add listeners for key navigation events

When your users are following guidance, the Navigation SDK will fire events that can notify the app about key state changes enroute, such as when the user reroutes or arrives at their destination. In the MainActivity.kt file, add listeners to handle these events:

  1. Within the MainActivity class, declare two variables to refer to event listener objects:
private var arrivalListener: Navigator.ArrivalListener? = null
private var routeChangedListener: Navigator.RouteChangedListener? = null
  1. Add a registerNavigationListeners() method to set up the listeners when the Navigator is initialized. This method calls Navigator.clearDestinations() to reset the NavigationView when the Arrival event is fired:
/**
* Registers a number of example event listeners that show an on screen message when certain
* navigation events occur (e.g. the driver's route changes or the destination is reached).
*/
private fun registerNavigationListeners() {
   withNavigatorAsync {
       arrivalListener =
           Navigator.ArrivalListener { // Show an onscreen message
               showToast("User has arrived at the destination!")
               mNavigator?.clearDestinations()
           }
       mNavigator?.addArrivalListener(arrivalListener)

       routeChangedListener =
           Navigator.RouteChangedListener { // Show an onscreen message when the route changes
               showToast("onRouteChanged: the driver's route changed")
           }
       mNavigator?.addRouteChangedListener(routeChangedListener)
   }
}
  1. Add a call to registerNavigationListeners() from the onNavigatorReady callback code in the initializeNavigationApi method:
override fun onNavigatorReady(navigator: Navigator) {
   // store a reference to the Navigator object
   mNavigator = navigator

   //listen for events en route
   registerNavigationListeners()


}
  1. Configure the user interface. You can control various aspects of the navigation user interface when guidance is running. One important customisation is the camera position. Add a call to the setTaskRemovedBehaviour method of the navigator object return in onNavigatorReady as follows. This will terminate guidance and notification if the app is swiped away:
// Disables the guidance notifications and shuts down the app and background service
// when the user dismisses/swipes away the app from Android's recent tasks.
navigator.setTaskRemovedBehavior(Navigator.TaskRemovedBehavior.QUIT_SERVICE)
  1. Add a call to GoogleMap.followMyLocation to specify a CameraPerspective. The GoogleMap is accessed through the NavigatorView.getMapAsync() method as follows:
navView.getMapAsync {
   googleMap  ->
   googleMap.followMyLocation(GoogleMap.CameraPerspective.TILTED)
}
  1. To ensure that navigation functions smoothly throughout the app lifecycle, implement the following methods in your MainActivity class:
override fun onSaveInstanceState(savedInstanceState: Bundle) {
   super.onSaveInstanceState(savedInstanceState)

   navView.onSaveInstanceState(savedInstanceState)
}

override fun onTrimMemory(level: Int) {
   super.onTrimMemory(level)
   navView.onTrimMemory(level)
}

override fun onStart() {
   super.onStart()
   navView.onStart()
}

override fun onResume() {
   super.onResume()
   navView.onResume()
}

override fun onPause() {
   navView.onPause()
   super.onPause()
}

override fun onConfigurationChanged(configuration: Configuration) {
   super.onConfigurationChanged(configuration)
   navView.onConfigurationChanged(configuration)
}

override fun onStop() {
   navView.onStop()
   super.onStop()
}

override fun onDestroy() {
   navView.onDestroy()
   withNavigatorAsync {
       // Unregister event listeners to avoid memory leaks.
       if (arrivalListener != null) {
           navigator.removeArrivalListener(arrivalListener)
       }
       if (routeChangedListener != null) {
           navigator.removeRouteChangedListener(routeChangedListener)
       }

       navigator.simulator?.unsetUserLocation()
       navigator.cleanup()
   }
   super.onDestroy()
}

9. Set a destination

You are now ready to set a destination and start navigation guidance. In the MainActivity.kt file, make the following changes:

  1. Add a new navigateToPlace() method that sets the navigation destination and accepts a placeId parameter.
/**
* Requests directions from the user's current location to a specific place (provided by the
* Place ID).
*/
private fun navigateToPlace(placeId: String) {

}
  1. In your navigateToPlace() method, use the Waypoint.builder() method to create a Waypoint from the Place ID passed to the method. Handle the UnsupportedPlaceIdException that this can throw, for situations where the Place ID doesn't resolve to a precise address:
val waypoint: Waypoint? =
// Set a destination by using a Place ID (the recommended method)
try {
   Waypoint.builder().setPlaceIdString(placeId).build()
} catch (e: Waypoint.UnsupportedPlaceIdException) {
   showToast("Place ID was unsupported.")
   return
}
  1. Add the following code to your navigateToPlace() method to set a destination using the Waypoint:
val pendingRoute = mNavigator?.setDestination(waypoint)

// Set an action to perform when a route is determined to the destination
pendingRoute?.setOnResultListener { code ->
   when (code) {
       RouteStatus.OK -> {
           // Code to start guidance will go here
       }

       RouteStatus.ROUTE_CANCELED -> showToast("Route guidance canceled.")
       RouteStatus.NO_ROUTE_FOUND,
       RouteStatus.NETWORK_ERROR ->
           // TODO: Add logic to handle when a route could not be determined
           showToast("Error starting guidance: $code")

       else -> showToast("Error starting guidance: $code")
   }
}

The Navigator object has a setDestinations() method that can take a variety of parameters. The most basic option is to supply a Waypoint. This will default to a travel mode of DRIVING, suitable for 4 wheeler cars. The setDestinations() method returns a ListenableResultFuture object containing a RouteStatus object. The RouteStatus will indicate whether a route was found to the destination and allow you to handle various error states if not.

  1. Make additional configuration changes to improve the navigation user experience:
// Hide the toolbar to maximize the navigation UI
supportActionBar?.hide()

// Enable voice audio guidance (through the device speaker)
mNavigator?.setAudioGuidance(Navigator.AudioGuidance.VOICE_ALERTS_AND_GUIDANCE)


// Simulate vehicle progress along the route (for demo/debug builds)
if (BuildConfig.DEBUG) {
   mNavigator?.simulator?.simulateLocationsAlongExistingRoute(
       SimulationOptions().speedMultiplier(5f)
   )
}

These changes include the following improvements:

  • Hiding the Action Bar to maximize space for the Navigation UI.
  • Enabling audio guidance to speak alerts and navigation instructions.
  • Setting up the Simulator for debugging by specifying a speed multiplier.
  1. Find a Place ID that will act as your destination. Ideally this will be not too far from the user location. Use the Google Maps Platform Place ID Finder utility or obtain a Place ID from a Places API call.

If you are simulating navigation you can set the user location in code or take it from your connected device. The codelab will assume you're simulating a location in London, UK.

  1. Add a companion object to your MainActivity class to store a start location, and a place ID. The Codelab will use a start location in London and the Place ID of Trafalgar Square:
companion object{
   const val TRAFALGAR_SQUARE ="ChIJH-tBOc4EdkgRJ8aJ8P1CUxo" //London, UK
   val startLocation = LatLng(51.345678, -0.1234456)
}
  1. Add a call to your navigateToPlace() method from the onNavigatorReady callback inside the initializeNavigationApi method, and add a branch of logic that will execute in Debug mode, that sets the user location:
// Disables the guidance notifications and shuts down the app and background service
// when the user dismisses/swipes away the app from Android's recent tasks.
navigator.setTaskRemovedBehavior(Navigator.TaskRemovedBehavior.QUIT_SERVICE)

mNavigator = navigator

if (BuildConfig.DEBUG) {
   mNavigator?.simulator?.setUserLocation(MainActivity.startLocation)
}
//listen for events en route
registerNavigationListeners()

navView.getMapAsync {
   googleMap  ->
   googleMap.followMyLocation(GoogleMap.CameraPerspective.TILTED)
}

//navigate to a destination
navigateToPlace(MainActivity.TRAFALGAR_SQUARE)

10. Build and run your code

The first time you run the app, you will need to grant location permissions to the app, and accept the Navigation SDK terms of use.

Note: running the app will call the setDestinations() method which incurs a charge after the first 1000 destinations used. See Usage and billing for more information.

93aa433000a14dfc.png

The Navigation SDK end user terms dialog.

Setting the location

By default the emulated device may have its location set to the Google campus in Mountain View California, unless you have set a location in code, or using the emulator properties dialog.

If so, you may find that the app can't find a route to the Place ID you configured (by default, Sydney Opera House, Sydney, Australia). This will be indicated by a message stating "No route found", displayed by your showToast() method.

The Navigation app map view showing the Google office in Mountain View, California.

Hard coding the start location

To set a different location in code, add the following line in your navigateToPlace() method in MainActivity.kt, before the call to mNavigator.startGuidance():

mNavigator?.simulator?.setUserLocation(startLocation)

Starting the emulator at a default location of your choice

To set a different location in the device emulator, start the emulator if it's not already running, and click the 3 dot menu with the tooltip "Extended Controls". The dialog that opens has a menu option for "Location".

For example, if you are using the Place ID of Sydney Opera House as the destination, choose a location in Sydney, Australia. For example, search for "Bondi beach", select a suggestion and click "Save Location" in the bottom right of the dialog. You can also click "Save Point" to add the location to a saved list for future use.

The Extended Controls dialog in the Android Device Manager, showing a place picker and a map centred on Bondi beach in Australia.

If you set a different Place ID as the destination, choose a location nearby to it so that the simulated route is a realistic one, and not too long for easy debugging.

Restart the app and it should now navigate to the destination.

A screenshot of the Navigation app giving guidance to the destination.

11. Congratulations!

You have completed this codelab. Well done - you have arrived at your destination! Happy coding :-)

55812f33256c0596.png

12. Taking it further

If you want to take your app development further, have a look at the following topics for inspiration.