Show your current position on the map, as well as switch between different types of auto pan modes.

Use case
When using a map within a GIS, it may be helpful for a user to know their own location within a map, whether that’s to aid the user’s navigation or to provide an easy means of identifying/collecting geospatial information at their location.
How to use the sample
On the lower left, there is a switch that allows you to turn location tracking On and Off.
Tap the button in the lower right (which starts in Re-Center mode). A menu will appear with the following options:
- Re-Center - Starts the location display with
LocationDisplayAutoPanModeset toRecenter. - Navigation - Starts the location display with
LocationDisplayAutoPanModeset toNavigation. - Compass - Starts the location display with
LocationDisplayAutoPanModeset toCompassNavigation.
How it works
- Add the composable
MapViewto your UI. - Get a
LocationDisplayobject by callingrememberLocationDisplay()and set it to the composableMapView. - Use
start()andstop()on theLocationDisplay.dataSourceas necessary.
Relevant API
- ArcGISMap
- LocationDisplay
- LocationDisplay.AutoPanMode
- MapView
Additional information
Location permissions are required for this sample.
This sample demonstrates the following AutoPanMode options:
-
Recenter: In this mode, the MapView attempts to keep the location symbol on-screen by re-centering the location symbol when the symbol moves outside a “wander extent”. The location symbol may move freely within the wander extent, but as soon as the symbol exits the wander extent, the MapView re-centers the map on the symbol.
-
Navigation: This mode is best suited for in-vehicle navigation.
-
CompassNavigation: This mode is better suited for waypoint navigation when the user is walking.
This sample uses the GeoView-Compose Toolkit module to be able to implement a composable MapView.
Tags
compass, geoview-compose, GPS, location, map, mobile, navigation, toolkit
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.showdevicelocation
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.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.showdevicelocation.screens.ShowDeviceLocationScreen
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 { ShowDeviceLocationApp() } } }
@Composable private fun ShowDeviceLocationApp() { Surface(color = MaterialTheme.colorScheme.background) { ShowDeviceLocationScreen( sampleName = getString(R.string.show_device_location_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. * */
package com.esri.arcgismaps.sample.showdevicelocation.components
import android.app.Applicationimport androidx.compose.runtime.Stableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.location.LocationDisplayAutoPanModeimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.view.LocationDisplayimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport com.esri.arcgismaps.sample.showdevicelocation.Rimport kotlinx.coroutines.launch
class ShowDeviceLocationViewModel(app: Application) : AndroidViewModel(app) {
val arcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight)
// Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel()
init { viewModelScope.launch { arcGISMap.load().onFailure { messageDialogVM.showMessageDialog(it) } } }
// available options in dropdown menu val dropDownMenuOptions = arrayListOf( ItemData("Re-center", R.drawable.locationdisplayrecenter), ItemData("Navigation", R.drawable.locationdisplaynavigation), ItemData("Compass", R.drawable.locationdisplayheading) )
// This variable holds the currently selected item from the dropdown menu var selectedItem by mutableStateOf(dropDownMenuOptions[0]) private set
// This function handles the selection of an item from the dropdown menu fun onItemSelected(item: ItemData, locationDisplay: LocationDisplay){ selectedItem = item when (item.text) { "Re-center" -> { // re-center MapView on location locationDisplay.setAutoPanMode(LocationDisplayAutoPanMode.Recenter) } "Navigation" -> { // start navigation mode locationDisplay.setAutoPanMode(LocationDisplayAutoPanMode.Navigation) } "Compass" -> { // start compass navigation mode locationDisplay.setAutoPanMode(LocationDisplayAutoPanMode.CompassNavigation) } } }
// variable to track if location tracking is enabled var isLocationTrackingEnabled by mutableStateOf(true) private set
// function to toggle location tracking based on the switch state fun toggleLocationTracking(newValue: Boolean, locationDisplay: LocationDisplay){ isLocationTrackingEnabled = newValue if(isLocationTrackingEnabled) { viewModelScope.launch { locationDisplay.dataSource.start() } } else { viewModelScope.launch { locationDisplay.dataSource.stop() } } }}
/** Represents an option from dropdown menu */@Stabledata class ItemData(val text: String, val imageId: Int)/* 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.showdevicelocation.screens
import android.Manifestimport android.content.Contextimport android.widget.Toastimport androidx.activity.compose.rememberLauncherForActivityResultimport androidx.activity.result.contract.ActivityResultContractsimport androidx.compose.foundation.Imageimport androidx.compose.foundation.borderimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.Spacerimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material3.Cardimport androidx.compose.material3.CardDefaultsimport androidx.compose.material3.DropdownMenuimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Switchimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport 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.platform.LocalContextimport androidx.compose.ui.res.painterResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.dpimport androidx.compose.ui.unit.spimport com.arcgismaps.ArcGISEnvironmentimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplayimport com.esri.arcgismaps.sample.showdevicelocation.components.ShowDeviceLocationViewModelimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Main screen layout for the sample app */@Composablefun ShowDeviceLocationScreen(sampleName: String) { val mapViewModel: ShowDeviceLocationViewModel = viewModel()
val context = LocalContext.current
// some parts of the API require an Android Context to properly interact with Android system // features, such as LocationProvider and application resources ArcGISEnvironment.applicationContext = context.applicationContext
// Create and remember a location display with a recenter auto pan mode. val locationDisplay = rememberLocationDisplay()
// this variable controls the visibility of the dropdown menu var showDropDownMenu by remember { mutableStateOf(false) }
RequestPermissions( context = context, onPermissionsGranted = { mapViewModel.toggleLocationTracking(mapViewModel.isLocationTrackingEnabled, locationDisplay) } )
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { Box( modifier = Modifier .fillMaxSize() .padding(it), ) { MapView( modifier = Modifier .fillMaxSize(), arcGISMap = mapViewModel.arcGISMap, locationDisplay = locationDisplay, )
Card ( modifier = Modifier .align(Alignment.BottomStart) .padding(horizontal = 20.dp, vertical = 70.dp) .border(2.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(20.dp)) .width(150.dp) .height(50.dp), shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation( defaultElevation = 4.dp ), ) { Row( modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly ) { Text(text = "Off", fontSize = 22.sp) Switch( checked = mapViewModel.isLocationTrackingEnabled, onCheckedChange = { isChecked -> mapViewModel.toggleLocationTracking( isChecked, locationDisplay ) } ) Text(text = "On", fontSize = 22.sp) } }
Column ( modifier = Modifier .align(Alignment.BottomEnd) .padding(horizontal = 20.dp, vertical = 70.dp) ) { DropdownMenu( expanded = showDropDownMenu, onDismissRequest = { showDropDownMenu = false }, modifier = Modifier .border(2.dp, MaterialTheme.colorScheme.primary), ) { mapViewModel.dropDownMenuOptions.forEach { option -> DropdownMenuItem( text = @Composable{ Text(option.text, style = TextStyle(fontSize = 22.sp)) }, trailingIcon = @Composable{ Image( painter = painterResource(option.imageId), contentDescription = null, modifier = Modifier.size(25.dp), )}, onClick = { showDropDownMenu = false mapViewModel.onItemSelected(option, locationDisplay) }, ) } }
Spacer(modifier = Modifier.height(5.dp))
Card( modifier = Modifier .clickable { showDropDownMenu = true } .border(2.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(20.dp)) .width(150.dp) .height(50.dp), shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation( defaultElevation = 4.dp ), ) { Row( modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly ) { Text(text = mapViewModel.selectedItem.text, fontSize = 22.sp) Image( painter = painterResource(mapViewModel.selectedItem.imageId), contentDescription = null, modifier = Modifier.size(25.dp), ) } } } }
mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}
@Composablefun RequestPermissions(context: Context, onPermissionsGranted: () -> Unit) {
// Create an activity result launcher using permissions contract and handle the result. val activityResultLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissions -> // Check if both fine & coarse location permissions are true. if (permissions.all { it.value }) { onPermissionsGranted() } else { showError(context, "Location permissions were denied") } }
LaunchedEffect(Unit) { activityResultLauncher.launch( // Request both fine and coarse location permissions. arrayOf( Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION ) ) }}
fun showError(context: Context, message: String) { Toast.makeText(context, message, Toast.LENGTH_LONG).show()}