Learn how to find a route and directions with the route 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. Learn more .

find a route and directions

Routing is the process of finding the path from an origin An origin is a point that defines the start of a route. Learn more to a destination A destination is a point that defines the final stop in a route. Learn more in a street network. You can use the 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. Learn more to find routes A route is a polyline that defines the best path between two or more points in a street network. Learn more , 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:

  1. An ArcGIS Location Platform or ArcGIS Online account.

  2. A development and deployment environment that meets the system requirements.

  3. An IDE for Android development in Kotlin.

Set up authentication

To access the secure ArcGIS location services ArcGIS Location Services, also referred to as Location Services, are services hosted by Esri that provide geospatial functionality for developing mapping applications. They include the ArcGIS Basemap Styles service, ArcGIS Static Basemap Tiles service, ArcGIS Places service, ArcGIS Geocoding service, ArcGIS Routing service, ArcGIS GeoEnrichment service, and ArcGIS Elevation service. An ArcGIS Location Platform or ArcGIS Online account is required to use the services. Learn more used in this tutorial, you must implement API key authentication API key authentication is a type of authentication that uses an API key to authenticate requests to ArcGIS services and secure portal items. Learn more or user authentication User authentication is a type of authentication that allows users with an ArcGIS account to sign into an application and allow it to access ArcGIS content, services, and resources on their behalf. The typical authorization protocol used is OAuth2.0. Learn more using an ArcGIS Location Platform An ArcGIS Location Platform account, formerly known as an ArcGIS Developer account, is an identity associated with an ArcGIS Location Platform subscription. Learn more or an ArcGIS Online An ArcGIS Online account, also known as an ArcGIS Organization account, is an identity associated with an ArcGIS Online subscription. It can be used to access ArcGIS tools and develop applications with ArcGIS location services for an organization. Learn more account.

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 An access token is an authorization string that provides access to secure ArcGIS content, data, and services. Its capabilities are determined by the privileges it supports. It is obtained by implementing API key authentication, User authentication, or App authentication. Learn more with privileges Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. Learn more to access the secure resources used in this tutorial.

  1. 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. Learn more :

    • Privileges
      • Location services > Basemaps
      • Location services > Routing
  2. 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:

  1. Option 1: Develop the code or
  2. Option 2: Download the completed solution

Option 1: Develop the code

Open an Android Studio project

  1. Open the project you created by completing the Display a map tutorial.

  2. 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. Learn more .

  3. Modify the old project for use in this new tutorial.

Set developer credentials

If you implemented API key authentication API key authentication is a type of authentication that uses an API key to authenticate requests to ArcGIS services and secure portal items. Learn more in the Display a map tutorial, the API key access token will only have the Basemaps privilege. The Find a route and directions tutorial requires the Routing privilege to find a route using the RouteTask. 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.

  1. In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity.

  2. In the onCreate() lifecycle method of the MainActivity class, set the ArcGISEnvironment.apiKey​ property by calling ApiKey.create(). Pass in your API key access token as a string and don’t forget the double quotes. Do this before the setContent block.

    MainActivity.kt
    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

  1. 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.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.HorizontalDivider
    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
  2. In the MainScreen composable, create variables that will be passed to various functions in the MainScreen.kt file.

    MainScreen.kt
    @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 A basemap layer is the layer in a map or scene that displays basemap data. The data source for a basemap layer is typically a basemap service. Learn more is typically used in routing applications. In createMap(), update the basemap to use the ArcGISStreets BasemapStyle, and change the position of the map to center on Los Angeles.

MainScreen.kt
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.

MainScreen.kt
@Composable
fun 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 origin An origin is a point that defines the start of a route. Learn more and a destination A destination is a point that defines the final stop in a route. Learn more stop to find a route A route is a polyline that defines the best path between two or more points in a street network. Learn more . Create a function that adds a Stop object to the routeStops list and creates a graphic to display the stop.

MainScreen.kt
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 A 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. Learn more or offline data and then return the results. In this tutorial, we will solve the route between the origin An origin is a point that defines the start of a route. Learn more and destination A destination is a point that defines the final stop in a route. Learn more stops A stop is a single point along a route: it can be the origin, an intermediate stop, or destination. Learn more using a RouteTask that accesses a 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. Learn more .

  1. Create a suspend function named findRoute() that takes the parameters shown below. Then create a RouteTask from a routing service.

    MainScreen.kt
    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"
    )
    }
  2. Using routeTask, create the default RouteParameters needed to solve a route. Then use routeParameters to set the route stops and specify that step-by-step directions should be returned. Wrap the code in try-catch blocks.

    MainScreen.kt
    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}")
    }
    }
  3. Using routeTask, solve the route and get the list of routes from the route result.

    MainScreen.kt
    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}")
    }
    }
  4. 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 the directionText (such as “In 50 ft, turn left on 1st Avenue”) to the directionsList.

    MainScreen.kt
    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.

