Learn how to display geometries in different projections.

display projected geometries

A geometry projection transforms the vertices of a geometric shape from one coordinate system A coordinate system is a reference framework consisting of a set of points, lines, or surfaces, and a set of rules, used to define the positions of points in space in two or three dimensions. Learn more (a spatial reference A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. Learn more ) to another. For example, you can project a geographic coordinate system such as World Geodetic System 1984 (wkid=4326) to a projected coordinate system such as World Sinusoidal (wkid=54008). A spatial reference has a unique integer identifier, or well-known id (wkid), defined by GIS standards organizations.

Each projection Projection refers to a projected coordinate system based on a map projection such as transverse Mercator, Albers equal area, or Robinson, all of which project maps of the earth's spherical surface onto a 2D Cartesian coordinate plane. Learn more can maintain one of the following: area, angle, or direction. The projection you use is based on your application’s requirements. For example, if you have data centered on the North Pole, the Web Mercator (wkid=3857) spatial reference A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. Learn more is typically not used, as the pole features A feature is a single record, also known as a row, that represents a real-world entity. It typically contains a geometry (point, multipoint, polyline, or polygon) and attributes but it can also contain just attributes. Learn more are not correctly represented by the projection; there is a large area distortion. Instead, you might use the North Pole Gnomonic (wkid=102034) spatial reference because it preserves the area around the North Pole.

In this tutorial, you will access a feature service A feature service is a data service that provides access to spatial and non-spatial data in feature layers, feature layer views, and tables. Learn more that has a spatial reference of Web Mercator (wkid=3857). The layer’s polygon features will be projected on-the-fly On-the-fly reprojection is a process that takes data stored in latitude and longitude format (unprojected data) and transforms its visual representation to match other projected datasets. Learn more to the new spatial reference as they are displayed in the map view. You can select a different spatial reference from a list, and the features will be reprojected to that spatial reference. Tap a location on the map to display a point buffer and a callout that shows the point’s x/y coordinates. As you change spatial references, observe the buffer and coordinates change for that same location on the earth. Additionally, for a given spatial reference, you can tap around the map to see how the buffer distorts at different locations.

Prerequisites

Before starting this tutorial, you need the following:

  1. An ArcGIS Location Platform or ArcGIS Online account.

  2. A development and deployment environment that meets the system requirements.

  3. An IDE for Android development in Kotlin.

Develop or download

You have two options for completing this tutorial:

  1. Option 1: Develop the code or
  2. Option 2: Download the completed solution

Option 1: Develop the code

Open an Android Studio project

  1. Open the project you created by completing the Display a map tutorial.

  2. Continue with the following instructions to display geometries in different projections.

  3. Modify the old project for use in this new tutorial.

Add imports

Modify import statements to reference the packages and classes required for this tutorial.

MainScreen.kt
@file:OptIn(ExperimentalMaterial3Api::class)
package com.example.app.screens
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.ExposedDropdownMenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
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.getValue
import androidx.compose.runtime.mutableIntStateOf
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.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import com.arcgismaps.Color
import com.arcgismaps.data.ServiceFeatureTable
import com.arcgismaps.geometry.Envelope
import com.arcgismaps.geometry.GeodeticCurveType
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.LinearUnit
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.layers.FeatureLayer
import com.arcgismaps.mapping.symbology.SimpleFillSymbol
import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
import com.arcgismaps.mapping.symbology.SimpleLineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
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.toolkit.geoviewcompose.MapView
import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
import com.example.app.R
import kotlinx.coroutines.launch
import kotlin.math.round

Add utilities

Delete some unneeded code inherited from the Display a map tutorial. Then add some utilities.

  1. In MainScreen.kt, delete the body of the MainScreen() composable and the entire createMap() function.

    MainScreen.kt
    @Composable
    fun MainScreen() {
    val map = remember {
    createMap()
    }
    Scaffold(
    topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }
    ) {
    MapView(
    modifier = Modifier.fillMaxSize().padding(it),
    arcGISMap = map
    )
    }
    }
    fun createMap(): ArcGISMap {
    return ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    latitude = 34.0270,
    longitude = -118.8050,
    scale = 72000.0
    )
    }
    }
  2. At the top-level of the file, create two utilities that will be used throughout this tutorial: a function to log errors and an extension function to round Double values to five decimal places.

    Top-level in MainScreen.kt
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }

Create a graphic and a callout

A line graphic will show the full extent of the current map. A callout will indicate a point on the map and display the point’s x/y coordinates.

  1. Create a Graphic to represent the world extent An extent is a bounding rectangle with points that delineate an area for a map or scene. Learn more using dashed-lines to denote the boundaries. Then create a GraphicsOverlay and add the world boundary graphic to it. The envelope for the extent is expressed in WGS84, but the vertices will be projected on-the-fly On-the-fly reprojection is a process that takes data stored in latitude and longitude format (unprojected data) and transforms its visual representation to match other projected datasets. Learn more to the spatial reference of the map.

    Top-level in MainScreen.kt
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
  2. Create a variable with mutable state named calloutPoint. Initialize it to (10.0, -30.0), specifying the WGS84 spatial reference.

    Top-level in MainScreen.kt
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
  3. Define a composable function named CalloutContainer that adds the ArcGIS Maps SDK for Kotlin Toolkit Callout component. The CalloutContainer takes a MapViewScope parameter.

    Note that the callout will be visible when the app launches.

    Top-level in MainScreen.kt
    97 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.util.Log
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.PaddingValues
    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.ExposedDropdownMenuDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.MenuAnchorType
    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.getValue
    import androidx.compose.runtime.mutableIntStateOf
    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.res.stringResource
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.unit.TextUnit
    import androidx.compose.ui.unit.TextUnitType
    import androidx.compose.ui.unit.dp
    import com.arcgismaps.Color
    import com.arcgismaps.data.ServiceFeatureTable
    import com.arcgismaps.geometry.Envelope
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.layers.FeatureLayer
    import com.arcgismaps.mapping.symbology.SimpleFillSymbol
    import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
    import com.arcgismaps.mapping.symbology.SimpleLineSymbol
    import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
    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.toolkit.geoviewcompose.MapView
    import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
    import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
    // Show a callout using the callout point for location and text.
    @Composable
    private fun CalloutContainer(mapViewScope: MapViewScope) {
    mapViewScope.Callout(
    location = calloutPoint,
    // Optional parameters to customize the callout appearance.
    shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))
    ) {
    Column {
    Text(
    text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",
    style = MaterialTheme.typography.labelSmall
    )
    }
    }
    }
    5 collapsed lines
    @Composable
    fun MainScreen() {
    }

Create a blank map and a feature layer

In this tutorial, you will create a map A map is a collection of layers that are displayed in 2D. It is typically composed of a basemap layer and data layers. Learn more using just a spatial reference instead of using a Basemap. A feature layer A feature layer (client-side) is a data layer that can access and display features from a feature service that has the same type of geometry and attribute fields. Learn more will be then be added to the map. The World Countries (Generalized) feature service A feature service is a data service that provides access to spatial and non-spatial data in feature layers, feature layer views, and tables. Learn more used in this tutorial has been published with Web Mercator spatial reference. When displayed in the map view A map view is a user interface that displays map layers and graphics in 2D. It controls the area (extent) of the map that is visible and supports user interactions such as pan and zoom. Learn more , features are projected on-the-fly On-the-fly reprojection is a process that takes data stored in latitude and longitude format (unprojected data) and transforms its visual representation to match other projected datasets. Learn more to the spatial reference A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. Learn more of the current map.

First, create a new map.

  1. Create a mutable state ArcGISMap with the WGS84 spatial reference A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. Learn more and a white background to hide the grid. Note that the map A map is a collection of layers that are displayed in 2D. It is typically composed of a basemap layer and data layers. Learn more starts out as blank.

    Top-level in MainScreen.kt
    private var map by mutableStateOf(
    ArcGISMap(SpatialReference.wgs84()).apply {
    backgroundColor = Color.white
    }
    )
  2. Create a ServiceFeatureTable from the feature service A feature service is a data service that provides access to spatial and non-spatial data in feature layers, feature layer views, and tables. Learn more . Then create a FeatureLayer from that table.

    Top-level in MainScreen.kt
    120 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.util.Log
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.PaddingValues
    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.ExposedDropdownMenuDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.MenuAnchorType
    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.getValue
    import androidx.compose.runtime.mutableIntStateOf
    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.res.stringResource
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.unit.TextUnit
    import androidx.compose.ui.unit.TextUnitType
    import androidx.compose.ui.unit.dp
    import com.arcgismaps.Color
    import com.arcgismaps.data.ServiceFeatureTable
    import com.arcgismaps.geometry.Envelope
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.layers.FeatureLayer
    import com.arcgismaps.mapping.symbology.SimpleFillSymbol
    import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
    import com.arcgismaps.mapping.symbology.SimpleLineSymbol
    import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
    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.toolkit.geoviewcompose.MapView
    import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
    import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
    // Show a callout using the callout point for location and text.
    @Composable
    private fun CalloutContainer(mapViewScope: MapViewScope) {
    mapViewScope.Callout(
    location = calloutPoint,
    // Optional parameters to customize the callout appearance.
    shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))
    ) {
    Column {
    Text(
    text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",
    style = MaterialTheme.typography.labelSmall
    )
    }
    }
    }
    private var map by mutableStateOf(
    ArcGISMap(SpatialReference.wgs84()).apply {
    backgroundColor = Color.white
    }
    )
    private val serviceFeatureTable = ServiceFeatureTable(
    uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0"
    )
    private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)
    5 collapsed lines
    @Composable
    fun MainScreen() {
    }

Add feature layer and project callout point

When the map’s spatial reference is ready to access, the MapView invokes a callback, where you can add the feature layer to the map and project the callout point.

  1. Create the callback onSpatialReferenceChanged() as a suspend function that takes the spatial reference A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. Learn more . Then add the feature layer A feature layer (client-side) is a data layer that can access and display features from a feature service that has the same type of geometry and attribute fields. Learn more to the map’s operational layers An operational layer is a layer used by a map or a scene to visualize geographic data. Operational layers are displayed on top of a basemap layer. Learn more , and load the layer.

    Top-level in MainScreen.kt
    private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {
    map.operationalLayers.add(featureLayer)
    featureLayer.load().onFailure { error ->
    logError(error)
    }
    }
  2. Project the callout point to the spatial reference A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. Learn more .

    Top-level in MainScreen.kt
    126 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.util.Log
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.PaddingValues
    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.ExposedDropdownMenuDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.MenuAnchorType
    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.getValue
    import androidx.compose.runtime.mutableIntStateOf
    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.res.stringResource
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.unit.TextUnit
    import androidx.compose.ui.unit.TextUnitType
    import androidx.compose.ui.unit.dp
    import com.arcgismaps.Color
    import com.arcgismaps.data.ServiceFeatureTable
    import com.arcgismaps.geometry.Envelope
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.layers.FeatureLayer
    import com.arcgismaps.mapping.symbology.SimpleFillSymbol
    import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
    import com.arcgismaps.mapping.symbology.SimpleLineSymbol
    import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
    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.toolkit.geoviewcompose.MapView
    import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
    import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
    // Show a callout using the callout point for location and text.
    @Composable
    private fun CalloutContainer(mapViewScope: MapViewScope) {
    mapViewScope.Callout(
    location = calloutPoint,
    // Optional parameters to customize the callout appearance.
    shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))
    ) {
    Column {
    Text(
    text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",
    style = MaterialTheme.typography.labelSmall
    )
    }
    }
    }
    private var map by mutableStateOf(
    ArcGISMap(SpatialReference.wgs84()).apply {
    backgroundColor = Color.white
    }
    )
    private val serviceFeatureTable = ServiceFeatureTable(
    uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0"
    )
    private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)
    private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {
    map.operationalLayers.add(featureLayer)
    featureLayer.load().onFailure { error ->
    logError(error)
    }
    // Project the callout point so it has the right spatial reference when used by the callout.
    calloutPoint = GeometryEngine.projectOrNull(
    geometry = calloutPoint,
    spatialReference = newSpatialReference
    ) ?: return logError(Exception("Callout point's projection is null."))
    }
    5 collapsed lines
    @Composable
    fun MainScreen() {
    }

Enable user to select a spatial reference

The user will select a spatial reference by name from a drop-down menu to see the feature layer projected in a different spatial reference.

The projection’s name is the same as the short name of the spatial reference to which it is projecting (the output spatial reference of the projection).

Your finished app will have two drop-down menus, one above the other. The upper menu displays the projection types, such as “maintain area” or “maintain length”. The lower menu displays the projection names for that type.

  1. Create an enum that contains a constant for each projection that a user can select. Each enum constant has two properties: label and wkid.

    Top-level in MainScreen.kt
    // List of various projection names along with their respective label and wkid.
    // The name of a projection is the spatial reference it projects to.
    private enum class ProjectionName(val label: String, val wkid: Int) {
    WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),
    WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),
    WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),
    WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),
    WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),
    WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),
    NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),
    WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),
    WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),
    WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),
    WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)
    }
  2. Create a list of projection types. Each item in the list is a Pair in which the first element is the projection type and the second element is a list of projection names that have that type. Note that each projection name belongs to only one projection type.

    Top-level in MainScreen.kt
    // List of various projection types long with their respective list of spatial references.
    private val projectionTypes = listOf(
    "Equidistant (maintain length)" to listOf(
    ProjectionName.WGS84,
    ProjectionName.WorldCassini,
    ProjectionName.WorldEquidistant
    ),
    "Conformal (maintain angles)" to listOf(
    ProjectionName.WorldStereographic
    ),
    "Equal-area (maintain area)" to listOf(
    ProjectionName.WorldEckertVI,
    ProjectionName.WorldSinusoidal
    ),
    "Gnomonic (distances)" to listOf(
    ProjectionName.NorthPoleGnomonic
    ),
    "Compromise (distort all)" to listOf(
    ProjectionName.WebMercator,
    ProjectionName.WorldGallStereographic,
    ProjectionName.WorldWinkelTripel,
    ProjectionName.WorldFullerDymaxionMap
    )
    )
  3. Create a composable named ProjectionByTypeDropdownMenu that will contain the two drop-down menus.

    Define state for the drop-down menus:

    • isProjectionTypeExpanded: Boolean for whether the projection type drop-down menu is currently expanded.
    • isProjectionNameExpanded: Boolean for whether the projection name drop-down menu is currently expanded.
    • selectedProjectionTypeIndex: Index of the current selection in the projection type menu.
    • selectedProjectionNameIndex: Index of the current selection in the projection name menu.
    Top-level in MainScreen.kt
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    private fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {
    // Expanded boolean for the projection type drop-down menu.
    var isProjectionTypeExpanded by remember { mutableStateOf(false) }
    // Expanded boolean for the project name drop-down menu.
    var isProjectionNameExpanded by remember { mutableStateOf(false) }
    // The index of the current selection in project types menu.
    var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }
    // The index of the current selection in the project name menu.
    var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }
    }
  4. Inside the new ProjectionByTypeDropdownMenu composable, create the upper drop-down menu for selecting the projection type. Add an ExposedDropDownMenuBox.

    In ProjectionByTypeDropdownMenu()
    ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),
    expanded = isProjectionTypeExpanded,
    onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {
    TextField(
    label = { Text("Select a projection type") },
    value = projectionTypes[selectedProjectionTypeIndex].first,
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionTypeExpanded,
    onDismissRequest = { isProjectionTypeExpanded = false }
    ) {
    projectionTypes.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.first) },
    onClick = {
    selectedProjectionTypeIndex = index
    isProjectionTypeExpanded = false
    selectedProjectionNameIndex = 0
    onProjectionSelected(selectedOption.second[0])
    }
    )
    }
    }
    }
  5. Create the lower drop-down menu for selecting projections of a selected type. Add an ExposedDropDownMenuBox.

    In ProjectionByTypeDropdownMenu()
    224 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.util.Log
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.PaddingValues
    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.ExposedDropdownMenuDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.MenuAnchorType
    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.getValue
    import androidx.compose.runtime.mutableIntStateOf
    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.res.stringResource
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.unit.TextUnit
    import androidx.compose.ui.unit.TextUnitType
    import androidx.compose.ui.unit.dp
    import com.arcgismaps.Color
    import com.arcgismaps.data.ServiceFeatureTable
    import com.arcgismaps.geometry.Envelope
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.layers.FeatureLayer
    import com.arcgismaps.mapping.symbology.SimpleFillSymbol
    import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
    import com.arcgismaps.mapping.symbology.SimpleLineSymbol
    import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
    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.toolkit.geoviewcompose.MapView
    import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
    import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
    // Show a callout using the callout point for location and text.
    @Composable
    private fun CalloutContainer(mapViewScope: MapViewScope) {
    mapViewScope.Callout(
    location = calloutPoint,
    // Optional parameters to customize the callout appearance.
    shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))
    ) {
    Column {
    Text(
    text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",
    style = MaterialTheme.typography.labelSmall
    )
    }
    }
    }
    private var map by mutableStateOf(
    ArcGISMap(SpatialReference.wgs84()).apply {
    backgroundColor = Color.white
    }
    )
    private val serviceFeatureTable = ServiceFeatureTable(
    uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0"
    )
    private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)
    private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {
    map.operationalLayers.add(featureLayer)
    featureLayer.load().onFailure { error ->
    logError(error)
    }
    // Project the callout point so it has the right spatial reference when used by the callout.
    calloutPoint = GeometryEngine.projectOrNull(
    geometry = calloutPoint,
    spatialReference = newSpatialReference
    ) ?: return logError(Exception("Callout point's projection is null."))
    }
    // List of various projection names along with their respective label and wkid.
    // The name of a projection is the spatial reference it projects to.
    private enum class ProjectionName(val label: String, val wkid: Int) {
    WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),
    WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),
    WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),
    WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),
    WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),
    WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),
    NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),
    WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),
    WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),
    WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),
    WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)
    }
    // List of various projection types long with their respective list of spatial references.
    private val projectionTypes = listOf(
    "Equidistant (maintain length)" to listOf(
    ProjectionName.WGS84,
    ProjectionName.WorldCassini,
    ProjectionName.WorldEquidistant
    ),
    "Conformal (maintain angles)" to listOf(
    ProjectionName.WorldStereographic
    ),
    "Equal-area (maintain area)" to listOf(
    ProjectionName.WorldEckertVI,
    ProjectionName.WorldSinusoidal
    ),
    "Gnomonic (distances)" to listOf(
    ProjectionName.NorthPoleGnomonic
    ),
    "Compromise (distort all)" to listOf(
    ProjectionName.WebMercator,
    ProjectionName.WorldGallStereographic,
    ProjectionName.WorldWinkelTripel,
    ProjectionName.WorldFullerDymaxionMap
    )
    )
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    private fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {
    // Expanded boolean for the projection type drop-down menu.
    var isProjectionTypeExpanded by remember { mutableStateOf(false) }
    // Expanded boolean for the project name drop-down menu.
    var isProjectionNameExpanded by remember { mutableStateOf(false) }
    // The index of the current selection in project types menu.
    var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }
    // The index of the current selection in the project name menu.
    var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }
    ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),
    expanded = isProjectionTypeExpanded,
    onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {
    TextField(
    label = { Text("Select a projection type") },
    value = projectionTypes[selectedProjectionTypeIndex].first,
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionTypeExpanded,
    onDismissRequest = { isProjectionTypeExpanded = false }
    ) {
    projectionTypes.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.first) },
    onClick = {
    selectedProjectionTypeIndex = index
    isProjectionTypeExpanded = false
    selectedProjectionNameIndex = 0
    onProjectionSelected(selectedOption.second[0])
    }
    )
    }
    }
    }
    ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,
    onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {
    TextField(
    textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),
    value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,
    label = {
    Text(
    text = "Select spacial reference using projection type",
    fontSize = TextUnit(10f, TextUnitType.Sp)
    )
    },
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionNameExpanded,
    onDismissRequest = { isProjectionNameExpanded = false }
    ) {
    projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.label) },
    onClick = {
    selectedProjectionNameIndex = index
    isProjectionNameExpanded = false
    onProjectionSelected(selectedOption)
    }
    )
    }
    }
    }
    7 collapsed lines
    }
    @Composable
    fun MainScreen() {
    }

Change the spatial reference

To display the feature layer in a new spatial reference, you create a new SpatialReference using the selected projection name, and then create a new map A map is a collection of layers that are displayed in 2D. It is typically composed of a basemap layer and data layers. Learn more with that spatial reference.

  1. Create a suspend function named changeSpatialReference() that takes the ProjectionName selected by the user. Create a new spatial reference A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. Learn more , using the wkid of the selected option. If the new spatial reference is the same as the current one, the user has selected the same projection name in the drop-down menu. Return without creating a new map.

    Top-level in MainScreen.kt
    private suspend fun changeSpatialReference(selectedOption: ProjectionName) {
    val newSpatialReference = SpatialReference(selectedOption.wkid)
    // If user clicked on the same spatial reference then, don't create a new map.
    if (newSpatialReference == map.spatialReference) return
    }
  2. Remove the feature layer A feature layer (client-side) is a data layer that can access and display features from a feature service that has the same type of geometry and attribute fields. Learn more from the operational layers An operational layer is a layer used by a map or a scene to visualize geographic data. Operational layers are displayed on top of a basemap layer. Learn more of the current map A map is a collection of layers that are displayed in 2D. It is typically composed of a basemap layer and data layers. Learn more so the layer is no longer owned by that map. Then create a new ArcGISMap using the new spatial reference A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. Learn more , and load the map.

    Top-level in MainScreen.kt
    private suspend fun changeSpatialReference(selectedOption: ProjectionName) {
    val newSpatialReference = SpatialReference(selectedOption.wkid)
    // If user clicked on the same spatial reference then, don't create a new map.
    if (newSpatialReference == map.spatialReference) return
    // Remove the feature layer on the old map so the layer is free to be used on the new map.
    map.operationalLayers.remove(featureLayer)
    map = ArcGISMap(newSpatialReference).apply {
    backgroundColor = Color.white
    }
    map.load().onFailure { error -> logError(error) }
    }

When the map’s spatial reference is ready to be accessed, the onSpatialReferenceChanged() you created in an earlier step will be automatically invoked.

Create buffer graphics

You will need graphics to display a geodetic buffer and its point A point is a type of geometry containing a single set of x,y coordinates and a spatial reference. Learn more .

  1. Create graphics A graphic is a visual element composed of a geometry, symbol, and attributes that is displayed on a map or scene. Learn more to display a point and a geodetic buffer around the point. Then create a GraphicsOverlay and add the graphics to it.

    Top-level in MainScreen.kt
    275 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.util.Log
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.PaddingValues
    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.ExposedDropdownMenuDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.MenuAnchorType
    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.getValue
    import androidx.compose.runtime.mutableIntStateOf
    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.res.stringResource
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.unit.TextUnit
    import androidx.compose.ui.unit.TextUnitType
    import androidx.compose.ui.unit.dp
    import com.arcgismaps.Color
    import com.arcgismaps.data.ServiceFeatureTable
    import com.arcgismaps.geometry.Envelope
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.layers.FeatureLayer
    import com.arcgismaps.mapping.symbology.SimpleFillSymbol
    import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
    import com.arcgismaps.mapping.symbology.SimpleLineSymbol
    import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
    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.toolkit.geoviewcompose.MapView
    import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
    import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
    // Show a callout using the callout point for location and text.
    @Composable
    private fun CalloutContainer(mapViewScope: MapViewScope) {
    mapViewScope.Callout(
    location = calloutPoint,
    // Optional parameters to customize the callout appearance.
    shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))
    ) {
    Column {
    Text(
    text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",
    style = MaterialTheme.typography.labelSmall
    )
    }
    }
    }
    private var map by mutableStateOf(
    ArcGISMap(SpatialReference.wgs84()).apply {
    backgroundColor = Color.white
    }
    )
    private val serviceFeatureTable = ServiceFeatureTable(
    uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0"
    )
    private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)
    private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {
    map.operationalLayers.add(featureLayer)
    featureLayer.load().onFailure { error ->
    logError(error)
    }
    // Project the callout point so it has the right spatial reference when used by the callout.
    calloutPoint = GeometryEngine.projectOrNull(
    geometry = calloutPoint,
    spatialReference = newSpatialReference
    ) ?: return logError(Exception("Callout point's projection is null."))
    }
    // List of various projection names along with their respective label and wkid.
    // The name of a projection is the spatial reference it projects to.
    private enum class ProjectionName(val label: String, val wkid: Int) {
    WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),
    WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),
    WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),
    WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),
    WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),
    WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),
    NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),
    WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),
    WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),
    WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),
    WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)
    }
    // List of various projection types long with their respective list of spatial references.
    private val projectionTypes = listOf(
    "Equidistant (maintain length)" to listOf(
    ProjectionName.WGS84,
    ProjectionName.WorldCassini,
    ProjectionName.WorldEquidistant
    ),
    "Conformal (maintain angles)" to listOf(
    ProjectionName.WorldStereographic
    ),
    "Equal-area (maintain area)" to listOf(
    ProjectionName.WorldEckertVI,
    ProjectionName.WorldSinusoidal
    ),
    "Gnomonic (distances)" to listOf(
    ProjectionName.NorthPoleGnomonic
    ),
    "Compromise (distort all)" to listOf(
    ProjectionName.WebMercator,
    ProjectionName.WorldGallStereographic,
    ProjectionName.WorldWinkelTripel,
    ProjectionName.WorldFullerDymaxionMap
    )
    )
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    private fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {
    // Expanded boolean for the projection type drop-down menu.
    var isProjectionTypeExpanded by remember { mutableStateOf(false) }
    // Expanded boolean for the project name drop-down menu.
    var isProjectionNameExpanded by remember { mutableStateOf(false) }
    // The index of the current selection in project types menu.
    var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }
    // The index of the current selection in the project name menu.
    var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }
    ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),
    expanded = isProjectionTypeExpanded,
    onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {
    TextField(
    label = { Text("Select a projection type") },
    value = projectionTypes[selectedProjectionTypeIndex].first,
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionTypeExpanded,
    onDismissRequest = { isProjectionTypeExpanded = false }
    ) {
    projectionTypes.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.first) },
    onClick = {
    selectedProjectionTypeIndex = index
    isProjectionTypeExpanded = false
    selectedProjectionNameIndex = 0
    onProjectionSelected(selectedOption.second[0])
    }
    )
    }
    }
    }
    ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,
    onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {
    TextField(
    textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),
    value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,
    label = {
    Text(
    text = "Select spacial reference using projection type",
    fontSize = TextUnit(10f, TextUnitType.Sp)
    )
    },
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionNameExpanded,
    onDismissRequest = { isProjectionNameExpanded = false }
    ) {
    projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.label) },
    onClick = {
    selectedProjectionNameIndex = index
    isProjectionNameExpanded = false
    onProjectionSelected(selectedOption)
    }
    )
    }
    }
    }
    }
    private suspend fun changeSpatialReference(selectedOption: ProjectionName) {
    val newSpatialReference = SpatialReference(selectedOption.wkid)
    // If user clicked on the same spatial reference then, don't create a new map.
    if (newSpatialReference == map.spatialReference) return
    // Remove the feature layer on the old map so the layer is free to be used on the new map.
    map.operationalLayers.remove(featureLayer)
    map = ArcGISMap(newSpatialReference).apply {
    backgroundColor = Color.white
    }
    map.load().onFailure { error -> logError(error) }
    }
    private var bufferPointGraphic = Graphic(
    geometry = null,
    symbol = SimpleMarkerSymbol(
    style = SimpleMarkerSymbolStyle.Circle,
    color = Color.red,
    size = 5f
    ).apply {
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dot,
    color = Color.white,
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    }
    )
    var bufferGraphic = Graphic(
    geometry = null, symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dash,
    color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    )
    )
    private val bufferGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic))
    )
    5 collapsed lines
    @Composable
    fun MainScreen() {
    }

Visualize spatial distortion effects

When you tap on the map, the buffer graphics display at the tapped point, and the Callout moves to that location. As you tap around the map, the buffer becomes distorted in size and/or shape. The distortions vary, depending on the current spatial reference.

The screen tap results in a single-tap confirmed event, whose mapPoint property returns the tapped point in the spatial reference of the map. Use the map point to create a point buffer and trigger the callout.

  1. Create a function that takes a single-tap confirmed event.

    Get the map point from the event and call GeometryEngine.bufferGeodeticOrNull() with the map point, a distance of 1000 kilometers, and curve type of geodesic. Next, display the graphics by assigning the buffer as the geometry of the buffer graphic, and the map point as the geometry of the buffer point graphic.

    Top-level in MainScreen.kt
    private fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
    val mapPoint = singleTapConfirmedEvent.mapPoint
    ?: return logError(Exception("Tap event has no map point."))
    val buffer = GeometryEngine.bufferGeodeticOrNull(
    geometry = mapPoint,
    distance = 1000.0,
    distanceUnit = LinearUnit.kilometers,
    maxDeviation = Double.NaN,
    curveType = GeodeticCurveType.Geodesic
    ) ?: return logError(Exception("Failed to create a buffer from the map point"))
    bufferGraphic.geometry = buffer
    bufferPointGraphic.geometry = mapPoint
    }
  2. Assign the map point to calloutPoint. In response, the Callout will display at the map point, showing the point’s x/y coordinates in the current spatial reference.

    Top-level in MainScreen.kt
    320 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.util.Log
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.PaddingValues
    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.ExposedDropdownMenuDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.MenuAnchorType
    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.getValue
    import androidx.compose.runtime.mutableIntStateOf
    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.res.stringResource
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.unit.TextUnit
    import androidx.compose.ui.unit.TextUnitType
    import androidx.compose.ui.unit.dp
    import com.arcgismaps.Color
    import com.arcgismaps.data.ServiceFeatureTable
    import com.arcgismaps.geometry.Envelope
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.layers.FeatureLayer
    import com.arcgismaps.mapping.symbology.SimpleFillSymbol
    import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
    import com.arcgismaps.mapping.symbology.SimpleLineSymbol
    import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
    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.toolkit.geoviewcompose.MapView
    import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
    import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
    // Show a callout using the callout point for location and text.
    @Composable
    private fun CalloutContainer(mapViewScope: MapViewScope) {
    mapViewScope.Callout(
    location = calloutPoint,
    // Optional parameters to customize the callout appearance.
    shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))
    ) {
    Column {
    Text(
    text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",
    style = MaterialTheme.typography.labelSmall
    )
    }
    }
    }
    private var map by mutableStateOf(
    ArcGISMap(SpatialReference.wgs84()).apply {
    backgroundColor = Color.white
    }
    )
    private val serviceFeatureTable = ServiceFeatureTable(
    uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0"
    )
    private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)
    private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {
    map.operationalLayers.add(featureLayer)
    featureLayer.load().onFailure { error ->
    logError(error)
    }
    // Project the callout point so it has the right spatial reference when used by the callout.
    calloutPoint = GeometryEngine.projectOrNull(
    geometry = calloutPoint,
    spatialReference = newSpatialReference
    ) ?: return logError(Exception("Callout point's projection is null."))
    }
    // List of various projection names along with their respective label and wkid.
    // The name of a projection is the spatial reference it projects to.
    private enum class ProjectionName(val label: String, val wkid: Int) {
    WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),
    WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),
    WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),
    WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),
    WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),
    WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),
    NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),
    WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),
    WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),
    WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),
    WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)
    }
    // List of various projection types long with their respective list of spatial references.
    private val projectionTypes = listOf(
    "Equidistant (maintain length)" to listOf(
    ProjectionName.WGS84,
    ProjectionName.WorldCassini,
    ProjectionName.WorldEquidistant
    ),
    "Conformal (maintain angles)" to listOf(
    ProjectionName.WorldStereographic
    ),
    "Equal-area (maintain area)" to listOf(
    ProjectionName.WorldEckertVI,
    ProjectionName.WorldSinusoidal
    ),
    "Gnomonic (distances)" to listOf(
    ProjectionName.NorthPoleGnomonic
    ),
    "Compromise (distort all)" to listOf(
    ProjectionName.WebMercator,
    ProjectionName.WorldGallStereographic,
    ProjectionName.WorldWinkelTripel,
    ProjectionName.WorldFullerDymaxionMap
    )
    )
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    private fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {
    // Expanded boolean for the projection type drop-down menu.
    var isProjectionTypeExpanded by remember { mutableStateOf(false) }
    // Expanded boolean for the project name drop-down menu.
    var isProjectionNameExpanded by remember { mutableStateOf(false) }
    // The index of the current selection in project types menu.
    var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }
    // The index of the current selection in the project name menu.
    var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }
    ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),
    expanded = isProjectionTypeExpanded,
    onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {
    TextField(
    label = { Text("Select a projection type") },
    value = projectionTypes[selectedProjectionTypeIndex].first,
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionTypeExpanded,
    onDismissRequest = { isProjectionTypeExpanded = false }
    ) {
    projectionTypes.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.first) },
    onClick = {
    selectedProjectionTypeIndex = index
    isProjectionTypeExpanded = false
    selectedProjectionNameIndex = 0
    onProjectionSelected(selectedOption.second[0])
    }
    )
    }
    }
    }
    ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,
    onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {
    TextField(
    textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),
    value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,
    label = {
    Text(
    text = "Select spacial reference using projection type",
    fontSize = TextUnit(10f, TextUnitType.Sp)
    )
    },
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionNameExpanded,
    onDismissRequest = { isProjectionNameExpanded = false }
    ) {
    projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.label) },
    onClick = {
    selectedProjectionNameIndex = index
    isProjectionNameExpanded = false
    onProjectionSelected(selectedOption)
    }
    )
    }
    }
    }
    }
    private suspend fun changeSpatialReference(selectedOption: ProjectionName) {
    val newSpatialReference = SpatialReference(selectedOption.wkid)
    // If user clicked on the same spatial reference then, don't create a new map.
    if (newSpatialReference == map.spatialReference) return
    // Remove the feature layer on the old map so the layer is free to be used on the new map.
    map.operationalLayers.remove(featureLayer)
    map = ArcGISMap(newSpatialReference).apply {
    backgroundColor = Color.white
    }
    map.load().onFailure { error -> logError(error) }
    }
    private var bufferPointGraphic = Graphic(
    geometry = null,
    symbol = SimpleMarkerSymbol(
    style = SimpleMarkerSymbolStyle.Circle,
    color = Color.red,
    size = 5f
    ).apply {
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dot,
    color = Color.white,
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    }
    )
    var bufferGraphic = Graphic(
    geometry = null, symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dash,
    color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    )
    )
    private val bufferGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic))
    )
    private fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
    val mapPoint = singleTapConfirmedEvent.mapPoint
    ?: return logError(Exception("Tap event has no map point."))
    val buffer = GeometryEngine.bufferGeodeticOrNull(
    geometry = mapPoint,
    distance = 1000.0,
    distanceUnit = LinearUnit.kilometers,
    maxDeviation = Double.NaN,
    curveType = GeodeticCurveType.Geodesic
    ) ?: return logError(Exception("Failed to create a buffer from the map point"))
    bufferGraphic.geometry = buffer
    bufferPointGraphic.geometry = mapPoint
    calloutPoint = mapPoint
    7 collapsed lines
    }
    @Composable
    fun MainScreen() {
    }

Connect your functionality to the map view

You will now connect your functionality to the MapView composable. Pass additional parameters to the MapView so it can display UI for your top-level functions and properties.

  1. In the MainScreen composable, create a local variable named coroutineScope.

    In MainScreen() composable
    327 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.util.Log
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.PaddingValues
    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.ExposedDropdownMenuDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.MenuAnchorType
    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.getValue
    import androidx.compose.runtime.mutableIntStateOf
    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.res.stringResource
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.unit.TextUnit
    import androidx.compose.ui.unit.TextUnitType
    import androidx.compose.ui.unit.dp
    import com.arcgismaps.Color
    import com.arcgismaps.data.ServiceFeatureTable
    import com.arcgismaps.geometry.Envelope
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.layers.FeatureLayer
    import com.arcgismaps.mapping.symbology.SimpleFillSymbol
    import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
    import com.arcgismaps.mapping.symbology.SimpleLineSymbol
    import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
    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.toolkit.geoviewcompose.MapView
    import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
    import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
    // Show a callout using the callout point for location and text.
    @Composable
    private fun CalloutContainer(mapViewScope: MapViewScope) {
    mapViewScope.Callout(
    location = calloutPoint,
    // Optional parameters to customize the callout appearance.
    shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))
    ) {
    Column {
    Text(
    text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",
    style = MaterialTheme.typography.labelSmall
    )
    }
    }
    }
    private var map by mutableStateOf(
    ArcGISMap(SpatialReference.wgs84()).apply {
    backgroundColor = Color.white
    }
    )
    private val serviceFeatureTable = ServiceFeatureTable(
    uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0"
    )
    private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)
    private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {
    map.operationalLayers.add(featureLayer)
    featureLayer.load().onFailure { error ->
    logError(error)
    }
    // Project the callout point so it has the right spatial reference when used by the callout.
    calloutPoint = GeometryEngine.projectOrNull(
    geometry = calloutPoint,
    spatialReference = newSpatialReference
    ) ?: return logError(Exception("Callout point's projection is null."))
    }
    // List of various projection names along with their respective label and wkid.
    // The name of a projection is the spatial reference it projects to.
    private enum class ProjectionName(val label: String, val wkid: Int) {
    WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),
    WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),
    WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),
    WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),
    WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),
    WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),
    NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),
    WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),
    WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),
    WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),
    WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)
    }
    // List of various projection types long with their respective list of spatial references.
    private val projectionTypes = listOf(
    "Equidistant (maintain length)" to listOf(
    ProjectionName.WGS84,
    ProjectionName.WorldCassini,
    ProjectionName.WorldEquidistant
    ),
    "Conformal (maintain angles)" to listOf(
    ProjectionName.WorldStereographic
    ),
    "Equal-area (maintain area)" to listOf(
    ProjectionName.WorldEckertVI,
    ProjectionName.WorldSinusoidal
    ),
    "Gnomonic (distances)" to listOf(
    ProjectionName.NorthPoleGnomonic
    ),
    "Compromise (distort all)" to listOf(
    ProjectionName.WebMercator,
    ProjectionName.WorldGallStereographic,
    ProjectionName.WorldWinkelTripel,
    ProjectionName.WorldFullerDymaxionMap
    )
    )
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    private fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {
    // Expanded boolean for the projection type drop-down menu.
    var isProjectionTypeExpanded by remember { mutableStateOf(false) }
    // Expanded boolean for the project name drop-down menu.
    var isProjectionNameExpanded by remember { mutableStateOf(false) }
    // The index of the current selection in project types menu.
    var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }
    // The index of the current selection in the project name menu.
    var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }
    ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),
    expanded = isProjectionTypeExpanded,
    onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {
    TextField(
    label = { Text("Select a projection type") },
    value = projectionTypes[selectedProjectionTypeIndex].first,
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionTypeExpanded,
    onDismissRequest = { isProjectionTypeExpanded = false }
    ) {
    projectionTypes.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.first) },
    onClick = {
    selectedProjectionTypeIndex = index
    isProjectionTypeExpanded = false
    selectedProjectionNameIndex = 0
    onProjectionSelected(selectedOption.second[0])
    }
    )
    }
    }
    }
    ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,
    onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {
    TextField(
    textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),
    value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,
    label = {
    Text(
    text = "Select spacial reference using projection type",
    fontSize = TextUnit(10f, TextUnitType.Sp)
    )
    },
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionNameExpanded,
    onDismissRequest = { isProjectionNameExpanded = false }
    ) {
    projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.label) },
    onClick = {
    selectedProjectionNameIndex = index
    isProjectionNameExpanded = false
    onProjectionSelected(selectedOption)
    }
    )
    }
    }
    }
    }
    private suspend fun changeSpatialReference(selectedOption: ProjectionName) {
    val newSpatialReference = SpatialReference(selectedOption.wkid)
    // If user clicked on the same spatial reference then, don't create a new map.
    if (newSpatialReference == map.spatialReference) return
    // Remove the feature layer on the old map so the layer is free to be used on the new map.
    map.operationalLayers.remove(featureLayer)
    map = ArcGISMap(newSpatialReference).apply {
    backgroundColor = Color.white
    }
    map.load().onFailure { error -> logError(error) }
    }
    private var bufferPointGraphic = Graphic(
    geometry = null,
    symbol = SimpleMarkerSymbol(
    style = SimpleMarkerSymbolStyle.Circle,
    color = Color.red,
    size = 5f
    ).apply {
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dot,
    color = Color.white,
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    }
    )
    var bufferGraphic = Graphic(
    geometry = null, symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dash,
    color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    )
    )
    private val bufferGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic))
    )
    private fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
    val mapPoint = singleTapConfirmedEvent.mapPoint
    ?: return logError(Exception("Tap event has no map point."))
    val buffer = GeometryEngine.bufferGeodeticOrNull(
    geometry = mapPoint,
    distance = 1000.0,
    distanceUnit = LinearUnit.kilometers,
    maxDeviation = Double.NaN,
    curveType = GeodeticCurveType.Geodesic
    ) ?: return logError(Exception("Failed to create a buffer from the map point"))
    bufferGraphic.geometry = buffer
    bufferPointGraphic.geometry = mapPoint
    calloutPoint = mapPoint
    }
    @Composable
    fun MainScreen() {
    val coroutineScope = rememberCoroutineScope()
    }
  2. Add a Scaffold with the following code, which adds a Column and the MapView.

    Note that in the Column’s content, you call the ProjectionByTypeDropdownMenu() composable, passing in a lambda. The lambda takes a dropdownSelection: ProjectionName parameter and calls changeSpatialReference() with it.

    In MainScreen() composable
    @Composable
    fun MainScreen() {
    val coroutineScope = rememberCoroutineScope()
    Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { innerPadding ->
    Column(
    modifier = Modifier.padding(innerPadding),
    verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    ProjectionByTypeDropdownMenu(
    onProjectionSelected = { dropdownSelection ->
    coroutineScope.launch { changeSpatialReference(dropdownSelection) }
    }
    )
    MapView(
    modifier = Modifier
    .fillMaxSize()
    .weight(1f),
    arcGISMap = map,
    ) {
    }
    }
    }
    }
  3. In the MapView call, pass the following additional parameters:

    • A list of the two graphics overlays A graphics overlay is a client-side, temporary container of graphics to display on a map view or scene view. Learn more .
    • A lambda that invokes your onSpatialReferenceChanged() callback, if the new spatial reference A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. Learn more is not null.
    • A function reference to your createBuffer() callback that handles the single tap confirmed event.
    In MainScreen() composable
    344 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.util.Log
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.PaddingValues
    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.ExposedDropdownMenuDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.MenuAnchorType
    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.getValue
    import androidx.compose.runtime.mutableIntStateOf
    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.res.stringResource
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.unit.TextUnit
    import androidx.compose.ui.unit.TextUnitType
    import androidx.compose.ui.unit.dp
    import com.arcgismaps.Color
    import com.arcgismaps.data.ServiceFeatureTable
    import com.arcgismaps.geometry.Envelope
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.layers.FeatureLayer
    import com.arcgismaps.mapping.symbology.SimpleFillSymbol
    import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
    import com.arcgismaps.mapping.symbology.SimpleLineSymbol
    import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
    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.toolkit.geoviewcompose.MapView
    import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
    import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
    // Show a callout using the callout point for location and text.
    @Composable
    private fun CalloutContainer(mapViewScope: MapViewScope) {
    mapViewScope.Callout(
    location = calloutPoint,
    // Optional parameters to customize the callout appearance.
    shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))
    ) {
    Column {
    Text(
    text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",
    style = MaterialTheme.typography.labelSmall
    )
    }
    }
    }
    private var map by mutableStateOf(
    ArcGISMap(SpatialReference.wgs84()).apply {
    backgroundColor = Color.white
    }
    )
    private val serviceFeatureTable = ServiceFeatureTable(
    uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0"
    )
    private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)
    private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {
    map.operationalLayers.add(featureLayer)
    featureLayer.load().onFailure { error ->
    logError(error)
    }
    // Project the callout point so it has the right spatial reference when used by the callout.
    calloutPoint = GeometryEngine.projectOrNull(
    geometry = calloutPoint,
    spatialReference = newSpatialReference
    ) ?: return logError(Exception("Callout point's projection is null."))
    }
    // List of various projection names along with their respective label and wkid.
    // The name of a projection is the spatial reference it projects to.
    private enum class ProjectionName(val label: String, val wkid: Int) {
    WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),
    WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),
    WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),
    WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),
    WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),
    WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),
    NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),
    WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),
    WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),
    WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),
    WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)
    }
    // List of various projection types long with their respective list of spatial references.
    private val projectionTypes = listOf(
    "Equidistant (maintain length)" to listOf(
    ProjectionName.WGS84,
    ProjectionName.WorldCassini,
    ProjectionName.WorldEquidistant
    ),
    "Conformal (maintain angles)" to listOf(
    ProjectionName.WorldStereographic
    ),
    "Equal-area (maintain area)" to listOf(
    ProjectionName.WorldEckertVI,
    ProjectionName.WorldSinusoidal
    ),
    "Gnomonic (distances)" to listOf(
    ProjectionName.NorthPoleGnomonic
    ),
    "Compromise (distort all)" to listOf(
    ProjectionName.WebMercator,
    ProjectionName.WorldGallStereographic,
    ProjectionName.WorldWinkelTripel,
    ProjectionName.WorldFullerDymaxionMap
    )
    )
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    private fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {
    // Expanded boolean for the projection type drop-down menu.
    var isProjectionTypeExpanded by remember { mutableStateOf(false) }
    // Expanded boolean for the project name drop-down menu.
    var isProjectionNameExpanded by remember { mutableStateOf(false) }
    // The index of the current selection in project types menu.
    var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }
    // The index of the current selection in the project name menu.
    var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }
    ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),
    expanded = isProjectionTypeExpanded,
    onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {
    TextField(
    label = { Text("Select a projection type") },
    value = projectionTypes[selectedProjectionTypeIndex].first,
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionTypeExpanded,
    onDismissRequest = { isProjectionTypeExpanded = false }
    ) {
    projectionTypes.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.first) },
    onClick = {
    selectedProjectionTypeIndex = index
    isProjectionTypeExpanded = false
    selectedProjectionNameIndex = 0
    onProjectionSelected(selectedOption.second[0])
    }
    )
    }
    }
    }
    ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,
    onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {
    TextField(
    textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),
    value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,
    label = {
    Text(
    text = "Select spacial reference using projection type",
    fontSize = TextUnit(10f, TextUnitType.Sp)
    )
    },
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionNameExpanded,
    onDismissRequest = { isProjectionNameExpanded = false }
    ) {
    projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.label) },
    onClick = {
    selectedProjectionNameIndex = index
    isProjectionNameExpanded = false
    onProjectionSelected(selectedOption)
    }
    )
    }
    }
    }
    }
    private suspend fun changeSpatialReference(selectedOption: ProjectionName) {
    val newSpatialReference = SpatialReference(selectedOption.wkid)
    // If user clicked on the same spatial reference then, don't create a new map.
    if (newSpatialReference == map.spatialReference) return
    // Remove the feature layer on the old map so the layer is free to be used on the new map.
    map.operationalLayers.remove(featureLayer)
    map = ArcGISMap(newSpatialReference).apply {
    backgroundColor = Color.white
    }
    map.load().onFailure { error -> logError(error) }
    }
    private var bufferPointGraphic = Graphic(
    geometry = null,
    symbol = SimpleMarkerSymbol(
    style = SimpleMarkerSymbolStyle.Circle,
    color = Color.red,
    size = 5f
    ).apply {
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dot,
    color = Color.white,
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    }
    )
    var bufferGraphic = Graphic(
    geometry = null, symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dash,
    color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    )
    )
    private val bufferGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic))
    )
    private fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
    val mapPoint = singleTapConfirmedEvent.mapPoint
    ?: return logError(Exception("Tap event has no map point."))
    val buffer = GeometryEngine.bufferGeodeticOrNull(
    geometry = mapPoint,
    distance = 1000.0,
    distanceUnit = LinearUnit.kilometers,
    maxDeviation = Double.NaN,
    curveType = GeodeticCurveType.Geodesic
    ) ?: return logError(Exception("Failed to create a buffer from the map point"))
    bufferGraphic.geometry = buffer
    bufferPointGraphic.geometry = mapPoint
    calloutPoint = mapPoint
    }
    @Composable
    fun MainScreen() {
    val coroutineScope = rememberCoroutineScope()
    Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { innerPadding ->
    Column(
    modifier = Modifier.padding(innerPadding),
    verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    ProjectionByTypeDropdownMenu(
    onProjectionSelected = { dropdownSelection ->
    coroutineScope.launch { changeSpatialReference(dropdownSelection) }
    }
    )
    MapView(
    modifier = Modifier
    .fillMaxSize()
    .weight(1f),
    arcGISMap = map,
    graphicsOverlays = listOf(boundaryGraphicsOverlay, bufferGraphicsOverlay),
    onSpatialReferenceChanged = { newSpatialReference ->
    newSpatialReference?.let {
    coroutineScope.launch {
    onSpatialReferenceChanged(newSpatialReference)
    }
    }
    },
    onSingleTapConfirmed = ::createBuffer
    ) {
    }
    6 collapsed lines
    }
    }
    }
  4. In MapView‘s content lambda, call CalloutContainer, passing in this, which is a reference to the MapViewScope.

    In MainScreen() composable
    359 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.util.Log
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.PaddingValues
    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.ExposedDropdownMenuDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.MenuAnchorType
    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.getValue
    import androidx.compose.runtime.mutableIntStateOf
    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.res.stringResource
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.unit.TextUnit
    import androidx.compose.ui.unit.TextUnitType
    import androidx.compose.ui.unit.dp
    import com.arcgismaps.Color
    import com.arcgismaps.data.ServiceFeatureTable
    import com.arcgismaps.geometry.Envelope
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.layers.FeatureLayer
    import com.arcgismaps.mapping.symbology.SimpleFillSymbol
    import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
    import com.arcgismaps.mapping.symbology.SimpleLineSymbol
    import com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyle
    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.toolkit.geoviewcompose.MapView
    import com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaults
    import com.arcgismaps.toolkit.geoviewcompose.MapViewScope
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    private fun logError(error: Throwable) {
    Log.e("MainScreen.kt", error.message.toString(), error.cause)
    }
    private fun Double.roundToFiveDecimals(): Double {
    return round(this * 100000.0) / 100000.0
    }
    private val worldBoundaryGraphic = Graphic(
    symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.transparent,
    outline = SimpleLineSymbol().apply {
    color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)
    width = 0.5f
    style = SimpleLineSymbolStyle.Dash
    }
    ),
    // The envelope covers the world extent of the map.
    geometry = Envelope(
    xMin = -180.0,
    yMin = -90.0,
    xMax = 180.0,
    yMax = 90.0,
    spatialReference = SpatialReference.wgs84()
    )
    )
    private val boundaryGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))
    )
    // Keep track of the state of the callout point.
    private var calloutPoint: Point by mutableStateOf(
    Point(x = -10.0, y = 30.0, SpatialReference.wgs84())
    )
    // Show a callout using the callout point for location and text.
    @Composable
    private fun CalloutContainer(mapViewScope: MapViewScope) {
    mapViewScope.Callout(
    location = calloutPoint,
    // Optional parameters to customize the callout appearance.
    shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))
    ) {
    Column {
    Text(
    text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",
    style = MaterialTheme.typography.labelSmall
    )
    }
    }
    }
    private var map by mutableStateOf(
    ArcGISMap(SpatialReference.wgs84()).apply {
    backgroundColor = Color.white
    }
    )
    private val serviceFeatureTable = ServiceFeatureTable(
    uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0"
    )
    private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)
    private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {
    map.operationalLayers.add(featureLayer)
    featureLayer.load().onFailure { error ->
    logError(error)
    }
    // Project the callout point so it has the right spatial reference when used by the callout.
    calloutPoint = GeometryEngine.projectOrNull(
    geometry = calloutPoint,
    spatialReference = newSpatialReference
    ) ?: return logError(Exception("Callout point's projection is null."))
    }
    // List of various projection names along with their respective label and wkid.
    // The name of a projection is the spatial reference it projects to.
    private enum class ProjectionName(val label: String, val wkid: Int) {
    WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),
    WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),
    WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),
    WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),
    WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),
    WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),
    NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),
    WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),
    WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),
    WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),
    WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)
    }
    // List of various projection types long with their respective list of spatial references.
    private val projectionTypes = listOf(
    "Equidistant (maintain length)" to listOf(
    ProjectionName.WGS84,
    ProjectionName.WorldCassini,
    ProjectionName.WorldEquidistant
    ),
    "Conformal (maintain angles)" to listOf(
    ProjectionName.WorldStereographic
    ),
    "Equal-area (maintain area)" to listOf(
    ProjectionName.WorldEckertVI,
    ProjectionName.WorldSinusoidal
    ),
    "Gnomonic (distances)" to listOf(
    ProjectionName.NorthPoleGnomonic
    ),
    "Compromise (distort all)" to listOf(
    ProjectionName.WebMercator,
    ProjectionName.WorldGallStereographic,
    ProjectionName.WorldWinkelTripel,
    ProjectionName.WorldFullerDymaxionMap
    )
    )
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    private fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {
    // Expanded boolean for the projection type drop-down menu.
    var isProjectionTypeExpanded by remember { mutableStateOf(false) }
    // Expanded boolean for the project name drop-down menu.
    var isProjectionNameExpanded by remember { mutableStateOf(false) }
    // The index of the current selection in project types menu.
    var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }
    // The index of the current selection in the project name menu.
    var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }
    ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),
    expanded = isProjectionTypeExpanded,
    onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {
    TextField(
    label = { Text("Select a projection type") },
    value = projectionTypes[selectedProjectionTypeIndex].first,
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionTypeExpanded,
    onDismissRequest = { isProjectionTypeExpanded = false }
    ) {
    projectionTypes.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.first) },
    onClick = {
    selectedProjectionTypeIndex = index
    isProjectionTypeExpanded = false
    selectedProjectionNameIndex = 0
    onProjectionSelected(selectedOption.second[0])
    }
    )
    }
    }
    }
    ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,
    onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {
    TextField(
    textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),
    value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,
    label = {
    Text(
    text = "Select spacial reference using projection type",
    fontSize = TextUnit(10f, TextUnitType.Sp)
    )
    },
    onValueChange = {},
    readOnly = true,
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor(MenuAnchorType.PrimaryNotEditable, true)
    )
    ExposedDropdownMenu(
    expanded = isProjectionNameExpanded,
    onDismissRequest = { isProjectionNameExpanded = false }
    ) {
    projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->
    DropdownMenuItem(
    text = { Text(text = selectedOption.label) },
    onClick = {
    selectedProjectionNameIndex = index
    isProjectionNameExpanded = false
    onProjectionSelected(selectedOption)
    }
    )
    }
    }
    }
    }
    private suspend fun changeSpatialReference(selectedOption: ProjectionName) {
    val newSpatialReference = SpatialReference(selectedOption.wkid)
    // If user clicked on the same spatial reference then, don't create a new map.
    if (newSpatialReference == map.spatialReference) return
    // Remove the feature layer on the old map so the layer is free to be used on the new map.
    map.operationalLayers.remove(featureLayer)
    map = ArcGISMap(newSpatialReference).apply {
    backgroundColor = Color.white
    }
    map.load().onFailure { error -> logError(error) }
    }
    private var bufferPointGraphic = Graphic(
    geometry = null,
    symbol = SimpleMarkerSymbol(
    style = SimpleMarkerSymbolStyle.Circle,
    color = Color.red,
    size = 5f
    ).apply {
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dot,
    color = Color.white,
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    }
    )
    var bufferGraphic = Graphic(
    geometry = null, symbol = SimpleFillSymbol(
    style = SimpleFillSymbolStyle.Solid,
    color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),
    outline = SimpleLineSymbol(
    style = SimpleLineSymbolStyle.Dash,
    color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),
    width = 0.5f,
    markerStyle = SimpleLineSymbolMarkerStyle.None
    )
    )
    )
    private val bufferGraphicsOverlay by mutableStateOf(
    GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic))
    )
    private fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
    val mapPoint = singleTapConfirmedEvent.mapPoint
    ?: return logError(Exception("Tap event has no map point."))
    val buffer = GeometryEngine.bufferGeodeticOrNull(
    geometry = mapPoint,
    distance = 1000.0,
    distanceUnit = LinearUnit.kilometers,
    maxDeviation = Double.NaN,
    curveType = GeodeticCurveType.Geodesic
    ) ?: return logError(Exception("Failed to create a buffer from the map point"))
    bufferGraphic.geometry = buffer
    bufferPointGraphic.geometry = mapPoint
    calloutPoint = mapPoint
    }
    @Composable
    fun MainScreen() {
    val coroutineScope = rememberCoroutineScope()
    Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { innerPadding ->
    Column(
    modifier = Modifier.padding(innerPadding),
    verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    ProjectionByTypeDropdownMenu(
    onProjectionSelected = { dropdownSelection ->
    coroutineScope.launch { changeSpatialReference(dropdownSelection) }
    }
    )
    MapView(
    modifier = Modifier
    .fillMaxSize()
    .weight(1f),
    arcGISMap = map,
    graphicsOverlays = listOf(boundaryGraphicsOverlay, bufferGraphicsOverlay),
    onSpatialReferenceChanged = { newSpatialReference ->
    newSpatialReference?.let {
    coroutineScope.launch {
    onSpatialReferenceChanged(newSpatialReference)
    }
    }
    },
    onSingleTapConfirmed = ::createBuffer
    ) {
    CalloutContainer(this)
    }
    6 collapsed lines
    }
    }
    }

Click Run > Run > app to run the app.

You should see an orange feature layer A feature layer (client-side) is a data layer that can access and display features from a feature service that has the same type of geometry and attribute fields. Learn more with no basemap A basemap is the foundational layer and data that provides the overall visual and geographic context for a map or scene. It typically includes geographic features and labels for land, water, roads, buildings, cities, places, and administrative boundaries, but can also include raster data such as satellite and areal images. Learn more . The initial projection type is Maintain length and the initial spatial reference is WGS84. You should also see a callout pointing to the location at (x = -10, y = 30).

You can interact with this app in two ways to learn about spatial references:

  • View coordinates of the same point in different projections: When the app launches, examine the callout and the point coordinates it displays. Then select other projections from the drop-down menus. You should see that same point with different coordinates for each spatial reference.

  • View spatial distortions of the buffer: In any spatial reference, tap on the map. The point buffer should display on the map, with the tapped point’s x/y coordinates showing in a callout. Tap around the map and watch how the buffer’s size and shape get distorted. Try other projections and observe how the distortions differ from one spatial reference to another.

Alternatively, you can download the tutorial solution, as follows.

Option 2: Download the solution

  1. Click the Download solution link in the right-hand side of this page.

  2. Unzip the file to a location on your machine.

  3. Run Android Studio.

  4. Go to File > Open…. Navigate to the solution folder and click Open.

    On Windows: If you are in the Welcome to Android Studio dialog, click Open and navigate to the solution folder. Then click Open.

Since the downloaded solution does not contain authentication credentials, you must first set up authentication to create credentials, and then add the developer credentials to the solution.

Set up authentication

To access the secure ArcGIS location services ArcGIS Location Services, also referred to as Location Services, are services hosted by Esri that provide geospatial functionality for developing mapping applications. They include the ArcGIS Basemap Styles service, ArcGIS Static Basemap Tiles service, ArcGIS Places service, ArcGIS Geocoding service, ArcGIS Routing service, ArcGIS GeoEnrichment service, and ArcGIS Elevation service. An ArcGIS Location Platform or ArcGIS Online account is required to use the services. Learn more used in this tutorial, you must implement API key authentication API key authentication is a type of authentication that uses an API key to authenticate requests to ArcGIS services and secure portal items. Learn more or user authentication User authentication is a type of authentication that allows users with an ArcGIS account to sign into an application and allow it to access ArcGIS content, services, and resources on their behalf. The typical authorization protocol used is OAuth2.0. Learn more using an ArcGIS Location Platform An ArcGIS Location Platform account, formerly known as an ArcGIS Developer account, is an identity associated with an ArcGIS Location Platform subscription. Learn more or an ArcGIS Online An ArcGIS Online account, also known as an ArcGIS Organization account, is an identity associated with an ArcGIS Online subscription. It can be used to access ArcGIS tools and develop applications with ArcGIS location services for an organization. Learn more account.

To complete this tutorial, click on the tab in the switcher below for your authentication type of choice, either API key authentication or User authentication.

Create a new API key access token An access token is an authorization string that provides access to secure ArcGIS content, data, and services. Its capabilities are determined by the privileges it supports. It is obtained by implementing API key authentication, User authentication, or App authentication. Learn more with privileges Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. Learn more to access the secure resources used in this tutorial.

  1. Complete the Create an API key tutorial and create an API key with the following privilege(s) Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. Learn more :

    • Privileges
      • Location services > Basemaps
  2. Copy and paste the API key access token into a safe location. It will be used in a later step.

Set developer credentials in the solution

To allow your app users to access ArcGIS location services ArcGIS Location Services, also referred to as Location Services, are services hosted by Esri that provide geospatial functionality for developing mapping applications. They include the ArcGIS Basemap Styles service, ArcGIS Static Basemap Tiles service, ArcGIS Places service, ArcGIS Geocoding service, ArcGIS Routing service, ArcGIS GeoEnrichment service, and ArcGIS Elevation service. An ArcGIS Location Platform or ArcGIS Online account is required to use the services. Learn more , use the developer credentials that you created in the Set up authentication step to authenticate requests for resources.

  1. In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the AuthenticationMode to .API_KEY.

    MainActivity.kt
    14 collapsed lines
    package com.example.app
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import com.arcgismaps.ApiKey
    import com.arcgismaps.ArcGISEnvironment
    import com.arcgismaps.httpcore.authentication.OAuthUserConfiguration
    import com.arcgismaps.toolkit.authentication.AuthenticatorState
    import com.arcgismaps.toolkit.authentication.DialogAuthenticator
    import com.example.app.screens.MainScreen
    import com.example.app.ui.theme.TutorialTheme
    class MainActivity : ComponentActivity() {
    private enum class AuthenticationMode { API_KEY, USER_AUTH }
    private val authenticationMode = AuthenticationMode.API_KEY
    42 collapsed lines
    private val authenticatorState = AuthenticatorState()
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    when (authenticationMode) {
    AuthenticationMode.API_KEY -> {
    ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN")
    }
    AuthenticationMode.USER_AUTH -> {
    authenticatorState.oAuthUserConfigurations = listOf(
    OAuthUserConfiguration(
    portalUrl = "https://www.arcgis.com",
    clientId = "YOUR_CLIENT_ID",
    redirectUrl = "YOUR_REDIRECT_URL"
    )
    )
    }
    }
    enableEdgeToEdge()
    setContent {
    TutorialTheme {
    MainScreen()
    if (authenticationMode == AuthenticationMode.USER_AUTH) {
    DialogAuthenticator(authenticatorState)
    }
    }
    }
    }
    }
  2. Set the apiKey property with your API key access token.

    MainActivity.kt
    22 collapsed lines
    package com.example.app
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import com.arcgismaps.ApiKey
    import com.arcgismaps.ArcGISEnvironment
    import com.arcgismaps.httpcore.authentication.OAuthUserConfiguration
    import com.arcgismaps.toolkit.authentication.AuthenticatorState
    import com.arcgismaps.toolkit.authentication.DialogAuthenticator
    import com.example.app.screens.MainScreen
    import com.example.app.ui.theme.TutorialTheme
    class MainActivity : ComponentActivity() {
    private enum class AuthenticationMode { API_KEY, USER_AUTH }
    private val authenticationMode = AuthenticationMode.API_KEY
    private val authenticatorState = AuthenticatorState()
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    when (authenticationMode) {
    AuthenticationMode.API_KEY -> {
    ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN")
    }
    30 collapsed lines
    AuthenticationMode.USER_AUTH -> {
    authenticatorState.oAuthUserConfigurations = listOf(
    OAuthUserConfiguration(
    portalUrl = "https://www.arcgis.com",
    clientId = "YOUR_CLIENT_ID",
    redirectUrl = "YOUR_REDIRECT_URL"
    )
    )
    }
    }
    enableEdgeToEdge()
    setContent {
    TutorialTheme {
    MainScreen()
    if (authenticationMode == AuthenticationMode.USER_AUTH) {
    DialogAuthenticator(authenticatorState)
    }
    }
    }
    }
    }

Best Practice: The access token is stored directly in the code as a convenience for this tutorial. Do not store credentials directly in source code in a production environment.

Run the app

Click Run > Run > app to run the app.

You should see an orange feature layer A feature layer (client-side) is a data layer that can access and display features from a feature service that has the same type of geometry and attribute fields. Learn more with no basemap A basemap is the foundational layer and data that provides the overall visual and geographic context for a map or scene. It typically includes geographic features and labels for land, water, roads, buildings, cities, places, and administrative boundaries, but can also include raster data such as satellite and areal images. Learn more . The initial projection type is Maintain length and the initial spatial reference is WGS84. You should also see a callout pointing to the location at (x = -10, y = 30).

You can interact with this app in two ways to learn about spatial references:

  • View coordinates of the same point in different projections: When the app launches, examine the callout and the point coordinates it displays. Then select other projections from the drop-down menus. You should see that same point with different coordinates for each spatial reference.

  • View spatial distortions of the buffer: In any spatial reference, tap on the map. The point buffer should display on the map, with the tapped point’s x/y coordinates showing in a callout. Tap around the map and watch how the buffer’s size and shape get distorted. Try other projections and observe how the distortions differ from one spatial reference to another.

What’s next?

Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: