Display directions for a route between two points.

Use case
Find routes with driving directions between any number of locations. You might use the ArcGIS platform to create a custom network for routing on a private roads.
How to use the sample
For simplicity, the sample comes loaded with a start and end stop. You can tap on the floating action button to display a route between these stops. Once the route is generated, turn-by-turn directions are shown in an expandable bottom sheet. Tap on a direction to zoom to that portion of the route.
How it works
- Set the
ArcGISEnvironment.applicationContextto use aRouteTask - Create a
RouteTaskusing a URL to an online route service. - Generate default
RouteParametersusingrouteTask.createDefaultParameters(). - Set
returnDirectionson the parameters to true. - Add
Stops to the parametersstopscollection for each destination. - Solve the route using
routeTask.solveRoute(routeParameters)to get aRouteResult. - Iterate through the result’s
Routes. To display the route, create a graphic using the geometry fromroute.routeGeometry. To display directions, useroute.directionManeuvers, and for eachDirectionManeuver, displayDirectionManeuver.directionText.
Relevant API
- DirectionManeuver
- Route
- RouteParameters
- RouteResult
- RouteTask
- Stop
Additional information
This sample uses the GeoView-Compose Toolkit module to be able to implement a composable MapView.
Tags
directions, driving, geoview-compose, navigation, network, network analysis, route, routing, shortest path, toolkit, turn-by-turn
Sample Code
/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.findroute
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.findroute.screens.FindRouteScreenimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
enableEdgeToEdge() setContent { SampleAppTheme { FindRouteApp() } } }
@Composable private fun FindRouteApp() { Surface(color = MaterialTheme.colorScheme.background) { FindRouteScreen( sampleName = getString(R.string.find_route_app_name) ) } }}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
@file:OptIn(ExperimentalMaterial3Api::class)
package com.esri.arcgismaps.sample.findroute.screens
import androidx.compose.foundation.backgroundimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.foundation.lazy.itemsIndexedimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Closeimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.FilledTonalIconButtonimport androidx.compose.material3.HorizontalDividerimport androidx.compose.material3.Iconimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.OutlinedButtonimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.draw.clipimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.platform.LocalConfigurationimport androidx.compose.ui.res.painterResourceimport androidx.compose.ui.text.style.TextAlignimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.tasks.networkanalysis.DirectionManeuverimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.findroute.Rimport com.esri.arcgismaps.sample.findroute.components.FindRouteViewModelimport com.esri.arcgismaps.sample.sampleslib.components.BottomSheetimport com.esri.arcgismaps.sample.sampleslib.components.LoadingDialogimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Main screen layout for the sample app */@Composablefun FindRouteScreen(sampleName: String) { val mapViewModel: FindRouteViewModel = viewModel()
var isRouteTaskRunning by remember { mutableStateOf(false) } var isBottomSheetVisible by remember { mutableStateOf(false) } val isDirectionsAvailable = mapViewModel.directions.isNotEmpty()
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { Column( modifier = Modifier .fillMaxSize() .padding(it) ) { MapView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISMap = mapViewModel.map, mapViewProxy = mapViewModel.mapViewProxy, graphicsOverlays = listOf(mapViewModel.graphicsOverlay) )
Row( modifier = Modifier .fillMaxWidth() .padding(12.dp), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { // solve route button OutlinedButton(onClick = { // show loading dialog isRouteTaskRunning = true // run the route task to solve route mapViewModel.solveRoute( onSolveRouteCompleted = { isRouteTaskRunning = false isBottomSheetVisible = true } ) }) { Text("Solve route directions") } // directions icon button FilledTonalIconButton( enabled = isDirectionsAvailable, onClick = { if (isDirectionsAvailable) isBottomSheetVisible = true }) { Icon( modifier = Modifier.size(30.dp), painter = painterResource(R.drawable.ic_navigate), contentDescription = "Directions icon", tint = if (!isDirectionsAvailable) Color.Gray else MaterialTheme.colorScheme.primary ) } } }
// Bottom sheet to display list of direction maneuvers BottomSheet(isBottomSheetVisible) { DirectionManeuversSheet( directions = mapViewModel.directions, routeDirectionsInfo = mapViewModel.routeDirectionsInfo, onDirectionManeuverSelected = { isBottomSheetVisible = false mapViewModel.selectDirectionManeuver(it) }, onDismissSelected = { isBottomSheetVisible = false } ) }
// display a MessageDialog if the sample encounters an error mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } }
if (isRouteTaskRunning) { LoadingDialog("Solving route ...") } } )}
@Composablefun DirectionManeuversSheet( directions: List<DirectionManeuver>, routeDirectionsInfo: String, onDirectionManeuverSelected: (DirectionManeuver) -> Unit, onDismissSelected: () -> Unit) { Column( modifier = Modifier // Use 1/2 screen height to display sheet .height((LocalConfiguration.current.screenHeightDp * 0.5).dp) .background(MaterialTheme.colorScheme.background) .padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Text( modifier = Modifier .fillMaxWidth() .weight(1f), text = "Directions: $routeDirectionsInfo", style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center )
FilledTonalIconButton(onClick = { onDismissSelected() }) { Icon( imageVector = Icons.Default.Close, contentDescription = "Dismiss sheet" ) } }
LazyColumn( modifier = Modifier .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.surfaceContainerHighest), ) { itemsIndexed(directions) { index: Int, directionManeuver: DirectionManeuver -> Column(Modifier.clickable { onDirectionManeuverSelected(directionManeuver) }) { Text( modifier = Modifier .fillMaxWidth() .padding(12.dp), text = directionManeuver.directionText, style = MaterialTheme.typography.bodyMedium )
if (index < directions.size - 1) HorizontalDivider() } } } }}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.findroute.components
import android.app.Applicationimport android.graphics.drawable.BitmapDrawableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.core.content.ContextCompatimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.ArcGISEnvironmentimport com.arcgismaps.Colorimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.symbology.PictureMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.tasks.networkanalysis.DirectionManeuverimport com.arcgismaps.tasks.networkanalysis.RouteResultimport com.arcgismaps.tasks.networkanalysis.RouteTaskimport com.arcgismaps.tasks.networkanalysis.Stopimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.esri.arcgismaps.sample.findroute.Rimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.launchimport kotlin.math.roundToLong
class FindRouteViewModel(private val application: Application) : AndroidViewModel(application) {
// use a map with navigation basemap style val map = ArcGISMap(BasemapStyle.ArcGISNavigation).apply { initialViewpoint = Viewpoint(latitude = 32.7222, longitude = -117.1530, scale = 100000.0) }
// create a graphic overlay val graphicsOverlay = GraphicsOverlay()
// create a proxy for viewpoint animations val mapViewProxy = MapViewProxy()
// start point private val startPoint = Point( x = -117.1508, y = 32.7411, spatialReference = SpatialReference.wgs84() )
// destination point private val destinationPoint = Point( x = -117.1555, y = 32.7033, spatialReference = SpatialReference.wgs84() )
// create a symbol for the selected maneuver private val selectedDirectionSymbol = SimpleLineSymbol( style = SimpleLineSymbolStyle.Solid, color = Color.red, width = 5f )
// create a simple line symbol for the solved route private val routeSymbol = SimpleLineSymbol( style = SimpleLineSymbolStyle.Solid, color = Color.fromRgba(0, 0, 255, 255), width = 5f )
// keep track of the list of directions maneuvers obtained from the RouteResult var directions by mutableStateOf(listOf<DirectionManeuver>()) private set
// text to display route distance and time var routeDirectionsInfo by mutableStateOf("") private set
// create a messageDialogViewModel to handle dialog interactions val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel()
init { setupSymbols() }
/** * Set up the source, destination and route symbols. */ private fun setupSymbols() { val startDrawable = ContextCompat.getDrawable( application.applicationContext, R.drawable.ic_source ) as BitmapDrawable
val endDrawable = ContextCompat.getDrawable( application.applicationContext, R.drawable.ic_destination ) as BitmapDrawable
val pinSourceSymbol = PictureMarkerSymbol.createWithImage(startDrawable).apply { // make the graphic smaller width = 30f height = 30f offsetY = 20f }
val pinDestinationSymbol = PictureMarkerSymbol.createWithImage(endDrawable).apply { // make the graphic smaller width = 30f height = 30f offsetY = 20f }
// create graphics and it to the graphics overlay graphicsOverlay.graphics.addAll( listOf( Graphic(geometry = startPoint, symbol = pinSourceSymbol), Graphic(geometry = destinationPoint, symbol = pinDestinationSymbol) ) ) }
/** * Solves the route using a [RouteTask], populates the bottom sheet with the directions, * and displays a graphic of the [RouteResult] on the map. */ fun solveRoute(onSolveRouteCompleted: () -> Unit) { // set the applicationContext as it is required with RouteTask ArcGISEnvironment.applicationContext = application.applicationContext
// create a route task instance val routeTask = RouteTask( url = "https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World" )
viewModelScope.launch { routeTask.createDefaultParameters().onSuccess { routeParams -> // create stops val stops = listOf( Stop(startPoint), Stop(destinationPoint) )
routeParams.apply { setStops(stops) // set return directions as true to return turn-by-turn directions in the route's directionManeuvers returnDirections = true } // solve the route val routeResult = routeTask.solveRoute(routeParameters = routeParams).getOrElse { messageDialogVM.showMessageDialog( title = "Error with SolveRoute", description = it.message.toString() ) } as RouteResult
// obtain the first route result val route = routeResult.routes.first()
// create a graphic for the route and add it to the graphics overlay graphicsOverlay.graphics.add( Graphic( geometry = route.routeGeometry, symbol = routeSymbol ) )
// get the list of direction maneuvers and display it // NOTE: to get turn-by-turn directions the route parameters // must have the returnDirections parameter set to true. directions = route.directionManeuvers
// set the time and distance info for the route routeDirectionsInfo = "${route.travelTime.roundToLong()} min " + "(${(route.totalLength / 1000.0).roundToLong()} km)"
// animate the map to the route's geometry route.routeGeometry?.let { geometry -> mapViewProxy.setViewpointGeometry( boundingGeometry = geometry, paddingInDips = 100.0 ) } // notify UI on route task completion onSolveRouteCompleted() }.onFailure { messageDialogVM.showMessageDialog( title = "Error creating route parameters", description = it.message.toString() ) // notify UI on route task completion onSolveRouteCompleted() } } }
/** * Selects the [directionManeuver] graphic and * set's the viewpoint to the bounds of the maneuver geometry */ fun selectDirectionManeuver(directionManeuver: DirectionManeuver) { directionManeuver.geometry?.let { geometry -> // update the graphic of the selected direction maneuver graphicsOverlay.graphics.add( Graphic(geometry = geometry, symbol = selectedDirectionSymbol) )
viewModelScope.launch { // set the viewpoint to the selected maneuver mapViewProxy.setViewpointGeometry( boundingGeometry = geometry, paddingInDips = 100.0 ) } } }}