Learn how to search for places of interest, such as hotels, cafes, and gas stations using the geocoding service.
Geocoding is the process of transforming an address or place name to a location on the earth's surface. A geocoding service allows you to quickly find places that meet specific criteria.
In this tutorial, you use a picklist in the user interface to select a category of places, for example, coffee shops or gas stations. You locate all the places that match this category by accessing a geocoding service. The places are displayed on the map so that you can click on them to get further information.
Prerequisites
Before starting this tutorial, you need the following:
-
An ArcGIS Location Platform or ArcGIS Online account.
-
A development and deployment environment that meets the system requirements.
-
An IDE for Android development in Kotlin.
Steps
Get an access token
You need an access token to use the location services used in this tutorial.
-
Go to the Create an API key tutorial to obtain an access token using your ArcGIS Location Platform or ArcGIS Online account.
-
Ensure that the following privileges are enabled: Location services > Basemaps > Basemap styles service and Location services > Geocoding.
-
Copy the access token as it will be used in the next step.
To learn more about other ways to get an access token, go to Types of authentication.
Open an Android Studio project
-
To start this tutorial, complete the Display a map tutorial, or download and unzip the Display a map solution in a new folder.
-
Modify the old project for use in this new tutorial.
-
On your file system, delete the .idea folder, if present, at the top level of your project.
-
In the Android view, open app > res > values > strings.xml.
In the
<string name="app
element, change the text content to Find places._name" > strings.xmlUse dark colors for code blocks <resources> <string name="app_name">Find places</string> </resources>
-
In the Android view, open Gradle Scripts > settings.gradle.kts.
Change the value of
root
to "Find places".Project.name settings.gradle.ktsUse dark colors for code blocks dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url = uri("https://esri.jfrog.io/artifactory/arcgis") } } } rootProject.name = "Find places" include(":app")
-
The UI theme composable in Display a map tutorial was
Display
. Rename the theme composable throughout the tutorial by refactoringA Map Theme Display
.A Map Theme In the Android view, open app > kotlin+java > com.exmple.app > ui.theme > Theme.kt.
Right-click the function name
Display
and select Refactor -> Rename. Replace the name withA Map Theme Find
.Places Theme Theme.ktUse dark colors for code blocks Copy @Composable fun DisplayAMapTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } darkTheme -> DarkColorScheme else -> LightColorScheme }
-
Click File > Sync Project with Gradle files. Android Studio will recognize your changes and create a new .idea folder.
-
-
Set the API key using the copied access token.
An API Key gives your app access to secure resources used in this tutorial.
-
In Android Studio: in the Android view, open app > java > com.example.app > MainActivity.
-
In the
set
function, find theApi Key() ApiKey.create()
call and paste your copied access token inside the double quotes, replacing YOUR_ACCESS_TOKEN.MainActivity.ktUse dark colors for code blocks Copy private fun setApiKey() { ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN") }
-
-
In
libs.versions.toml
, add a [libraries] entry for the dependency. In the module-levelbuild.gradle.kts (app)
, add the dependency for the lifecycle view model.Use dark colors for code blocks [libraries] arcgis-maps-kotlin = { group = "com.esri", name = "arcgis-maps-kotlin", version.ref = "arcgisMapsKotlin" } arcgis-maps-kotlin-toolkit-bom = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-bom", version.ref = "arcgisMapsKotlin" } arcgis-maps-kotlin-toolkit-geoview-compose = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-geoview-compose" } # Additional modules from Toolkit, if needed, such as: # arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
Add import statements and some Compose variables
-
In the Android view, open app > kotlin+java > com.example.app > screens > MainScreen.kt. Replace the import statements with the imports needed for this tutorial.
MainScreen.ktUse dark colors for code blocks @file:OptIn(ExperimentalMaterial3Api::class) package com.example.app.screens import android.app.Application import android.util.Log import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.arcgismaps.Color import com.arcgismaps.geometry.Envelope import com.arcgismaps.geometry.Point import com.arcgismaps.geometry.SpatialReference import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.BasemapStyle import com.arcgismaps.mapping.GeoElement 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.geocode.GeocodeParameters import com.arcgismaps.tasks.geocode.LocatorTask import com.arcgismaps.toolkit.geoviewcompose.MapView import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy import com.example.app.R import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch
Create a view model
Modern app architecture uses a map view model to hold the business logic and the mutable state of your app.
-
In Android Studio: in the Android view, open app > kotlin+java > com.example.app > screens > MainScreen.kt, create a map view model that extends
View
.Model MainScreen.ktUse dark colors for code blocks class MapViewModel() : ViewModel() { }
-
In the
Main
composable, delete the entire function body. Leave just the function declaration as shown below. Then delete the top-level code for creating a map that is part of the Display a map tutorial. In this tutorial, you will create the map within the view model.Screen MainScreen.ktUse dark colors for code blocks Copy @Composable fun MainScreen() { }
MainScreen.ktUse dark colors for code blocks fun createMap(): ArcGISMap { return ArcGISMap(BasemapStyle.ArcGISTopographic).apply { initialViewpoint = Viewpoint( latitude = 34.0270, longitude = -118.8050, scale = 72000.0 ) } }
-
In the view model, create a map from a
BasemapStyle
and center theArcGISMap.initialViewpoint
on downtown Santa Monica, CA. Then create aMapViewProxy
, which is defined in the ArcGIS Maps SDK for Kotlin Toolkit.MapViewModel classUse dark colors for code blocks // Create a map using the basemap style ArcGISTopographic. val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply { initialViewpoint = Viewpoint( center = Point( x = -118.477324, y = 33.999390, spatialReference = SpatialReference.wgs84() ), scale = 72_000.0 ) } val mapViewProxy = MapViewProxy()
-
Create a
GraphicsOverlay
to display graphics that represent places of the same category, such as coffee shops.A graphics overlay is a container for graphics. It is used with a map view to display graphics on a map. You can add more than one graphics overlay to a map view. Graphics overlays are displayed on top of all the other layers.
Then create other map view model properties as follows:
- The current extent visible on the map view.
- The location on the map where the user taps to obtain further information about the place represented by a graphic.
- The selected geoelement, which is the closest geoelement to the tap location. (A graphic is a type of
GeoElement
.) - The current coroutine Job, so the job can be cancelled before launching a new coroutine.
MapViewModel classUse dark colors for code blocks class MapViewModel() : ViewModel() { // Create a map using the basemap style ArcGISTopographic. val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply { initialViewpoint = Viewpoint( center = Point( x = -118.477324, y = 33.999390, spatialReference = SpatialReference.wgs84() ), scale = 72_000.0 ) } val mapViewProxy = MapViewProxy() val graphicsOverlay = GraphicsOverlay() // The current extent visible in the map view. lateinit var geoViewExtent: Envelope // The location on the map that the user tapped to obtain further information. (A graphic is a type of GeoElement). private val _tapLocation = MutableStateFlow<Point?>(null) val tapLocation: StateFlow<Point?> = _tapLocation.asStateFlow() // The geoelement closest to the location that the user tapped. private val _selectedGeoElement = MutableStateFlow<GeoElement?>(null) val selectedGeoElement: StateFlow<GeoElement?> = _selectedGeoElement.asStateFlow() private var currentIdentifyJob: Job? = null }
Set up the LocatorTask
A locator task is used to search for places using a geocoding service. Results from this search contain the place location and additional information (attributes). Create the locator task along with any variables and methods needed to perform the search and display the results.
-
In the view model, create a
LocatorTask
property namedlocator
based on the Geocoding service.A locator task is used to convert an address to a point (geocode) or vice-versa (reverse geocode). An address includes any type of information that distinguishes a place. A locator involves finding matching locations for a given address. Reverse-geocoding is the opposite and finds the closest address for a given point.
MapViewModel classUse dark colors for code blocks val locator = LocatorTask(uri = "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer")
-
To support the geocode operation, create an
enum
namedCategory
. It should have two properties: aString
named "label" and aColor
named "color". Each category is searched using itslabel
and is distinguished on the map using its associatedcolor
.This tutorial uses category filtering to provide accurate search results based on pre-determined place categories. Feel free to modify this list to your specific requirements.
MapViewModel classUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. enum class Category(val label: String, val color: Color) { // CoffeeShop color is brown. CoffeeShop(label = "Coffee shop", color = Color.fromRgba(r =150, g = 75, b= 0, a = 255)), // GasStation color is orange. GasStation(label = "Gas station", color = Color.fromRgba(r = 255, g = 165, b = 0, a =255)), // Food color is purple. Food(label = "Food", color = Color.fromRgba(r = 160, g = 32, b = 240, a = 255)), // Hotel color is blue. Hotel(label = "Hotel", color = Color.fromRgba(r = 0, g = 0, b = 255, a = 255)), ParksOutdoors(label = "Parks and Outdoors", Color.green) }
-
Create a suspend function called
find
to perform the geocode search operation. The method takes a parameter of typePlaces() Category
that you created in the previous step to indicate which category of places to search for.MapViewModel classUse dark colors for code blocks suspend fun findPlaces(category: Category) { }
-
Clear the previous results by removing all graphics from the graphics overlay. Create and configure new
Geocode
. Populate them with theParameters search
parameter (the current map view extent) and the result attribute names.Area MapViewModel classUse dark colors for code blocks suspend fun findPlaces(category: Category) { graphicsOverlay.graphics.clear() val geocodeParameters = GeocodeParameters().apply { searchArea = geoViewExtent resultAttributeNames.addAll(listOf("Place_addr", "PlaceName")) } }
-
Perform the search query using
geocode()
. Pass in the category'slabel
and the geocode parameters.MapViewModel classUse dark colors for code blocks suspend fun findPlaces(category: Category) { graphicsOverlay.graphics.clear() val geocodeParameters = GeocodeParameters().apply { searchArea = geoViewExtent resultAttributeNames.addAll(listOf("Place_addr", "PlaceName")) } val geocodeResultsList = locator.geocode( searchText = category.label, parameters = geocodeParameters ).getOrElse { error -> return logError(error) } }
-
Create graphics for each of the results and add them to the graphics overlay.
Populate the
graphics
withOverlay SimpleMarkerSymbol
s representing each place returned in the search results. This is very similar to the Add a point, line, and polygon tutorial.MapViewModel classUse dark colors for code blocks suspend fun findPlaces(category: Category) { graphicsOverlay.graphics.clear() val geocodeParameters = GeocodeParameters().apply { searchArea = geoViewExtent resultAttributeNames.addAll(listOf("Place_addr", "PlaceName")) } val geocodeResultsList = locator.geocode( searchText = category.label, parameters = geocodeParameters ).getOrElse { error -> return logError(error) } if (geocodeResultsList.isNotEmpty()) { val placeSymbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.Circle, color = category.color, size = 10f ).apply { outline = SimpleLineSymbol( style = SimpleLineSymbolStyle.Solid, color = Color.white, width = 2f ) } val graphics = geocodeResultsList.map { geocodeResult -> Graphic( geometry = geocodeResult.displayLocation, attributes = geocodeResult.attributes, symbol = placeSymbol ) } graphicsOverlay.graphics.addAll(graphics) } }
Identify the closest geoelement to the user's tap location
An identify operation can be used to get information about a geoelement (such as a graphic) at a location where the user has tapped on the map.
-
Create a function called
identify()
that takes aSingleTapConfirmedEvent
. In the function, first cancel the current identify job. Then launch a new coroutine and callMapViewProxy.identify()
to identify the closest graphic(s) at the tap location.MapViewModel classUse dark colors for code blocks /** * Identifies the tapped screen coordinate in the provided [singleTapConfirmedEvent]. The * identified geoelement is set to [_selectedGeoElement]. * * @since 200.5.0 */ fun identify(singleTapConfirmedEvent: SingleTapConfirmedEvent) { currentIdentifyJob?.cancel() currentIdentifyJob = viewModelScope.launch { val result = mapViewProxy.identify( graphicsOverlay = graphicsOverlay, screenCoordinate = singleTapConfirmedEvent.screenCoordinate, tolerance = 2.dp ) result.onSuccess { identifyGraphicsOverlayResult -> _selectedGeoElement.value = identifyGraphicsOverlayResult.geoElements.firstOrNull() }.onFailure { error -> logError(error) } } }
Add functions to clear the selected geoelement and log errors
-
Add a function that clears the currently selected geoelement. Then add a function to log errors.
MapViewModel classUse dark colors for code blocks fun clearSelectedGeoElement() { _selectedGeoElement.value = null } private fun logError(error: Throwable) { Log.e(this.javaClass.simpleName, error.message.toString(), error.cause) }
Add a Scaffold with a top bar and a dropdown menu
The Main
composable needs some variables to hold state for a dropdown menu, a view model, and the currently selected geoelement. It also needs a Scaffold
composable to display a dropdown menu, the map view, and a callout.
-
In the
Main
composable function add the following:Screen - A mutable state boolean to hold whether a dropdown menu, which you'll create in a later step, is expanded.
- An instance of your
Map
class, using the Jetpack ComposeView Model view
function.Model() - A
selected
variable that collects theGeo Element map
, which was defined as aView Model.selected Geo Element State
.Flow <Geo Element >
MainScreen()Use dark colors for code blocks @Composable fun MainScreen() { var isDropdownExpanded by remember { mutableStateOf(false) } // Create a ViewModel to handle MapView interactions. val mapViewModel: MapViewModel = viewModel() val selectedGeoElement = mapViewModel.selectedGeoElement.collectAsState().value }
-
Add a
Scaffold
that has a top bar. Add an empty content lambda to theScaffold
. That lambda is where you will addMapView
in a later step.MainScreen()Use dark colors for code blocks @Composable fun MainScreen() { var isDropdownExpanded by remember { mutableStateOf(false) } // Create a ViewModel to handle MapView interactions. val mapViewModel: MapViewModel = viewModel() val selectedGeoElement = mapViewModel.selectedGeoElement.collectAsState().value Scaffold( topBar = { TopAppBar( title = { Text(text = stringResource(id = R.string.app_name)) }, ) } ) { } }
-
Add an
actions
lambda as a parameter to theTop
. The lambda will first display a More options icon (the three vertical dots), which the user taps to display a dropdown menu of place categories.App Bar MainScreen()Use dark colors for code blocks @Composable fun MainScreen() { var isDropdownExpanded by remember { mutableStateOf(false) } // Create a ViewModel to handle MapView interactions. val mapViewModel: MapViewModel = viewModel() val selectedGeoElement = mapViewModel.selectedGeoElement.collectAsState().value Scaffold( topBar = { TopAppBar( title = { Text(text = stringResource(id = R.string.app_name)) }, actions = { IconButton(onClick = { isDropdownExpanded = !isDropdownExpanded }) { Icon( imageVector = Icons.Default.MoreVert, contentDescription = "More options" ) } } ) } ) { } }
-
The
actions
lambda then defines aDropdown
that has aMenu Dropdown
for each of the categories you created in the map view model.Menu Item MainScreen()Use dark colors for code blocks @Composable fun MainScreen() { var isDropdownExpanded by remember { mutableStateOf(false) } // Create a ViewModel to handle MapView interactions. val mapViewModel: MapViewModel = viewModel() val selectedGeoElement = mapViewModel.selectedGeoElement.collectAsState().value Scaffold( topBar = { TopAppBar( title = { Text(text = stringResource(id = R.string.app_name)) }, actions = { IconButton(onClick = { isDropdownExpanded = !isDropdownExpanded }) { Icon( imageVector = Icons.Default.MoreVert, contentDescription = "More options" ) } DropdownMenu( expanded = isDropdownExpanded, onDismissRequest = { isDropdownExpanded = false } ) { DropdownMenuItem( onClick = { mapViewModel.clearSelectedGeoElement() mapViewModel.viewModelScope.launch { mapViewModel.findPlaces(category = MapViewModel.Category.CoffeeShop) isDropdownExpanded = false } }, text = { Text(MapViewModel.Category.CoffeeShop.label) } ) DropdownMenuItem( onClick = { mapViewModel.clearSelectedGeoElement() mapViewModel.viewModelScope.launch { mapViewModel.findPlaces(category = MapViewModel.Category.GasStation) isDropdownExpanded = false } }, text = { Text(MapViewModel.Category.GasStation.label) } ) DropdownMenuItem( onClick = { mapViewModel.clearSelectedGeoElement() mapViewModel.viewModelScope.launch { mapViewModel.findPlaces(category = MapViewModel.Category.Food) isDropdownExpanded = false } }, text = { Text(MapViewModel.Category.Food.label) } ) DropdownMenuItem( onClick = { mapViewModel.clearSelectedGeoElement() mapViewModel.viewModelScope.launch { mapViewModel.findPlaces(category = MapViewModel.Category.Hotel) isDropdownExpanded = false } }, text = { Text(MapViewModel.Category.Hotel.label) } ) DropdownMenuItem( onClick = { mapViewModel.clearSelectedGeoElement() mapViewModel.viewModelScope.launch { mapViewModel.findPlaces(category = MapViewModel.Category.ParksOutdoors) isDropdownExpanded = false } }, text = { Text(MapViewModel.Category.ParksOutdoors.label) } ) } } ) } ) { } }
Add Map View
to display the map and interact with the map view model
The MapView
in this tutorial does the following:
-
Displays the current extent of the map when the extent changes.
-
Defines a
MapViewProxy
-
Attaches the graphics overlay to display the graphics that represent places matching the category selected in the dropdown menu.
-
Calls the
identify()
function when the user taps on the map. You created this function in the map view model.MapView()Use dark colors for code blocks MapView(modifier = Modifier .fillMaxSize() .padding(it), arcGISMap = mapViewModel.map, onVisibleAreaChanged = { newVisibleArea -> mapViewModel.geoViewExtent = newVisibleArea.extent }, mapViewProxy = mapViewModel.mapViewProxy, graphicsOverlays = listOf(mapViewModel.graphicsOverlay), onSingleTapConfirmed = { singleTapConfirmedEvent -> mapViewModel.identify(singleTapConfirmedEvent) }, )
Define Callout Content
to display name and address for the found place
When a user taps on a graphic, a callout displays showing the name and address of the geoelement closest to the tapped location on the map.
-
Define a composable function named
Callout
that displays the name of the place in larger font size and the address of the place in a smaller size.Content CalloutContent()Use dark colors for code blocks /** * Content for the Callout to display information on the tapped graphics overlay and its [selectedElementAttributes] */ @Composable fun CalloutContent( selectedElementAttributes: Map<String, Any?> ) { LazyColumn(contentPadding = PaddingValues(8.dp)) { selectedElementAttributes.forEach { attribute -> item { val style = if (attribute.key == "PlaceName") { MaterialTheme.typography.titleLarge } else { MaterialTheme.typography.bodyMedium } Text( text = "${attribute.value}", fontStyle = FontStyle.Normal, style = style, textAlign = TextAlign.Start ) } } } }
Display information for the found place
The Map
needs one more parameter so the callout content displays on top of the map view.
-
Go back to
Map
in your code and add theView content
parameter, which is a lambda that calls theMapViewScope.Callout()
composable defined in the ArcGIS Maps SDK for Kotlin Toolkit. TheCallout
function has its owncontent
lambda parameter, which in turn calls yourCallout
function with the attributes (Content Place
,_addr Place
) you specified in the map view model .Name MapView()Use dark colors for code blocks MapView(modifier = Modifier .fillMaxSize() .padding(it), arcGISMap = mapViewModel.map, onVisibleAreaChanged = { newVisibleArea -> mapViewModel.geoViewExtent = newVisibleArea.extent }, mapViewProxy = mapViewModel.mapViewProxy, graphicsOverlays = listOf(mapViewModel.graphicsOverlay), onSingleTapConfirmed = { singleTapConfirmedEvent -> mapViewModel.identify(singleTapConfirmedEvent) }, content = if (selectedGeoElement != null) { { Callout( modifier = Modifier .wrapContentSize() .height(120.dp) .widthIn(max = 300.dp), geoElement = selectedGeoElement, tapLocation = mapViewModel.tapLocation.value ) { CalloutContent( selectedElementAttributes = selectedGeoElement.attributes ) } } } else { null } )
-
Click Run > Run > app to run the app.
When the app opens, tap the More options icon in the upper right and select a place category. Then tap one of the places and see its name and address. If you zoom or pan to another area, tap the More options icon again and select a category to display places in the new extent.
What's next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: