With the ArcGIS Maps SDK for Kotlin, you can perform advanced operations on the composable MapView and SceneView components using proxy classes like MapViewProxy and SceneViewProxy. The proxy classes provides access to methods and properties that aren’t suitable for use in a composable function’s initializer or a modifier. They enable operations such as:

  • Identifying features on the map.
  • Setting the viewpoint with animation.
  • Converting coordinates between the map and screen.
  • Exporting a snapshot of the view’s image.

To use a proxy, you must first create a one-to-one relationship between it and the composable view. This is done by passing a proxy instance to the composable MapView or SceneView function.

MapView(
arcGISMap = map,
graphicsOverlays = listOf(mapViewModel.graphicsOverlay),
mapViewProxy = mapViewModel.mapViewProxy
)

Note: The example above shows a MapView that uses a MapViewProxy. Similarly, you would use a SceneViewProxy with a SceneView.

Identify features

The following example demonstrates how to use the MapViewProxy to identify features on a map. When a user taps the screen, the identifyLayers method is called to retrieve features at that location and display their associated popups or attributes.

// In the ViewModel:
class MapViewModel(application: Application) : AndroidViewModel(application) {
// ....
// Proxy object used to help identify the feature at the tapped coordinate
val mapViewProxy = MapViewProxy()
fun identifyFeatureAt(
screenCoordinate: ScreenCoordinate,
onFeatureSelected: (ArcGISFeature) -> Unit
) {
viewModelScope.launch {
// Identify the feature tapped at the given screen coords
val identifyLayerResults = mapViewProxy.identifyLayers(
screenCoordinate = screenCoordinate,
tolerance = 5.dp,
returnPopupsOnly = false,
maximumResults = 1
).getOrThrow()
// Identify the layer
val identifyLayerResult = identifyLayerResults.firstOrNull()
// Identify the feature
val foundFeature = identifyLayerResult?.geoElements?.firstOrNull() as? ArcGISFeature
if (foundFeature != null) {
// Report the selected feature to UI
onFeatureSelected(foundFeature)
}
}
}
// In the Composable:
@Composable
fun MainScreen(modifier: Modifier, viewmodel: MapViewModel = viewModel()) {
var selectedFeature by remember { mutableStateOf<ArcGISFeature?>(null) }
MapView(
modifier = modifier.fillMaxSize(),
arcGISMap = viewmodel.map,
mapViewProxy = viewmodel.mapViewProxy,
onSingleTapConfirmed = { tapEvent ->
viewmodel.identifyFeatureAt(
screenCoordinate = tapEvent.screenCoordinate,
onFeatureSelected = { feature ->
selectedFeature = feature
}
)
}
) {
}
}

Set a Viewpoint

You can use the MapViewProxy to programmatically change the viewpoint of the map. This is useful for things like zooming to a specific feature or panning to a new location.

// Identify the feature
val foundFeature = identifyLayerResult?.geoElements?.firstOrNull() as? ArcGISFeature
if (foundFeature != null) {
// Animate the viewpoint to center on the feature.
foundFeature.geometry?.let { centerPoint ->
mapViewProxy.setViewpointAnimated(
viewpoint = Viewpoint(centerPoint),
duration = 300.milliseconds,
curve = AnimationCurve.EaseOutSine
)
}
// Report the selected feature to UI
onFeatureSelected(foundFeature)
}

Display a Callout on a GeoElement

When a user taps on a map and a feature is identified, you often want to display a Popup, or Callout, to show more information. You can use a mutableStateOf property in your ViewModel to hold the identified GeoElement. When this property is updated, Compose automatically recomposes the UI and displays the Callout.

The following example combines the above patterns to create a full workflow.

  1. A user taps the map, triggering the onSingleTapConfirmed event handler.
  2. The ViewModel’s identifyFeatureAt function is called, which uses the MapViewProxy to find a feature at the tapped location.
  3. The setViewpointAnimated function is used to gracefully move the map’s viewpoint to the identified feature.
  4. The ViewModel updates its selectedFeature state.
  5. Compose detects the state change and displays a Callout at the feature’s location, using its attributes to populate the content.

This seamless pattern demonstrates how MapViewProxy and composable state work together to build a responsive and interactive user experience.

Perform Geoview Operations Demo
39 collapsed lines
package com.example.guide.programmingpatterns.kotlinprogrammingpractices.snips.screens.performgeoviewoperations
import android.app.Application
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.data.ServiceFeatureTable
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.layers.FeatureLayer
import com.arcgismaps.mapping.view.AnimationCurve
import com.arcgismaps.mapping.view.ScreenCoordinate
import com.arcgismaps.toolkit.geoviewcompose.MapView
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
// In the Composable:
@Composable
fun MainScreen(modifier: Modifier, viewmodel: MapViewModel = viewModel()) {
var selectedFeature by remember { mutableStateOf<ArcGISFeature?>(null) }
MapView(
modifier = modifier.fillMaxSize(),
arcGISMap = viewmodel.map,
mapViewProxy = viewmodel.mapViewProxy,
onSingleTapConfirmed = { tapEvent ->
viewmodel.identifyFeatureAt(
screenCoordinate = tapEvent.screenCoordinate,
onFeatureSelected = { feature ->
selectedFeature = feature
}
)
}
) {
selectedFeature?.let { selectedFeature ->
Callout(geoElement = selectedFeature) {
CalloutContent(
selectedElementAttributes = viewmodel.filterAttributes(
attributes = selectedFeature.attributes
)
)
}
}
}
}
@Composable
fun CalloutContent(selectedElementAttributes: Map<String, Any?>) {
LazyColumn(
modifier = Modifier.widthIn(max = 250.dp),
contentPadding = PaddingValues(8.dp)
) {
selectedElementAttributes.forEach { attribute ->
item {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Text(
text = "${attribute.key}:",
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.labelMedium
)
Spacer(modifier = Modifier.width(20.dp))
Text(
text = "${attribute.value}",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.End
)
}
}
}
}
}
65 collapsed lines
// In the ViewModel:
class MapViewModel(application: Application) : AndroidViewModel(application) {
// ....
// Feature table of street trees in Portland.
private val featureTable = ServiceFeatureTable(
uri = "https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/rest/services/Trees_of_Portland/FeatureServer/0"
)
val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
// Set a viewpoint to downtown Portland, OR.
initialViewpoint = Viewpoint(latitude = 45.5266, longitude = -122.6219, scale = 6000.0)
// Create a feature layer from the feature table and add it to the map.
operationalLayers.add(element = FeatureLayer.createWithFeatureTable(featureTable))
}
// Proxy object used to help identify the feature at the tapped coordinate
val mapViewProxy = MapViewProxy()
fun identifyFeatureAt(
screenCoordinate: ScreenCoordinate,
onFeatureSelected: (ArcGISFeature) -> Unit
) {
viewModelScope.launch {
// Identify the feature tapped at the given screen coords
val identifyLayerResults = mapViewProxy.identifyLayers(
screenCoordinate = screenCoordinate,
tolerance = 5.dp,
returnPopupsOnly = false,
maximumResults = 1
).getOrThrow()
// Identify the layer
val identifyLayerResult = identifyLayerResults.firstOrNull()
// Identify the feature
val foundFeature = identifyLayerResult?.geoElements?.firstOrNull() as? ArcGISFeature
if (foundFeature != null) {
// Animate the viewpoint to center on the feature.
foundFeature.geometry?.let { centerPoint ->
mapViewProxy.setViewpointAnimated(
viewpoint = Viewpoint(centerPoint),
duration = 300.milliseconds,
curve = AnimationCurve.EaseOutSine
)
}
// Report the selected feature to UI
onFeatureSelected(foundFeature)
}
}
}
// Helper function for simple Callout content
fun filterAttributes(attributes: Map<String, Any?>): Map<String, Any?> {
// Filter undesired feature attributes like, empty or null values and GlobalIDs.
return attributes
.filter { attribute -> attribute.value != null }
.filter { attribute -> attribute.value.toString().trim().isNotEmpty() }
.filter { attribute -> !attribute.key.contains("GlobalID") }
}
}

Note:

  • See the tutorial guide Display device location to learn how to display the current device location on a map or scene.
  • See the sample app Identify layer features to learn how to identify features in all layers in a map.
  • See the sample app Show popup to learn how to show a predefined popup from a web map.
  • See the sample app Show callout to learn how to show a callout with the latitude and longitude of user-tapped points.