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

Routing is the process of finding the path from an origin
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
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
API key credentials are an item that contains the parameters used to create and manage long-lived access tokens for API key authentication. They are a type of developer credential. with the correct privileges. - API keys
An API key is a long-lived access token created using API key credentials. They are valid for up to one year and are typically embedded directly into client applications. 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
An ArcGIS account is an identity with a user type and set of privileges that can access specific ArcGIS products, tools, APIs, services, and resources. The main account types that can be used for development are an ArcGIS Location Platform account, ArcGIS Online account, and ArcGIS Enterprise account. ArcGIS Location Platform and ArcGIS Online accounts are also associated with a subscription. . - User accounts must have privilege
Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. to access the ArcGIS servicesA service, also known as an ArcGIS service, is software that supports an ArcGIS REST API and provides geospatial functionality or data. A service can be hosted by Esri or in ArcGIS Enterprise. used in application. - Requires creating OAuth credentials
OAuth credentials are an item that contains parameters required to implement user authentication or app authentication, including a .client_id,client_secret, and redirect URIs. They are a type of developer credential. - 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.
To complete this tutorial, click on the tab in the switcher below for your authentication type of choice, either API key authentication or User authentication.
Create a new API key access token
-
Complete the Create an API key tutorial and create an API key with the following privilege(s)
Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. :- 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.
Create new OAuth credentials to access the secure resources used in this tutorial.
-
Complete the Create OAuth credentials for user authentication tutorial to obtain a Client ID and Redirect URL.
A
Client IDuniquely identifies your app on the authenticating server. If the server cannot find an app with the provided Client ID, it will not proceed with authentication.The
Redirect URL(also referred to as a callback url) is used to identify a response from the authenticating server when the system returns control back to your app after an OAuth login. Since it does not necessarily represent a valid endpoint that a user could navigate to, the redirect URL can use a custom scheme, such asmy-app://auth. It is important to make sure the redirect URL used in your app’s code matches a redirect URL configured on the authenticating server. -
Copy and paste the Client ID and Redirect URL into a safe location. They will be used in a later step.
All users that access this application need account privileges
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
A routing service is a service that uses network analysis and streets data to calculate the most effective path and turn-by-turn directions on a street network, optimize fleet routing and deliveries, find the closest facilities, calculate service areas, and more. It is hosted by Esri as the ArcGIS Routing service and can also be hosted in ArcGIS Enterprise. . -
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_name">element, change the text content to Find a route and directions.strings.xml<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
rootProject.nameto “Find a route and directions”.settings.gradle.kts14 collapsed linespluginManagement {repositories {google {content {includeGroupByRegex("com\\.android.*")includeGroupByRegex("com\\.google.*")includeGroupByRegex("androidx.*")}}mavenCentral()gradlePluginPortal()}}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
If you implemented API key authenticationRouteTask. To create an API Key access token that has the Basemaps and Routing privileges, see the Set up authentication step and then follow the instructions below.
-
In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity.
-
In the
onCreate()lifecycle method of theMainActivityclass, set theArcGISEnvironment.apiKeyproperty by callingApiKey.create(). Pass in your API key access token as a string and don’t forget the double quotes. Do this before thesetContentblock.MainActivity.ktoverride 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.
Use the Authenticator toolkit component to manage your OAuth credentialsclient_id, client_secret, and redirect URIs. They are a type of developer credential. ArcGISEnvironment.
-
Open the app > kotlin+java > com.example.app > MainActivity.kt file.
-
Set your
clientIDandredirectURLvalues.MainActivity.ktauthenticatorState.oAuthUserConfigurations = listOf(OAuthUserConfiguration(portalUrl = "https://www.arcgis.com",clientId = "YOUR_CLIENT_ID",redirectUrl = "YOUR_REDIRECT_URL"))
Best Practice: The OAuth credentials are 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.kt@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.content.Contextimport android.widget.Toastimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxHeightimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.material3.Dividerimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.HorizontalDividerimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.MutableStateimport androidx.compose.runtime.mutableStateListOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.tasks.networkanalysis.DirectionManeuverimport com.arcgismaps.tasks.networkanalysis.RouteParametersimport com.arcgismaps.tasks.networkanalysis.RouteResultimport com.arcgismaps.tasks.networkanalysis.RouteTaskimport com.arcgismaps.tasks.networkanalysis.Stopimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.example.app.Rimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Job -
In the
MainScreencomposable, create variables that will be passed to various functions in theMainScreen.ktfile.The
remember()function will remember the value across recomposition. TherememberCoroutineScope()obtains a composition-aware scope to launch a coroutine outside a composable.MainScreen.kt@Composablefun MainScreen() {val context = LocalContext.currentval 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 layercreateMap(), update the basemap to use the ArcGISStreets 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 RoutesList that takes a MutableList<String> parameter named directionsList.
Inside the RouteList block, add a LazyColumn to hold UI items that are based on the directions list. Inside the LazyColumn, 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 LazyColumn 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 RoutesList will be a scrollable list of directions, with items separated by lines.
@Composablefun RoutesList(directionList: MutableList<String>) { LazyColumn { items(directionList.size) { index -> Text(text = directionList[index] + ".") HorizontalDivider( modifier = Modifier.fillMaxWidth(), thickness = 1.dp, color = androidx.compose.ui.graphics.Color.LightGray ) } }}Create an addStop() function
A RouteTask requires at least an originStop object to the routeStops 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 serviceRouteTask that accesses a routing service
-
Create a
suspendfunction namedfindRoute()that takes the parameters shown below. Then create aRouteTaskfrom 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.ktsuspend 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
routeTask, create the defaultRouteParametersneeded to solve a route. Then userouteParametersto set the route stops and specify that step-by-step directions should be returned. Wrap the code intry-catchblocks.MainScreen.ktsuspend 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
routeTask, solve the route and get the list of routes from the route result.MainScreen.ktsuspend 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
Graphicto 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 thedirectionText(such as “In 50 ft, turn left on 1st Avenue”) to thedirectionsList.MainScreen.ktsuspend 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
suspendfunction namedonSingleTapConfirmed()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.MainScreen.kt106 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.content.Contextimport android.widget.Toastimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxHeightimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.material3.Dividerimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.HorizontalDividerimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.MutableStateimport androidx.compose.runtime.mutableStateListOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.tasks.networkanalysis.DirectionManeuverimport com.arcgismaps.tasks.networkanalysis.RouteParametersimport com.arcgismaps.tasks.networkanalysis.RouteResultimport com.arcgismaps.tasks.networkanalysis.RouteTaskimport com.arcgismaps.tasks.networkanalysis.Stopimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.example.app.Rimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Jobimport kotlinx.coroutines.launch@Composablefun MainScreen() {val context = LocalContext.currentval 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,)}}@Composablefun RoutesList(directionList: MutableList<String>) {LazyColumn {items(directionList.size) { index ->Text(text = directionList[index] + ".")HorizontalDivider(modifier = Modifier.fillMaxWidth(),thickness = 1.dp,color = androidx.compose.ui.graphics.Color.LightGray)}}}fun createMap(): ArcGISMap {return ArcGISMap(BasemapStyle.ArcGISStreets).apply {initialViewpoint = Viewpoint(latitude = 34.0539,longitude = -118.2453,scale = 144447.638572)}}fun onSingleTapConfirmed(context: Context,coroutineScope: CoroutineScope,currentJob: MutableState<Job?>,event: SingleTapConfirmedEvent,routeStops: MutableList<Stop>,graphicsOverlay: GraphicsOverlay,directionList: MutableList<String>) {}83 collapsed linesfun 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))}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.")}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}")}}fun showMessage(context: Context, message: String) {Toast.makeText(context, message, Toast.LENGTH_LONG).show()} -
Cancel any coroutine
Jobthat is currently running.Then get the tapped
Pointusing themapPointproperty ofSingleTapConfirmedEvent, returning an error message if there is no point.Last, create a
Stopusing the point.MainScreen.ktfun 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 SingleTapConfirmedEventval point: Point = event.mapPoint ?: return showMessage(context, "No map point retrieved from tap.")val stop = Stop(point)} -
Call
addStop()to add the stop torouteStopsand add its graphic to the graphics overlay. Use awhenstatement to handle whether therouteStopslist currently contains zero, one, or two 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
findRoute(). - If two stops, empty the
routeStopslist by callingclearStops(). Then add the stop.
Assign
coroutineScope.launchto the state value ofcurrentJob.MainScreen.ktfun 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 SingleTapConfirmedEventval 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
Scaffoldblock, find theMapViewfrom the Display a map tutorial and replace it with a call of theColumncomposable. AColumnallows you to display the map view at the top of screen and the routes list directly below.MainScreen.kt68 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.content.Contextimport android.widget.Toastimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxHeightimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.material3.Dividerimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.HorizontalDividerimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.MutableStateimport androidx.compose.runtime.mutableStateListOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.tasks.networkanalysis.DirectionManeuverimport com.arcgismaps.tasks.networkanalysis.RouteParametersimport com.arcgismaps.tasks.networkanalysis.RouteResultimport com.arcgismaps.tasks.networkanalysis.RouteTaskimport com.arcgismaps.tasks.networkanalysis.Stopimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.example.app.Rimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Jobimport kotlinx.coroutines.launch@Composablefun MainScreen() {val context = LocalContext.currentval 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)) }) }) {Column(modifier = Modifier.fillMaxSize().padding(it)) {}}148 collapsed lines}@Composablefun RoutesList(directionList: MutableList<String>) {LazyColumn {items(directionList.size) { index ->Text(text = directionList[index] + ".")HorizontalDivider(modifier = Modifier.fillMaxWidth(),thickness = 1.dp,color = androidx.compose.ui.graphics.Color.LightGray)}}}fun createMap(): ArcGISMap {return ArcGISMap(BasemapStyle.ArcGISStreets).apply {initialViewpoint = Viewpoint(latitude = 34.0539,longitude = -118.2453,scale = 144447.638572)}}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 SingleTapConfirmedEventval 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)}}}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))}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.")}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}")}}fun showMessage(context: Context, message: String) {Toast.makeText(context, message, Toast.LENGTH_LONG).show()} -
Inside the
Columnblock, add back theMapViewcode. Then callRoutesList.MainScreen.kt68 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.content.Contextimport android.widget.Toastimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxHeightimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.material3.Dividerimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.HorizontalDividerimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.MutableStateimport androidx.compose.runtime.mutableStateListOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.tasks.networkanalysis.DirectionManeuverimport com.arcgismaps.tasks.networkanalysis.RouteParametersimport com.arcgismaps.tasks.networkanalysis.RouteResultimport com.arcgismaps.tasks.networkanalysis.RouteTaskimport com.arcgismaps.tasks.networkanalysis.Stopimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.example.app.Rimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Jobimport kotlinx.coroutines.launch@Composablefun MainScreen() {val context = LocalContext.currentval 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)) }) }) {Column(modifier = Modifier.fillMaxSize().padding(it)) {MapView(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.7f),arcGISMap = map,)RoutesList(directionList)}}148 collapsed lines}@Composablefun RoutesList(directionList: MutableList<String>) {LazyColumn {items(directionList.size) { index ->Text(text = directionList[index] + ".")HorizontalDivider(modifier = Modifier.fillMaxWidth(),thickness = 1.dp,color = androidx.compose.ui.graphics.Color.LightGray)}}}fun createMap(): ArcGISMap {return ArcGISMap(BasemapStyle.ArcGISStreets).apply {initialViewpoint = Viewpoint(latitude = 34.0539,longitude = -118.2453,scale = 144447.638572)}}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 SingleTapConfirmedEventval 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)}}}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))}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.")}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}")}}fun showMessage(context: Context, message: String) {Toast.makeText(context, message, Toast.LENGTH_LONG).show()} -
Pass two additional parameters to
MapView:graphicsOverlays- a lambda that calls
onSingleTapConfirmed()and passes the parameters shown below.
MainScreen.kt68 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.content.Contextimport android.widget.Toastimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxHeightimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.material3.Dividerimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.HorizontalDividerimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.MutableStateimport androidx.compose.runtime.mutableStateListOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.tasks.networkanalysis.DirectionManeuverimport com.arcgismaps.tasks.networkanalysis.RouteParametersimport com.arcgismaps.tasks.networkanalysis.RouteResultimport com.arcgismaps.tasks.networkanalysis.RouteTaskimport com.arcgismaps.tasks.networkanalysis.Stopimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.example.app.Rimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Jobimport kotlinx.coroutines.launch@Composablefun MainScreen() {val context = LocalContext.currentval 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)) }) }) {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)}}148 collapsed lines}@Composablefun RoutesList(directionList: MutableList<String>) {LazyColumn {items(directionList.size) { index ->Text(text = directionList[index] + ".")HorizontalDivider(modifier = Modifier.fillMaxWidth(),thickness = 1.dp,color = androidx.compose.ui.graphics.Color.LightGray)}}}fun createMap(): ArcGISMap {return ArcGISMap(BasemapStyle.ArcGISStreets).apply {initialViewpoint = Viewpoint(latitude = 34.0539,longitude = -118.2453,scale = 144447.638572)}}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 SingleTapConfirmedEventval 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)}}}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))}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.")}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}")}}fun showMessage(context: Context, message: String) {Toast.makeText(context, message, Toast.LENGTH_LONG).show()} -
(Optional) The code in this tutorial calls a function to display messages to the user. One possible implementation of
showMessage()is the following.MainScreen.kt238 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.content.Contextimport android.widget.Toastimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxHeightimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.material3.Dividerimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.HorizontalDividerimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.MutableStateimport androidx.compose.runtime.mutableStateListOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.tasks.networkanalysis.DirectionManeuverimport com.arcgismaps.tasks.networkanalysis.RouteParametersimport com.arcgismaps.tasks.networkanalysis.RouteResultimport com.arcgismaps.tasks.networkanalysis.RouteTaskimport com.arcgismaps.tasks.networkanalysis.Stopimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.example.app.Rimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Jobimport kotlinx.coroutines.launch@Composablefun MainScreen() {val context = LocalContext.currentval 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)) }) }) {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)}}}@Composablefun RoutesList(directionList: MutableList<String>) {LazyColumn {items(directionList.size) { index ->Text(text = directionList[index] + ".")HorizontalDivider(modifier = Modifier.fillMaxWidth(),thickness = 1.dp,color = androidx.compose.ui.graphics.Color.LightGray)}}}fun createMap(): ArcGISMap {return ArcGISMap(BasemapStyle.ArcGISStreets).apply {initialViewpoint = Viewpoint(latitude = 34.0539,longitude = -118.2453,scale = 144447.638572)}}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 SingleTapConfirmedEventval 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)}}}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))}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.")}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}")}}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 originx,y coordinates and a spatial reference.
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
-
In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the
AuthenticationModeto.API_KEY.MainActivity.kt14 collapsed linespackage com.example.appimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.arcgismaps.httpcore.authentication.OAuthUserConfigurationimport com.arcgismaps.toolkit.authentication.AuthenticatorStateimport com.arcgismaps.toolkit.authentication.DialogAuthenticatorimport com.example.app.screens.MainScreenimport com.example.app.ui.theme.TutorialThemeclass MainActivity : ComponentActivity() {private enum class AuthenticationMode { API_KEY, USER_AUTH }private val authenticationMode = AuthenticationMode.API_KEY42 collapsed linesprivate val authenticatorState = AuthenticatorState()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)when (authenticationMode) {AuthenticationMode.API_KEY -> {ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN")}AuthenticationMode.USER_AUTH -> {authenticatorState.oAuthUserConfigurations = listOf(OAuthUserConfiguration(portalUrl = "https://www.arcgis.com",clientId = "YOUR_CLIENT_ID",redirectUrl = "YOUR_REDIRECT_URL"))}}enableEdgeToEdge()setContent {TutorialTheme {MainScreen()if (authenticationMode == AuthenticationMode.USER_AUTH) {DialogAuthenticator(authenticatorState)}}}}} -
Set the
apiKeyproperty with your API key access token.MainActivity.kt22 collapsed linespackage com.example.appimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.arcgismaps.httpcore.authentication.OAuthUserConfigurationimport com.arcgismaps.toolkit.authentication.AuthenticatorStateimport com.arcgismaps.toolkit.authentication.DialogAuthenticatorimport com.example.app.screens.MainScreenimport com.example.app.ui.theme.TutorialThemeclass MainActivity : ComponentActivity() {private enum class AuthenticationMode { API_KEY, USER_AUTH }private val authenticationMode = AuthenticationMode.API_KEYprivate val authenticatorState = AuthenticatorState()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)when (authenticationMode) {AuthenticationMode.API_KEY -> {ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN")}30 collapsed linesAuthenticationMode.USER_AUTH -> {authenticatorState.oAuthUserConfigurations = listOf(OAuthUserConfiguration(portalUrl = "https://www.arcgis.com",clientId = "YOUR_CLIENT_ID",redirectUrl = "YOUR_REDIRECT_URL"))}}enableEdgeToEdge()setContent {TutorialTheme {MainScreen()if (authenticationMode == AuthenticationMode.USER_AUTH) {DialogAuthenticator(authenticatorState)}}}}}
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.
-
In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the
AuthenticationModeto.USER_AUTH.MainActivity.ktclass MainActivity : ComponentActivity() {private enum class AuthenticationMode { API_KEY, USER_AUTH }private val authenticationMode = AuthenticationMode.USER_AUTH -
Set your
clientIDandredirectURLvalues. You must use the RedirectURL that you supplied for your app in theuser authenticationpart of the Set up authentication step.MainActivity.ktAuthenticationMode.USER_AUTH -> {authenticatorState.oAuthUserConfigurations = listOf(OAuthUserConfiguration(portalUrl = "https://www.arcgis.com",clientId = "YOUR_CLIENT_ID",redirectUrl = "YOUR_REDIRECT_URL")) -
Open app > manifests > AndroidManifest.xml.
-
Set the
android:schemeandandroid:hostusing the scheme and host from your RedirectURL.A redirectURL is composed of a scheme and a host component. The format for the redirect url is
scheme://host. For example, if the redirect url ismyscheme://myhostthen the scheme ismyschemeand the host ismyhost.AndroidManifest.xml41 collapsed lines<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><uses-permission android:name="android.permission.INTERNET" /><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.Tutorial"tools:targetApi="31"><activityandroid:name=".MainActivity"android:exported="true"android:label="@string/app_name"android:theme="@style/Theme.Tutorial"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activityandroid:name="com.arcgismaps.toolkit.authentication.AuthenticationActivity"android:configChanges="keyboard|keyboardHidden|orientation|screenSize"android:exported="true"android:launchMode="singleTop" ><intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><dataandroid:scheme="your_redirect_url_scheme"android:host="your_redirect_url_host" />6 collapsed lines</intent-filter></activity></application></manifest>
Best Practice: The OAuth credentials are 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 originx,y coordinates and a spatial reference.
What’s next?
To explore more API features and ArcGIS location services, try the following tutorial: