Learn how to find an address or place with a search bar and the geocoding service.
Geocoding is the process of converting address or place text into a location. The geocoding service can search for an address or a place and perform reverse geocoding.
In this tutorial, you use a search bar in the user interface to access the Geocoding service and search for addresses and places.
Prerequisites
Before starting this tutorial:
-
You need 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. Expand More info for instructions.
-
On your file system, delete the .idea folder, if present, at the top level of your project.
-
In the Android tool window, open app > res > values > strings.xml.
In the
<string name="app
element, change the text content to Search for an address._name" > strings.xmlUse dark colors for code blocks <resources> <string name="app_name">Search for an address</string> </resources>
-
In the Android tool window, open Gradle Scripts > settings.gradle.kts.
Change the value of
root
to "Search for an address".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 = "Search for an address" 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 tool window, 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 Search
.For An Address 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 tool window, 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") }
-
Add import statements and some Compose variables
-
Modify import statements to reference the packages and classes required for this tutorial.
Theme.ktUse dark colors for code blocks @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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBar import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import com.arcgismaps.Color import com.arcgismaps.geometry.SpatialReference import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.BasemapStyle import com.arcgismaps.mapping.Viewpoint import com.arcgismaps.mapping.symbology.HorizontalAlignment import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle import com.arcgismaps.mapping.symbology.TextSymbol import com.arcgismaps.mapping.symbology.VerticalAlignment import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.tasks.geocode.GeocodeParameters import com.arcgismaps.tasks.geocode.GeocodeResult 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.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch
-
In the
Main
composable, create Compose variables that will be passed to various functions in theScreen Main
file.Screen.kt These are remembered state variables and use either
remember()
orremember
Coroutine Scope() Briefly, these variables are as follows:
-
context
: The local context of your app. -
coroutine
: Set toScope remember
. You will use this variable to launch a coroutine.Coroutine Scope() -
focus
: AManager Focus
will allow you to clear focus from the search bar, dismissing the device keyboard after a search is submitted.Manager -
query
: The address that the user enters in the search bar.Text -
current
: TheJob current
variable is of typeJob Mutable
, which references the Kotlin coroutineState <Job? > Job
. (It does not reference the interfaceJob
from ArcGIS Maps SDK for Kotlin.) Note thatcoroutine
returns a Kotlin coroutineScope.launch {} Job
. -
graphics
: AOverlay GraphicsOverlay
to hold the text symbol (the search address) and a red square symbol (the found location on the map). -
graphics
: A list ofOverlay GraphicsOverlay
. You pass this list to theMapView
. -
map
: AView Proxy MapViewProxy
, defined in ArcGIS Maps SDK for Kotlin Toolkit, allows you to set the view point on the map view. -
current
: The currentSpatial Reference SpatialReference
.
Theme.ktUse dark colors for code blocks @Composable fun MainScreen() { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() // The focus manager is used to dismiss keyboard after search query submission. val focusManager = LocalFocusManager.current var queryText by remember { mutableStateOf("") } val currentJob = remember { mutableStateOf<Job?>(null) } val graphicsOverlay = remember { GraphicsOverlay() } val graphicsOverlays = remember { listOf(graphicsOverlay) } val mapViewProxy = remember { MapViewProxy() } val currentSpatialReference = remember { mutableStateOf<SpatialReference?>(null) } val map = remember { createMap() } Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { MapView( modifier = Modifier.fillMaxSize(), arcGISMap = map, ) } }
-
Create a function to geocode an address
Geocoding is implemented with a locator, typically created by referencing a service such as the geocoding service or, for offline geocoding, by referencing locator data contained in a mobile package. Geocoding parameters can be used to refine the results, such as setting a maximum number of results or requesting additional attributes in the results.
-
Define a top-level
suspend
function namedsearch
. Declare the parameters shown below.Address() MainScreen.ktUse dark colors for code blocks suspend fun searchAddress( context: Context, coroutineScope: CoroutineScope, query: String, currentSpatialReference: SpatialReference?, graphicsOverlay: GraphicsOverlay, mapViewProxy: MapViewProxy ) { }
-
Create a
LocatorTask
based on the Geocoding service.A locator task is used to find the location of an address (geocode) or to interpolate an address for a location (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 location.
MainScreen.ktUse dark colors for code blocks suspend fun searchAddress( context: Context, coroutineScope: CoroutineScope, query: String, currentSpatialReference: SpatialReference?, graphicsOverlay: GraphicsOverlay, mapViewProxy: MapViewProxy ) { val geocodeServerUri = "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer" val locatorTask = LocatorTask(geocodeServerUri) }
-
Create a new
GeocodeParameters
and define some of its properties.- Add the names of attributes to return to the
GeocodeParameters.resultAttributeNames
collection. An asterisk (*
) indicates all attributes. - Set the maximum number of results to be returned with
GeocodeParameters.maxResults
. Results are ordered byscore
, so that the first result has the best match score (ranging from 0 for no match to 100 for the best match). - Set the spatial reference for result locations with
GeocodeParameters.outputSpatialReference
. By default, the output spatial reference is defined by the geocode service. For optimal performance when displaying the geocode result, you can ensure that returned coordinates match those of the map view by providing the map view's spatial reference.
When geocoding an address, you can optionally provide
GeocodeParameters
to control certain aspects of the geocoding operation and specify the kinds of results to return from the locator task. Learn more about these parameters in theGeocodeParameters
. For a list of attributes returned with geocode results, see Geocoding service output in the ArcGIS services reference.MainScreen.ktUse dark colors for code blocks suspend fun searchAddress( context: Context, coroutineScope: CoroutineScope, query: String, currentSpatialReference: SpatialReference?, graphicsOverlay: GraphicsOverlay, mapViewProxy: MapViewProxy ) { val geocodeServerUri = "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer" val locatorTask = LocatorTask(geocodeServerUri) // Create geocode parameters val geocodeParameters = GeocodeParameters().apply { resultAttributeNames.add("*") maxResults = 1 outputSpatialReference = currentSpatialReference } }
- Add the names of attributes to return to the
-
To find the location for the address that the user entered, call
LocatorTask.geocode()
, passing thequery
andgeocode
variables.Parameters In the
.on
block, call theSuccess handle
function (which we will define in a later section). Pass parameters toGeocode Results() handle
as shown below.Geocode Results() The geocode results are implemented as a list of
GeocodeResult
objects. In this tutorial, either one or zero results will be returned, as the maximum results parameter was set to 1.MainScreen.ktUse dark colors for code blocks suspend fun searchAddress( context: Context, coroutineScope: CoroutineScope, query: String, currentSpatialReference: SpatialReference?, graphicsOverlay: GraphicsOverlay, mapViewProxy: MapViewProxy ) { val geocodeServerUri = "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer" val locatorTask = LocatorTask(geocodeServerUri) // Create geocode parameters val geocodeParameters = GeocodeParameters().apply { resultAttributeNames.add("*") maxResults = 1 outputSpatialReference = currentSpatialReference } // Search for the address locatorTask.geocode(searchText = query, parameters = geocodeParameters) .onSuccess { geocodeResults: List<GeocodeResult> -> handleGeocodeResults( context, coroutineScope, geocodeResults, graphicsOverlay, mapViewProxy ) }.onFailure { error -> showMessage(context, "The locatorTask.geocode() call failed: ${error.message}") } }
Create two graphics
You will create a graphic that uses a text symbol to display the address as a string. You will create another graphic that uses a red square to indicate the location on the map.
-
Define a top-level function named
create
and declare aText Graphic() GeocodeResult
parameter. For thetext
of theTextSymbol
, use theGeocodeResult.label
property. For thegeometry
of theGraphic
, use theGeocodeResult.displayLocation
property.MainScreen.ktUse dark colors for code blocks fun createTextGraphic(geocodeResult: GeocodeResult): Graphic { val textSymbol = TextSymbol( text = geocodeResult.label, color = Color.black, size = 18f, horizontalAlignment = HorizontalAlignment.Center, verticalAlignment = VerticalAlignment.Bottom ).apply { offsetY = 8f haloColor = Color.white haloWidth = 2f } return Graphic( geometry = geocodeResult.displayLocation, symbol = textSymbol ) }
-
Define a top-level function named
create
and declare aMarker Graphic() GeocodeResult
parameter. For thegeometry
of theGraphic
, use theGeocodeResult.label
property. For theattributes
of theGraphic
, use theGeocodeResult.attributes
property.MainScreen.ktUse dark colors for code blocks fun createMarkerGraphic(geocodeResult: GeocodeResult): Graphic { val simpleMarkerSymbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.Square, color = Color.red, size = 12.0f ) return Graphic( geometry = geocodeResult.displayLocation, attributes = geocodeResult.attributes, symbol = simpleMarkerSymbol ) }
Display the result
The result obtained from the geocode operation can be displayed by adding the two graphics you just created to the map view's graphics overlay.
-
Define a
suspend
function namedhandle
. Declare the parameters shown below.Geocode Results() MainScreen.ktUse dark colors for code blocks fun handleGeocodeResults( context: Context, coroutineScope: CoroutineScope, geocodeResults: List<GeocodeResult>, graphicsOverlay: GraphicsOverlay, mapViewProxy: MapViewProxy ) { }
-
If the
geocode
list is not empty, do the following: First, get the firstResults GeocodeResult
from thegeocode
list. Next, callResults create
andText Graphic() create
, passing that geocode result to each. Last, add the two graphics to theMarker Graphic() GraphicsOverlay
.MainScreen.ktUse dark colors for code blocks fun handleGeocodeResults( context: Context, coroutineScope: CoroutineScope, geocodeResults: List<GeocodeResult>, graphicsOverlay: GraphicsOverlay, mapViewProxy: MapViewProxy ) { if (geocodeResults.isNotEmpty()) { val geocodeResult = geocodeResults[0] // Create a Text graphic to display the address text, and add it to the graphics overlay. val textGraphic = createTextGraphic(geocodeResult) // Create a red square marker graphic, and add it to the graphics overlay. val markerGraphic = createMarkerGraphic(geocodeResult) // Clear previous results and add graphics. graphicsOverlay.graphics.apply { clear() add(textGraphic) add(markerGraphic) } }
-
Inside a
coroutine
block, get the display location (which is aScope.launch Point
) from the geocode result and assign it to a variable namedcenter
. Then callPoint MapViewProxy.setViewpointCenter()
, which is defined in the ArcGIS Maps SDK for Kotlin Toolkit, and passcenter
.Point Last, add code to handle the case where the
geocode
list is empty.Results MainScreen.ktUse dark colors for code blocks fun handleGeocodeResults( context: Context, coroutineScope: CoroutineScope, geocodeResults: List<GeocodeResult>, graphicsOverlay: GraphicsOverlay, mapViewProxy: MapViewProxy ) { if (geocodeResults.isNotEmpty()) { val geocodeResult = geocodeResults[0] // Create a Text graphic to display the address text, and add it to the graphics overlay. val textGraphic = createTextGraphic(geocodeResult) // Create a red square marker graphic, and add it to the graphics overlay. val markerGraphic = createMarkerGraphic(geocodeResult) // Clear previous results and add graphics. graphicsOverlay.graphics.apply { clear() add(textGraphic) add(markerGraphic) } coroutineScope.launch { val centerPoint = geocodeResult.displayLocation ?: return@launch showMessage(context, "The locatorTask.geocode() call failed") // Animate the map view to the center point. mapViewProxy.setViewpointCenter(centerPoint) .onFailure { error -> showMessage(context, "Failed to set Viewpoint center: ${error.message}") } } } else { showMessage(context, "No address found for the given query") } }
Add a search bar and pass parameters to Map View
To search an address using the application, add a search bar that will accept an address as text input.
Next, modify the Scaffold
so it contains a Column
. Inside the Column
, you will add a Search
, followed by MapView
.
-
Inside the
Scaffold
block, find theMapView
from the Display a map tutorial and replace it with a call ofColumn
. AColumn
allows you to display the search bar at the top of the screen and the map view directly below.MainScreen.ktUse dark colors for code blocks @Composable fun MainScreen() { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() // The focus manager is used to dismiss keyboard after search query submission. val focusManager = LocalFocusManager.current var queryText by remember { mutableStateOf("") } val currentJob = remember { mutableStateOf<Job?>(null) } val graphicsOverlay = remember { GraphicsOverlay() } val graphicsOverlays = remember { listOf(graphicsOverlay) } val mapViewProxy = remember { MapViewProxy() } val currentSpatialReference = remember { mutableStateOf<SpatialReference?>(null) } val map = remember { createMap() } Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { Column( modifier = Modifier .fillMaxSize() .padding(it) ) { } } }
-
Call the
Search
composable, passing the parameters shown below.Bar Pass a lambda for the
on
parameter. When theSearch on
callback is invoked, the lambda should clear the focus from the search bar (thus dismissing the device keyboard), then cancel any previous search job, and start a new search job by callingSearch search
. Note that theAddress() on
lambda provides a parameter with the value of the current input query, which you must pass toSearch search
.Address() MainScreen.ktUse dark colors for code blocks @Composable fun MainScreen() { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() // The focus manager is used to dismiss keyboard after search query submission. val focusManager = LocalFocusManager.current var queryText by remember { mutableStateOf("") } val currentJob = remember { mutableStateOf<Job?>(null) } val graphicsOverlay = remember { GraphicsOverlay() } val graphicsOverlays = remember { listOf(graphicsOverlay) } val mapViewProxy = remember { MapViewProxy() } val currentSpatialReference = remember { mutableStateOf<SpatialReference?>(null) } val map = remember { createMap() } Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { Column( modifier = Modifier .fillMaxSize() .padding(it) ) { SearchBar( modifier = Modifier.fillMaxWidth(), query = queryText, onQueryChange = { query -> queryText = query }, onSearch = { currentQuery -> focusManager.clearFocus() // Cancel any previous search job. currentJob.value?.cancel() // Start a new search job. currentJob.value = coroutineScope.launch { searchAddress( context, coroutineScope, currentQuery, currentSpatialReference.value, graphicsOverlay, mapViewProxy ) } }, active = false, onActiveChange = { /* Use this for dynamic search results */ }, leadingIcon = { Icon(Icons.Filled.Search, contentDescription = "Search") }, placeholder = { Text("Search for an address") } ) {} } } }
-
Add back the
MapView
, passing aModifier
andmap
.MainScreen.ktUse dark colors for code blocks Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { Column( modifier = Modifier .fillMaxSize() .padding(it) ) { SearchBar( modifier = Modifier.fillMaxWidth(), query = queryText, onQueryChange = { query -> queryText = query }, onSearch = { currentQuery -> focusManager.clearFocus() // Cancel any previous search job. currentJob.value?.cancel() // Start a new search job. currentJob.value = coroutineScope.launch { searchAddress( context, coroutineScope, currentQuery, currentSpatialReference.value, graphicsOverlay, mapViewProxy ) } }, active = false, onActiveChange = { /* Use this for dynamic search results */ }, leadingIcon = { Icon(Icons.Filled.Search, contentDescription = "Search") }, placeholder = { Text("Search for an address") } ) {} MapView( modifier = Modifier.fillMaxSize(), arcGISMap = map, ) } }
-
Pass these additional parameters to
MapView
:graphics
: Pass theOverlays graphics
variable, defined at the top of theOverlays Main
block.Screen map
: Pass theView Proxy map
variable (of typeView Proxy MapViewProxy
, defined at the top of theMain
block.Screen on
: Pass a lambda that retrieves a spatial reference and assigns it to theSpatial Reference Changed current
variable, defined at the top of theSpatial Reference Main
block.Screen
MainScreen.ktUse dark colors for code blocks MapView( modifier = Modifier.fillMaxSize(), arcGISMap = map, graphicsOverlays = graphicsOverlays, mapViewProxy = mapViewProxy, onSpatialReferenceChanged = { spatialReference -> currentSpatialReference.value = spatialReference } )
-
(Optional) The code in this tutorial calls a function to display messages to the user. One possible implementation of
show
is the following.Message() MainScreen.ktUse dark colors for code blocks fun showMessage(context: Context, message: String) { Toast.makeText(context, message, Toast.LENGTH_LONG).show() }
-
Click Run > Run > app to run the app.
You should see a search box above the map. Search for an address by entering an address and press the magnifying glass on the on the device keyboard. The result of the search should display on the map as a red square.
What's next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: