Learn how to execute a SQL query to return features from a feature layer based on spatial and attribute criteria.
A feature layer can contain a large number of features stored in ArcGIS. You can query a layer to access a subset of its features using any combination of spatial and attribute criteria. You can control whether or not each feature's geometry is returned, as well as which attributes are included in the results. Queries allow you to return a well-defined subset of your hosted data for analysis or display in your app.
In this tutorial, you'll write code to perform SQL queries that return a subset of features in the LA County Parcel feature layer (containing over 2.4 million features). Features that meet the query criteria are selected in the map.
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
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 Query a feature layer (SQL)._name" > strings.xmlUse dark colors for code blocks <resources> <string name="app_name">Query a feature layer (SQL)</string> </resources>
-
In the Android tool window, open Gradle Scripts > settings.gradle.kts.
Change the value of
root
to "Query a feature layer (SQL)".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 = "Query a feature layer (SQL)" 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 Query
.A Feature Layer SQL 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.
-
-
If you downloaded the solution, get an access token and set the API key in MainActivity.kt.
An API Key gives your app access to secure resources used in this tutorial.
-
Go to the Create an API key tutorial to obtain a new API key access token. Ensure that the following privilege is enabled: Location services > Basemaps > Basemap styles service. Copy the access token as it will be used in the next step.
-
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 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
-
Modify import statements to reference the packages and classes required for this tutorial.
MainScreen.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.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable 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 com.arcgismaps.data.QueryParameters import com.arcgismaps.data.ServiceFeatureTable import com.arcgismaps.geometry.Envelope import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.BasemapStyle import com.arcgismaps.mapping.Viewpoint import com.arcgismaps.mapping.layers.FeatureLayer import com.arcgismaps.toolkit.geoviewcompose.MapView import com.example.app.R import kotlinx.coroutines.Job import kotlinx.coroutines.launch
-
In the
Main
composable, create variables that will be passed to various functions in theScreen Main
file.Screen.kt MainScreen.ktUse dark colors for code blocks @Composable fun MainScreen() { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val currentQueryJob = remember { mutableStateOf<Job?>(null) } // Store the current viewpoint geometry extent of the map. val currentExtent = remember { mutableStateOf<Envelope?>(null) } Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { MapView( modifier = Modifier.fillMaxSize(), arcGISMap = map, ) } }
Most of these are remembered variables and use either
remember()
orremember
Coroutine Scope() Briefly, these variables are:
-
context
: The local context of your app. -
coroutine
: Set toScope remember
. You will use this variable to launch a coroutine.Coroutine Scope() -
current
: Of typeQuery Job 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
. -
current
: Of typeExtent Mutable
. The extent (which is anState <Envelope? > Envelope
) of the currentFeatureLayer
. Your query will be limited to this extent.
-
Steps
Create the Parcels feature layer and create a map with it
You will create a service feature table from a feature service URL. Then you will create a feature layer from that table and create an ArcGISMap
with the feature layer.
-
In the
Main
block, create aScreen ServiceFeatureTable
using the feature service URL. Next, create aFeatureLayer
using that service feature table. Define bothservice
andFeature Table feature
as local variables in theLayer Main
composable.Screen The features in this feature service are land parcels in Los Angeles county.
MainScreen.ktUse dark colors for code blocks @Composable fun MainScreen() { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val currentQueryJob = remember { mutableStateOf<Job?>(null) } // Store the current viewpoint geometry extent of the map. val currentExtent = remember { mutableStateOf<Envelope?>(null) } // Create a service feature table from a Los Angeles County parcels feature service. val serviceFeatureTable = ServiceFeatureTable( uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/LA_County_Parcels/FeatureServer/0" ) val featureLayer = remember { FeatureLayer.createWithFeatureTable(serviceFeatureTable) } Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { MapView( modifier = Modifier.fillMaxSize(), arcGISMap = map, ) } }
-
Modify the top-level function
create
to take aMap() FeatureLayer
. Then add the feature layer to theoperationalLayers
property ofMapView
.MainScreen.ktUse dark colors for code blocks fun createMap(featureLayer: FeatureLayer): ArcGISMap { return ArcGISMap(BasemapStyle.ArcGISTopographic).apply { initialViewpoint = Viewpoint( latitude = 34.0270, longitude = -118.8050, scale = 72000.0 ) operationalLayers.add(featureLayer) } }
-
In the
Main
composable, modify the existingScreen create
call by passingMap() feature
as a parameter.Layer MainScreen.ktUse dark colors for code blocks @Composable fun MainScreen() { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val currentQueryJob = remember { mutableStateOf<Job?>(null) } // Store the current viewpoint geometry extent of the map. val currentExtent = remember { mutableStateOf<Envelope?>(null) } // Create a service feature table from a Los Angeles County parcels feature service. val serviceFeatureTable = ServiceFeatureTable( uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/LA_County_Parcels/FeatureServer/0" ) val featureLayer = remember { FeatureLayer.createWithFeatureTable(serviceFeatureTable) } val map = remember { createMap(featureLayer) } Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { MapView( modifier = Modifier.fillMaxSize(), arcGISMap = map, ) } }
Create a function to query the feature layer
Create a function that clears any currently selected features and executes a new query to find features in the map's current extent that meet the selected attribute expression (the SQL WHERE expression). It then gets the features returned by FeatureQueryResult
and selects them (in yellow highlight) in the parcels layer.
-
Define a top-level
suspend
function namedquery
. Declare the parameters shown below.Feature Layer() MainScreen.ktUse dark colors for code blocks /** * Query the [serviceFeatureTable] based on the [whereExpression] on the given * [queryExtent] and select the resulting features on the [featureLayer] */ suspend fun queryFeatureLayer( context: Context, serviceFeatureTable: ServiceFeatureTable, featureLayer: FeatureLayer, whereExpression: String, queryExtent: Envelope? ) { }
-
Create a
QueryParameters
instance, and set thewhere
,Clause return
, andGeometry geometry
properties on the query parameters.MainScreen.ktUse dark colors for code blocks /** * Query the [serviceFeatureTable] based on the [whereExpression] on the given * [queryExtent] and select the resulting features on the [featureLayer] */ suspend fun queryFeatureLayer( context: Context, serviceFeatureTable: ServiceFeatureTable, featureLayer: FeatureLayer, whereExpression: String, queryExtent: Envelope? ) { // Clear any previous selections. featureLayer.clearSelection() // Create query parameters with the where expression and the current extent // and have geometry values returned in the results. val queryParameters = QueryParameters().apply { whereClause = whereExpression returnGeometry = true geometry = queryExtent } }
-
Within
try-catch
statements, callServiceFeatureTable.queryFeatures()
, passingquery
.Parameters Next, get the iterator on
feature
. If theQuery Result result
has any features to return, then iterate over those features and select them (with highlight) on the feature layer.Iterator Then show a message if the query returns no features in the current extent. Last, display a message in the
catch
clause in case the feature search failed.MainScreen.ktUse dark colors for code blocks /** * Query the [serviceFeatureTable] based on the [whereExpression] on the given * [queryExtent] and select the resulting features on the [featureLayer] */ suspend fun queryFeatureLayer( context: Context, serviceFeatureTable: ServiceFeatureTable, featureLayer: FeatureLayer, whereExpression: String, queryExtent: Envelope? ) { // Clear any previous selections. featureLayer.clearSelection() // Create query parameters with the where expression and the current extent // and have geometry values returned in the results. val queryParameters = QueryParameters().apply { whereClause = whereExpression returnGeometry = true geometry = queryExtent } try { // Query the feature table with the query parameters. val featureQueryResult = serviceFeatureTable.queryFeatures(queryParameters).getOrThrow() // Iterate through the result and select the features on the feature layer. val resultIterator = featureQueryResult.iterator() if (resultIterator.hasNext()) { resultIterator.forEach { feature -> featureLayer.selectFeature(feature) } } else { showMessage( context, "No parcels found in the current extent, using Where expression: $whereExpression" ) } } catch (e: Exception) { showMessage(context, "Feature search failed for: $whereExpression, ${e.message}") } }
Create a drop-down menu for query expressions
Create a drop-down menu that allows the user to choose from a list of pre-defined SQL query expressions.
-
Define a composable function named
Query
. Declare anDrop Down Menu on
parameter that takes a lambda to be invoked when the user selects an item from the drop-down menu.Item Clicked In the
Query
block, create twoDrop Down Menu remember
variables namedexpanded
andselection
.-
The
expanded
variable holds a state value, of typeMutable
, that indicates whether the drop-down menu is visually expanded on the device screen.State <Boolean > -
The
selection
variable holds a state value, of typeMutable
, indicates the item that the user chose from the drop-down menu.State <String >
Create a list of the SQL query expressions and assign it to a variable named
sql
. Each expression is a string.Query Expressions MainScreen.ktUse dark colors for code blocks @Composable fun QueryDropDownMenu(onItemClicked: (String) -> Unit) { val expanded = remember { mutableStateOf(false) } val selection = remember { mutableStateOf("") } val sqlQueryExpressions = listOf( "UseType = \'Government\'", "UseType = \'Residential\'", "UseType = \'Irrigated Farm\'", "TaxRateArea = 10853", "TaxRateArea = 10860", "Roll_LandValue > 1000000", "Roll_LandValue < 1000000" ) }
-
-
Call the
Exposed
composable. Pass the following parameters:Drop Down Menu Box - The state value of the
expanded
variable. - A lambda that toggles the state value of the
expanded
variable. The lambda is automatically called when the exposed dropdown menu is clicked and the expansion state changes.
In the
Exposed
block, call the composableDrop Down Menu Box Text
. Pass the parameters shown below. For theField value
parameter, pass the state value of theselection
variable.MainScreen.ktUse dark colors for code blocks @Composable fun QueryDropDownMenu(onItemClicked: (String) -> Unit) { val expanded = remember { mutableStateOf(false) } val selection = remember { mutableStateOf("") } val sqlQueryExpressions = listOf( "UseType = \'Government\'", "UseType = \'Residential\'", "UseType = \'Irrigated Farm\'", "TaxRateArea = 10853", "TaxRateArea = 10860", "Roll_LandValue > 1000000", "Roll_LandValue < 1000000" ) ExposedDropdownMenuBox( expanded = expanded.value, onExpandedChange = { expanded.value = !expanded.value } ) { TextField( modifier = Modifier.fillMaxWidth().menuAnchor(), readOnly = true, value = selection.value, onValueChange = { }, label = { Text("Select a query expression") }, ) } }
- The state value of the
-
Continuing in the
Exposed
composable: callDrop Down Menu Box Exposed
. Pass the state value of theDropdown Menu expanded
variable. Also pass a lambda that sets the state value ofexpanded
to false (that is, hides the displayed drop-down menu).In the
Exposed
block, loop over the list of SQL query expressions. For each expression, call theDrop Down Menu Drop
composable. Pass values for the following parameters.Down Menu Item -
For
text
, pass a lambda that adds aText
displaying the current SQL query expression. -
For
on
, pass a lambda that does the following:Click - Assigns the
selection
to the state value of theOption selection
variable. - Sets the state value of the
expanded
variable to false (to hide the exposed drop-down menu when the user selects an item from the menu). - Call the
on
parameter (the function passed to theItem Clicked Query
) and pass the state value of theDrop Down Menu selection
variable.
- Assigns the
MainScreen.ktUse dark colors for code blocks @Composable fun QueryDropDownMenu(onItemClicked: (String) -> Unit) { val expanded = remember { mutableStateOf(false) } val selection = remember { mutableStateOf("") } val sqlQueryExpressions = listOf( "UseType = \'Government\'", "UseType = \'Residential\'", "UseType = \'Irrigated Farm\'", "TaxRateArea = 10853", "TaxRateArea = 10860", "Roll_LandValue > 1000000", "Roll_LandValue < 1000000" ) ExposedDropdownMenuBox( expanded = expanded.value, onExpandedChange = { expanded.value = !expanded.value } ) { TextField( modifier = Modifier.fillMaxWidth().menuAnchor(), readOnly = true, value = selection.value, onValueChange = { }, label = { Text("Select a query expression") }, ) ExposedDropdownMenu( expanded = expanded.value, onDismissRequest = { expanded.value = false } ) { sqlQueryExpressions.forEach { selectionOption -> DropdownMenuItem( text = { Text(text = selectionOption) }, onClick = { selection.value = selectionOption expanded.value = false onItemClicked(selection.value) } ) } } } }
-
In Scaffold
, call the Query Drop Down Menu
composable.
-
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 drop-down menu at the top of the screen and the map view directly below.MainScreen.ktUse dark colors for code blocks Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { Column( Modifier.fillMaxSize().padding(it) ) { } }
-
Call the
Query
composable function you created above. For theDrop Down Menu on
parameter, pass a lambda that does the following:Item Clicked - Cancels any coroutine
Job
that is currently running. - Launches a coroutine.
- Within the
launch
block, calls thequery
function you defined above. You should pass the arguments shown below. Note that theFeature Layer() sql
is the parameter passed to the lambda.Query Expression
MainScreen.ktUse dark colors for code blocks Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { Column( Modifier.fillMaxSize().padding(it) ) { QueryDropDownMenu( onItemClicked = { sqlQueryExpression -> // Cancel the previous query job if it exists. currentQueryJob.value?.cancel() currentQueryJob.value = coroutineScope.launch { queryFeatureLayer( context = context, serviceFeatureTable = serviceFeatureTable, featureLayer = featureLayer, whereExpression = sqlQueryExpression, queryExtent = currentExtent.value ) } }) } }
- Cancels any coroutine
-
Add back the
MapView
.MainScreen.ktUse dark colors for code blocks Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { Column( Modifier.fillMaxSize().padding(it) ) { QueryDropDownMenu( onItemClicked = { sqlQueryExpression -> // Cancel the previous query job if it exists. currentQueryJob.value?.cancel() currentQueryJob.value = coroutineScope.launch { queryFeatureLayer( context = context, serviceFeatureTable = serviceFeatureTable, featureLayer = featureLayer, whereExpression = sqlQueryExpression, queryExtent = currentExtent.value ) } }) MapView( modifier = Modifier.fillMaxSize(), arcGISMap = map, ) } }
-
Pass the
on
parameter toViewpoint Changed For Bounding Geometry MapView
. For that parameter, pass a lambda that assigns the current viewpoint's extent to the state value of thecurrent
variable.Extent MainScreen.ktUse dark colors for code blocks Scaffold( topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) } ) { Column( Modifier.fillMaxSize().padding(it) ) { QueryDropDownMenu( onItemClicked = { sqlQueryExpression -> // Cancel the previous query job if it exists. currentQueryJob.value?.cancel() currentQueryJob.value = coroutineScope.launch { queryFeatureLayer( context = context, serviceFeatureTable = serviceFeatureTable, featureLayer = featureLayer, whereExpression = sqlQueryExpression, queryExtent = currentExtent.value ) } }) MapView( modifier = Modifier.fillMaxSize(), arcGISMap = map, onViewpointChangedForBoundingGeometry = { viewpoint -> currentExtent.value = viewpoint.targetGeometry.extent } ) } }
-
(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.
The app loads with the map centered on the Santa Monica Mountains in California with the parcels feature layer displayed. Choose an attribute expression, and parcels in the current extent that meet the selected criteria will display in the specified selection color.
What's next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: