Learn how to find a route and directions with the route service.

Routing is the process of finding the path from an origin to a destination in a street network. You can use the Routing service to find routes, get driving directions, calculate drive times, and solve complicated, multiple vehicle routing problems. To create a route, you typically define a set of stops (origin and one or more destinations) and use the service to find a route with directions. You can also use a number of additional parameters such as barriers and mode of travel to refine the results.
In this tutorial, you define an origin and destination by clicking on the map. These values are used to get a route and directions from the route service. The directions are also displayed on the map.
Prerequisites
Before starting this tutorial, you need the following:
-
An ArcGIS Location Platform or ArcGIS Online account.
-
A development and deployment environment that meets the system requirements.
-
An IDE for Android development in Kotlin.
Set up authentication
To access the secure ArcGIS location services used in this tutorial, you must implement API key authentication or user authentication using an ArcGIS Location Platform or an ArcGIS Online account.
You can implement API key authentication or user authentication in this tutorial. Compare the differences below:
API key authentication
- Users are not required to sign in.
- Requires creating an API key credential with the correct privileges.
- API keys are long-lived access tokens.
- Service usage is billed to the API key owner/developer.
- Simplest authentication method to implement.
- Recommended approach for new ArcGIS developers.
Learn more in API key authentication.
User authentication
- Users are required to sign in with an ArcGIS account.
- User accounts must have privilege to access the ArcGIS services used in application.
- Requires creating OAuth credentials.
- Application uses a redirect URL and client ID.
- Service usage is billed to the organization of the user signed into the application.
Learn more in User authentication.
Create a new API key access token with privileges to access the secure resources used in this tutorial.
-
Complete the Create an API key tutorial and create an API key with the following privilege(s):
- Privileges
- Location services > Basemaps
- Location services > Routing
- Privileges
-
Copy and paste the API key access token into a safe location. It will be used in a later step.
Develop or download
You have two options for completing this tutorial:
Option 1: Develop the code
Open an Android Studio project
-
Open the project you created by completing the Display a map tutorial.
-
Continue with the following instructions to find a route and directions with the ArcGIS routing service.
-
Modify the old project for use in this new tutorial.
-
On your file system, delete the .idea folder, if present, at the top level of your project.
-
In the Android view, open app > res > values > strings.xml.
In the
<string name="app
element, change the text content to Find a route and directions._name" > strings.xmlUse dark colors for code blocks <resources> <string name="app_name">Find a route and directions</string> </resources>
-
In the Android view, open Gradle Scripts > settings.gradle.kts.
Change the value of
root
to "Find a route and directions".Project.name settings.gradle.ktsUse dark colors for code blocks dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url = uri("https://esri.jfrog.io/artifactory/arcgis") } } } rootProject.name = "Find a route and directions" include(":app")
-
Click File > Sync Project with Gradle files. Android Studio will recognize your changes and create a new .idea folder.
-
Set developer credentials
To allow your app users to access ArcGIS location services, use the developer credentials that you created in the Set up authentication step to authenticate requests for resources.
-
In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity.
-
In the
on
lifecycle method of theCreate() Main
class, set theActivity ArcGISEnvironment.apiKey
property by callingApiKey.create()
. Pass in your API key access token as a string and don't forget the double quotes. Do this before theset
block.Content MainActivity.ktUse dark colors for code blocks override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN")
Best Practice: The access token is stored directly in the code as a convenience for this tutorial. Do not store credentials directly in source code in a production environment.
Add import statements and some Compose variables
-
In the Android view, open app > kotlin+java > com.example.app > screens > MainScreen.kt. Replace the import statements with the imports needed for this tutorial.
MainScreen.ktUse dark colors for code blocks @file:OptIn(ExperimentalMaterial3Api::class) package com.example.app.screens import android.content.Context import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.arcgismaps.Color import com.arcgismaps.geometry.Point import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.BasemapStyle import com.arcgismaps.mapping.Viewpoint import com.arcgismaps.mapping.symbology.SimpleLineSymbol import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.mapping.view.SingleTapConfirmedEvent import com.arcgismaps.tasks.networkanalysis.DirectionManeuver import com.arcgismaps.tasks.networkanalysis.RouteParameters import com.arcgismaps.tasks.networkanalysis.RouteResult import com.arcgismaps.tasks.networkanalysis.RouteTask import com.arcgismaps.tasks.networkanalysis.Stop import com.arcgismaps.toolkit.geoviewcompose.MapView import com.example.app.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch
-
In the
Main
composable, create variables that will be passed to various functions in theScreen Main
file.Screen.kt The
remember()
function will remember the value across recomposition. Theremember
obtains a composition-aware scope to launch a coroutine outside a composable.Coroutine Scope() MainScreen.ktUse dark colors for code blocks @Composable fun MainScreen() { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val directionList = remember { mutableStateListOf("Tap to add two points to the map to find a route between them.") } val routeStops = remember { mutableListOf<Stop>() } val currentJob = remember { mutableStateOf<Job?>(null) } // Create a graphics overlay to display the selected stops and route. val graphicsOverlay = remember { GraphicsOverlay() } val graphicsOverlays = remember { listOf(graphicsOverlay) } val map = remember { createMap() } Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { MapView( modifier = Modifier.fillMaxWidth().fillMaxHeight(0.7f), arcGISMap = map, ) } }
Update the map
A streets basemap layer is typically used in routing applications. In create
, update the basemap to use the ArcGIS
BasemapStyle
, and change the position of the map to center on Los Angeles.
fun createMap(): ArcGISMap {
return ArcGISMap(BasemapStyle.ArcGISStreets).apply {
initialViewpoint = Viewpoint(
latitude = 34.0539,
longitude = -118.2453,
scale = 144447.638572
)
}
}
Define a RoutesList
to display driving directions
To display the turn-by-turn directions from the route, define a composable function named Routes
that takes a Mutable
parameter named directions
.
Inside the Route
block, add a Lazy
to hold UI items that are based on the directions list. Inside the Lazy
, call the items()
function, passing the size of the directions list.
The trailing lambda for items()
is automatically invoked once for each string in the list, adding a UI item to the Lazy
for each string. In the lambda, call Text
to display the string, and then call Divider
to create a light-gray horizontal line after the text. The result of calling Routes
will be a scrollable list of directions, with items separated by lines.
@Composable
fun RoutesList(directionList: MutableList<String>) {
LazyColumn {
items(directionList.size) { index ->
Text(text = directionList[index] + ".")
Divider(
modifier = Modifier.fillMaxWidth(),
thickness = 1.dp,
color = androidx.compose.ui.graphics.Color.LightGray
)
}
}
}
Create an addStop()
function
A RouteTask
requires at least an origin and a destination stop to find a route. Create a function that adds a Stop
object to the route
list and creates a graphic to display the stop.
When a user taps on the map, a stop will be added to a list of route stops. In this tutorial, the first tap will create the origin stop and the second will create the destination stop.
fun addStop(
routeStops: MutableList<Stop>,
stop: Stop,
graphicsOverlay: GraphicsOverlay
) {
routeStops.add(stop)
// Create a green circle symbol for the stop.
val stopMarker = SimpleMarkerSymbol(
style = SimpleMarkerSymbolStyle.Circle, color = Color.green, size = 20f
)
// Get the stop's geometry.
val routeStopGeometry = stop.geometry
// Add graphic to graphics overlay.
graphicsOverlay.graphics.add(
Graphic(
geometry = routeStopGeometry,
symbol = stopMarker
)
)
}
Create a findRoute()
function
A task can make asynchronous requests to an online service or offline data and then return the results. In this tutorial, we will solve the route between the origin and destination stops using a RouteTask
that accesses a routing service.
-
Create a
suspend
function namedfind
that takes the parameters shown below. Then create aRoute() RouteTask
from a routing service.A routing service with global coverage is part of ArcGIS location services. You can also publish custom routing services using ArcGIS Enterprise.
MainScreen.ktUse dark colors for code blocks suspend fun findRoute( context: Context, routeStops: MutableList<Stop>, graphicsOverlay: GraphicsOverlay, directionsList: MutableList<String> ) { val routeTask = RouteTask( url = "https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World" ) }
-
Using
route
, create the defaultTask RouteParameters
needed to solve a route. Then useroute
to set the route stops and specify that step-by-step directions should be returned. Wrap the code inParameters try-catch
blocks.MainScreen.ktUse dark colors for code blocks suspend fun findRoute( context: Context, routeStops: MutableList<Stop>, graphicsOverlay: GraphicsOverlay, directionsList: MutableList<String> ) { val routeTask = RouteTask( url = "https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World" ) // Create a job to find the route. try { val routeParameters: RouteParameters = routeTask.createDefaultParameters().getOrThrow() routeParameters.setStops(routeStops) routeParameters.returnDirections = true } catch (e: Exception) { showMessage(context, "Failed to find route: ${e.message}") } }
-
Using
route
, solve the route and get the list of routes from the route result.Task MainScreen.ktUse dark colors for code blocks suspend fun findRoute( context: Context, routeStops: MutableList<Stop>, graphicsOverlay: GraphicsOverlay, directionsList: MutableList<String> ) { val routeTask = RouteTask( url = "https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World" ) // Create a job to find the route. try { val routeParameters: RouteParameters = routeTask.createDefaultParameters().getOrThrow() routeParameters.setStops(routeStops) routeParameters.returnDirections = true // Solve a route using the route parameters created. val routeResult: RouteResult = routeTask.solveRoute(routeParameters).getOrThrow() val routes = routeResult.routes } catch (e: Exception) { showMessage(context, "Failed to find route: ${e.message}") } }
-
Get the first route from the list. Using the route's geometry and a green line symbol, create a
Graphic
to display the route.Next, add the route graphic to the graphics overlay. Last, get the list of
Route.directionManeuvers
, and for each direction maneuver, add thedirection
(such as "In 50 ft, turn left on 1st Avenue") to theText directions
.List MainScreen.ktUse dark colors for code blocks suspend fun findRoute( context: Context, routeStops: MutableList<Stop>, graphicsOverlay: GraphicsOverlay, directionsList: MutableList<String> ) { val routeTask = RouteTask( url = "https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World" ) // Create a job to find the route. try { val routeParameters: RouteParameters = routeTask.createDefaultParameters().getOrThrow() routeParameters.setStops(routeStops) routeParameters.returnDirections = true // Solve a route using the route parameters created. val routeResult: RouteResult = routeTask.solveRoute(routeParameters).getOrThrow() val routes = routeResult.routes // If a route is found. if (routes.isNotEmpty()) { val route = routes[0] val routeGraphic = Graphic( geometry = route.routeGeometry, symbol = SimpleLineSymbol( style = SimpleLineSymbolStyle.Solid, color = Color.green, width = 2f ) ) // Add the route graphic to the graphics overlay. graphicsOverlay.graphics.add(routeGraphic) // Get the direction text for each maneuver and display it as a list on the UI. directionsList.clear() route.directionManeuvers.forEach { directionManeuver: DirectionManeuver -> directionsList.add(directionManeuver.directionText) } } } catch (e: Exception) { showMessage(context, "Failed to find route: ${e.message}") } }
Create a clearStops()
function
Create a function to clear the current route.
fun clearStops(
routeStops: MutableList<Stop>,
directionList: MutableList<String>,
graphicsOverlay: GraphicsOverlay
) {
graphicsOverlay.graphics.clear()
routeStops.clear()
directionList.clear()
directionList.add("Tap to add two points to the map to find a route between them.")
}
Handle user input
-
Define a
suspend
function namedon
that handles the user tapping on the screen to set an origin and tapping again to set a destination point. Declare the parameters shown below.Single Tap Confirmed() MainScreen.ktUse dark colors for code blocks fun onSingleTapConfirmed( context: Context, coroutineScope: CoroutineScope, currentJob: MutableState<Job?>, event: SingleTapConfirmedEvent, routeStops: MutableList<Stop>, graphicsOverlay: GraphicsOverlay, directionList: MutableList<String> ) { }
-
Cancel any coroutine
Job
that is currently running.Then get the tapped
Point
using themap
property ofPoint SingleTapConfirmedEvent
, returning an error message if there is no point.Last, create a
Stop
using the point.MainScreen.ktUse dark colors for code blocks fun onSingleTapConfirmed( context: Context, coroutineScope: CoroutineScope, currentJob: MutableState<Job?>, event: SingleTapConfirmedEvent, routeStops: MutableList<Stop>, graphicsOverlay: GraphicsOverlay, directionList: MutableList<String> ) { currentJob.value?.cancel() // Retrieve the tapped map point from the SingleTapConfirmedEvent val point: Point = event.mapPoint ?: return showMessage(context, "No map point retrieved from tap.") val stop = Stop(point) }
-
Call
add
to add the stop toStop() route
and add its graphic to the graphics overlay. Use aStops when
statement to handle whether theroute
list currently contains zero, one, or two stops.Stops - If zero stops, add the stop and do nothing else.
- If one stop, add the stop. Then launch a new coroutine and solve the route by calling
find
.Route() - If two stops, empty the
route
list by callingStops clear
. Then add the stop.Stops()
Assign
coroutine
to the state value ofScope.launch current
.Job MainScreen.ktUse dark colors for code blocks fun onSingleTapConfirmed( context: Context, coroutineScope: CoroutineScope, currentJob: MutableState<Job?>, event: SingleTapConfirmedEvent, routeStops: MutableList<Stop>, graphicsOverlay: GraphicsOverlay, directionList: MutableList<String> ) { currentJob.value?.cancel() // Retrieve the tapped map point from the SingleTapConfirmedEvent val point: Point = event.mapPoint ?: return showMessage(context, "No map point retrieved from tap.") val stop = Stop(point) when (routeStops.size) { // On first tap, add a stop. 0 -> { addStop(routeStops, stop, graphicsOverlay) } // On second tap, add a stop and find route between them. 1 -> { addStop(routeStops, stop, graphicsOverlay) currentJob.value = coroutineScope.launch { findRoute(context, routeStops, graphicsOverlay, directionList) } showMessage(context, "Calculating route...") } // On a further tap, clear and add a new first stop. else -> { clearStops(routeStops, directionList, graphicsOverlay) addStop(routeStops, stop, graphicsOverlay) } } }
Pass parameters to MapView
and call RoutesList
-
Inside the
Scaffold
block, find theMapView
from the Display a map tutorial and replace it with a call of theColumn
composable. AColumn
allows you to display the map view at the top of screen and the routes list directly below.MainScreen.ktUse dark colors for code blocks Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { Column( modifier = Modifier.fillMaxSize().padding(it) ) { } }
-
Inside the
Column
block, add back theMapView
code. Then callRoutes
.List MainScreen.ktUse dark colors for code blocks Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { Column( modifier = Modifier.fillMaxSize().padding(it) ) { MapView( modifier = Modifier.fillMaxWidth().fillMaxHeight(0.7f), arcGISMap = map, ) RoutesList(directionList) } }
-
Pass two additional parameters to
MapView
:graphics
Overlays - a lambda that calls
on
and passes the parameters shown below.Single Tap Confirmed()
MainScreen.ktUse dark colors for code blocks Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { Column( modifier = Modifier.fillMaxSize().padding(it) ) { MapView( modifier = Modifier.fillMaxWidth().fillMaxHeight(0.7f), arcGISMap = map, graphicsOverlays = graphicsOverlays, onSingleTapConfirmed = { event -> onSingleTapConfirmed( context, coroutineScope, currentJob, event, routeStops, graphicsOverlay, directionList ) } ) RoutesList(directionList) } }
-
(Optional) The code in this tutorial calls a function to display messages to the user. One possible implementation of
show
is the following.Message() MainScreen.ktUse dark colors for code blocks fun showMessage(context: Context, message: String) { Toast.makeText(context, message, Toast.LENGTH_LONG).show() }
-
Click Run > Run > app to run the app.
The map should support two taps to create origin and destination points and then use the route service to display the resulting route and turn-by-turn directions.
Alternatively, you can download the tutorial solution, as follows.
Option 2: Download the solution
-
Click the Download solution link in the right-hand side of this page.
-
Unzip the file to a location on your machine.
-
Run Android Studio.
-
Go to File > Open.... Navigate to the solution folder and click Open.
On Windows: If you are in the Welcome to Android Studio dialog, click Open and navigate to the solution folder. Then click Open.
Since the downloaded solution does not contain authentication credentials, you must add the developer credentials that you created in the set up authentication section.
Set developer credentials in the solution
To allow your app users to access ArcGIS location services, use the developer credentials that you created in the Set up authentication step to authenticate requests for resources.
-
In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the
Authentication
toMode .
.API _KEY MainActivity.ktUse dark colors for code blocks class MainActivity : ComponentActivity() { private enum class AuthenticationMode { API_KEY, USER_AUTH } private val authenticationMode = AuthenticationMode.API_KEY
-
Set the
api
property with your API key access token.Key MainActivity.ktUse dark colors for code blocks override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) when (authenticationMode) { AuthenticationMode.API_KEY -> { ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN") }
Best Practice: The access token is stored directly in the code as a convenience for this tutorial. Do not store credentials directly in source code in a production environment.
Run the app
Click Run > Run > app to run the app.
The map should support two taps to create origin and destination points and then use the route service to display the resulting route and turn-by-turn directions.
What's next?
To explore more API features and ArcGIS location services, try the following tutorial: