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 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.
Steps
Get an access token
You need an access token to use the location services used in this tutorial.
-
Go to the Create an API key tutorial to obtain an access token.
-
Ensure that the following privileges are enabled: Location services > Basemaps > Basemap styles service and Location services > Routing.
-
Copy the access token as it will be used in the next step.
To learn more about other ways to get an access token, go to Types of authentication.
Open an Android Studio project
-
To start this tutorial, complete the Display a map tutorial. Or download and unzip the Display a map solution in a new folder.
-
Modify the old project for use in this new tutorial. Expand More info for instructions.
-
On your file system, delete the .idea folder, if present, at the top level of your project.
-
In the Android tool window, 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 tool window, 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")
-
The UI theme composable in Display a map tutorial was
Display
. Rename the theme composable throughout the tutorial by refactoringA Map Theme Display
.A Map Theme In the Android tool window, open app > kotlin+java > com.exmple.app > ui.theme > Theme.kt.
Right-click the function name
Display
and select Refactor -> Rename. Replace the name withA Map Theme Find
.A Route And Directions Theme Theme.ktUse dark colors for code blocks Copy @Composable fun DisplayAMapTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } darkTheme -> DarkColorScheme else -> LightColorScheme }
-
Click File > Sync Project with Gradle files. Android Studio will recognize your changes and create a new .idea folder.
-
-
Set the API key using the copied access token.
An API Key gives your app access to secure resources used in this tutorial.
-
In Android Studio: in the Android tool window, open app > java > com.example.app > MainActivity.
-
In the
set
function, find theApi Key() ApiKey.create()
call and paste your copied access token inside the double quotes, replacing YOUR_ACCESS_TOKEN.MainActivity.ktUse dark colors for code blocks Copy private fun setApiKey() { ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN") }
-
Add import statements and some Compose variables
-
In the Android tool window, 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 Routes List
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 add Stop()
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 find Route()
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 clear Stops()
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 Map View
and call Routes List
-
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.
What's next?
To explore more API features and ArcGIS location services, try the following tutorial: