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.
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.
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.
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.
- From the Welcome to Android Studio dialog, click the Open button.
- 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. - 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:
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.
- Open the file and switch to Code view.
- Replace the entire contents of the file with a new layout of a
NavigationView
inside aRelativeLayout
as in the example below. As you will just be adding a navigation view to the app, a simple layout will do. - 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:
- Declare a variable in your
MainActivity
class to reference yourNavigationView
:
private lateinit var navView: NavigationView
- Add some code to the
onCreate()
method to obtain a reference to yourNavigationView
:
navView = findViewById(R.id.navigation_view)
navView.onCreate(savedInstanceState)
- 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)
- Edit the code that calls
ViewCompat.setOnApplyWindowInsetsListener
to reference the id of yourNavigationView
.
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
}
- 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:
- 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
- Add a
registerNavigationListeners()
method to set up the listeners when the Navigator is initialized. This method callsNavigator.clearDestinations()
to reset theNavigationView
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)
}
}
- Add a call to
registerNavigationListeners()
from theonNavigatorReady
callback code in theinitializeNavigationApi
method:
override fun onNavigatorReady(navigator: Navigator) {
// store a reference to the Navigator object
mNavigator = navigator
//listen for events en route
registerNavigationListeners()
}
- 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 thenavigator
object return inonNavigatorReady
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)
- Add a call to
GoogleMap.followMyLocation
to specify aCameraPerspective
. TheGoogleMap
is accessed through theNavigatorView.getMapAsync()
method as follows:
navView.getMapAsync {
googleMap ->
googleMap.followMyLocation(GoogleMap.CameraPerspective.TILTED)
}
- 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:
- Add a new
navigateToPlace()
method that sets the navigation destination and accepts aplaceId
parameter.
/**
* Requests directions from the user's current location to a specific place (provided by the
* Place ID).
*/
private fun navigateToPlace(placeId: String) {
}
- In your
navigateToPlace()
method, use theWaypoint.builder()
method to create aWaypoint
from the Place ID passed to the method. Handle theUnsupportedPlaceIdException
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
}
- 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.
- 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.
- 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.
- 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)
}
- Add a call to your
navigateToPlace()
method from theonNavigatorReady
callback inside theinitializeNavigationApi
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.
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.
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.
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.
11. Congratulations!
You have completed this codelab. Well done - you have arrived at your destination! Happy coding :-)
12. Taking it further
If you want to take your app development further, have a look at the following topics for inspiration.
- Listen for more navigation events. Add code to display a message if the driver re-routes, or when they arrive.
- Customize the navigation interface.
- If you fancy a bigger challenge, see if you can add a Places API Place Picker control to allow the user to set the destination. Hint: the Navigation SDK demo apps on Github have a sample implementation.
- Prevent potential issues in calling the Navigator and GoogleMap objects asynchronously by adopting the approach used in the Navigation SDK demo apps on Github. In more complex app scenarios, these objects may not have finished initializing when your code runs. Tip: you can add the InitializedNavScope class at the end of your MainActivity.kt file for a very rapid implementation.