MainScreen.kt
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

  1. Define a suspend function named onSingleTapConfirmed() 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.kt
    106 collapsed lines
    @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.HorizontalDivider
    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
    @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,
    )
    }
    }
    @Composable
    fun 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 lines
    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()
    }
  2. Cancel any coroutine Job that is currently running.

    Then get the tapped Point using the mapPoint property of SingleTapConfirmedEvent, returning an error message if there is no point.

    Last, create a Stop using the point.

    MainScreen.kt
    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)
    }
  3. Call addStop() to add the stop to routeStops and add its graphic to the graphics overlay. Use a when statement to handle whether the routeStops list 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 routeStops list by calling clearStops(). Then add the stop.
    MainScreen.kt
    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

  1. Inside the Scaffold block, find the MapView from the Display a map tutorial and replace it with a call of the Column composable. A Column allows you to display the map view at the top of screen and the routes list directly below.

    MainScreen.kt
    68 collapsed lines
    @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.HorizontalDivider
    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
    @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)) }) }) {
    Column(
    modifier = Modifier.fillMaxSize().padding(it)
    ) {
    }
    }
    148 collapsed lines
    }
    @Composable
    fun 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 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)
    }
    }
    }
    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()
    }
  2. Inside the Column block, add back the MapView code. Then call RoutesList.

    MainScreen.kt
    68 collapsed lines
    @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.HorizontalDivider
    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
    @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)) }) }) {
    Column(
    modifier = Modifier.fillMaxSize().padding(it)
    ) {
    MapView(
    modifier = Modifier.fillMaxWidth().fillMaxHeight(0.7f),
    arcGISMap = map,
    )
    RoutesList(directionList)
    }
    }
    148 collapsed lines
    }
    @Composable
    fun 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 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)
    }
    }
    }
    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()
    }
  3. Pass two additional parameters to MapView:

    • graphicsOverlays
    • a lambda that calls onSingleTapConfirmed() and passes the parameters shown below.
    MainScreen.kt
    68 collapsed lines
    @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.HorizontalDivider
    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
    @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)) }) }) {
    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
    }
    @Composable
    fun 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 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)
    }
    }
    }
    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()
    }
  4. (Optional) The code in this tutorial calls a function to display messages to the user. One possible implementation of showMessage() is the following.

    MainScreen.kt
    238 collapsed lines
    @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.HorizontalDivider
    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
    @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)) }) }) {
    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)
    }
    }
    }
    @Composable
    fun 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 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)
    }
    }
    }
    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()
    }
  5. Click Run > Run > app to run the app.

The map should support two taps to create origin An origin is a point that defines the start of a route. Learn more and destination A destination is a point that defines the final stop in a route. Learn more points A point is a type of geometry containing a single set of x,y coordinates and a spatial reference. Learn more and then use the route 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. Learn more to display the resulting route and turn-by-turn directions.

Alternatively, you can download the tutorial solution, as follows.

Option 2: Download the solution

  1. Click the Download solution link in the right-hand side of this page.

  2. Unzip the file to a location on your machine.

  3. Run Android Studio.

  4. 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 ArcGIS Location Services, also referred to as Location Services, are services hosted by Esri that provide geospatial functionality for developing mapping applications. They include the ArcGIS Basemap Styles service, ArcGIS Static Basemap Tiles service, ArcGIS Places service, ArcGIS Geocoding service, ArcGIS Routing service, ArcGIS GeoEnrichment service, and ArcGIS Elevation service. An ArcGIS Location Platform or ArcGIS Online account is required to use the services. Learn more , use the developer credentials that you created in the Set up authentication step to authenticate requests for resources.

  1. In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the AuthenticationMode to .API_KEY.

    MainActivity.kt
    14 collapsed lines
    package com.example.app
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import com.arcgismaps.ApiKey
    import com.arcgismaps.ArcGISEnvironment
    import com.arcgismaps.httpcore.authentication.OAuthUserConfiguration
    import com.arcgismaps.toolkit.authentication.AuthenticatorState
    import com.arcgismaps.toolkit.authentication.DialogAuthenticator
    import com.example.app.screens.MainScreen
    import com.example.app.ui.theme.TutorialTheme
    class MainActivity : ComponentActivity() {
    private enum class AuthenticationMode { API_KEY, USER_AUTH }
    private val authenticationMode = AuthenticationMode.API_KEY
    42 collapsed lines
    private 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)
    }
    }
    }
    }
    }
  2. Set the apiKey property with your API key access token.

    MainActivity.kt
    22 collapsed lines
    package com.example.app
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import com.arcgismaps.ApiKey
    import com.arcgismaps.ArcGISEnvironment
    import com.arcgismaps.httpcore.authentication.OAuthUserConfiguration
    import com.arcgismaps.toolkit.authentication.AuthenticatorState
    import com.arcgismaps.toolkit.authentication.DialogAuthenticator
    import com.example.app.screens.MainScreen
    import com.example.app.ui.theme.TutorialTheme
    class MainActivity : ComponentActivity() {
    private enum class AuthenticationMode { API_KEY, USER_AUTH }
    private val authenticationMode = AuthenticationMode.API_KEY
    private 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 lines
    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)
    }
    }
    }
    }
    }

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 An origin is a point that defines the start of a route. Learn more and destination A destination is a point that defines the final stop in a route. Learn more points A point is a type of geometry containing a single set of x,y coordinates and a spatial reference. Learn more and then use the route 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. Learn more 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: