ArcGIS Runtime SDK version 100.15 is a long-term support release focused exclusively on bug fixes and minor updates. ArcGIS Maps SDKs for Native Apps version 200.x builds on the proven architecture of 100.15, and is designed to leverage the latest developer framework innovations. This topic outlines areas of the API that have undergone changes and provides guidance for re-factoring 100.x code for a 200.x app.

Release 200.0 introduced a new Kotlin-based API for Android called ArcGIS Maps SDK for Kotlin.

This release is a full re-imagining of the ArcGIS Runtime SDK for Android 100.x as a Kotlin-first SDK, with out-of-the-box support for features like coroutines, flows, and null safety. The Java-based ArcGIS Runtime SDK for Android 100.x will receive long-term support (LTS), with ongoing bug fixes but no further feature updates.

To utilize the new features, migrate to ArcGIS Maps SDK for Kotlin 200.x, which replaces the Java-based ArcGIS Runtime SDK for Android 100.x. For more information, check out the blog ArcGIS Runtime in 2022 and beyond.

The code snippets on this page have two tabs: one demonstrating the ArcGIS Maps SDK for Kotlin 200.x patterns, and the other showing the ArcGIS Runtime SDK for Android 100.x patterns that have been replaced.

Support for Jetpack Compose

ArcGIS Maps SDK for Kotlin 200.2 and later provides full support for Jetpack Compose. Code snippets on this page demonstrate the use of the MapView composable function, which is defined, along with other composables, in the ArcGIS Maps SDK for Kotlin Tookit. For more information, see Toolkit.

If you do not wish to use Jetpack Compose, you can continue using XML layouts and the MapView and SceneView classes instead.

LifeCycleObserver

The GeoView class is a base class for MapView and SceneView, which implements the DefaultLifecycleObserver. Hence, forwarding lifecycle events like onResume, onPause, onDestroy to a GeoView is no longer required.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// set the API key
ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.YOUR_ACCESS_TOKEN)
// set up data binding for the activity
val activityMainBinding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
mapView = activityMainBinding.mapView
// add the MapView to the activity's lifecycle
lifecycle.addObserver(mapView)
}

Coroutines and Coroutines Scope

Kotlin’s suspending function is safer and less error-prone for asynchronous operations. All suspend functions return a Result, so there is no longer a need for try/catch block. Additionally, you can launch a coroutine using a lifecycle-aware coroutine scope, which will be canceled automatically if the lifecycle is destroyed.

coroutineScope.launch {
// run the suspend function to get a geodatabase
val geodatabaseResult = Geodatabase.create(filePath)
// get the result once suspend is completed
geodatabaseResult.onSuccess { geodatabase ->
// run the suspend function to create a geodatabase feature table
val featureTableResult = geodatabase.createTable(tableDescription)
// get the result once suspend is completed
featureTableResult.onSuccess { geodatabaseFeatureTable ->
setupMapFromGeodatabase(geodatabaseFeatureTable)
}.onFailure { throwable ->
showMessage(throwable.message)
}
}.onFailure { throwable ->
showMessage(throwable.message)
}
}

Events handling

All events are now represented using SharedFlow, so callbacks are no longer necessary.

val simulatedLocationDataSource = SimulatedLocationDataSource(polyline)
simulatedLocationDataSource.start()
simulatedLocationDataSource.locationChanged.collect { location ->
val locationPoint = location.position
}

Loadable

Loadable state (LoadStatus) is represented using a StateFlow, while load() is a suspending function which returns a load process complete Result<Unit>. The load error can be obtained from the Result if it fails.

private suspend fun loadPortalItem() {
val portalItem = PortalItem(Portal("https://www.arcgis.com"), itemID)
portalItem.load().onSuccess {
val featureLayer = FeatureLayer.createWithItem(portalItem)
setFeatureLayer(featureLayer, viewpoint)
}.onFailure { throwable ->
showError(throwable.message)
}
}

There’s an asynchronous way to listen to LoadStatus updates, as the Loadable.loadStatus property is now a StateFlow. This way, you can get the intermediate LoadStatus values.

ArcGIS Maps SDK for Kotlin v200.x
coroutineScope.launch {
portalItem.load()
}
coroutineScope.launch {
portalItem.loadStatus.collect { loadStatus ->
when (loadStatus) {
LoadStatus.Loaded -> {
val featureLayer = FeatureLayer.createWithItem(portalItem)
setFeatureLayer(featureLayer)
}
is LoadStatus.FailedToLoad -> {
showError("Error loading portal item: ${loadStatus.error.message}")
}
LoadStatus.Loading -> { ... }
LoadStatus.NotLoaded -> { ... }
}
}
}

Tasks and jobs

ArcGIS Maps SDK for Kotlin 200.x substantially changes the Job and task workflows. A job needs to be run in a CoroutineScope, which provides control over the flow of the progress or completion. Multiple jobs and tasks can be performed using coroutine flows; thus, you can in general avoid working with nested callbacks.

// create a new offline map task
val offlineMapTask = OfflineMapTask(map)
// set the parameters of the task
val generateOfflineMapParameters = GenerateOfflineMapParameters(
areaOfInterest = geometry,
minScale = minScale,
maxScale = maxScale
)
// create a job with the offline map parameters
val offlineMapJob = offlineMapTask.createGenerateOfflineMapJob(
parameters = generateOfflineMapParameters,
downloadDirectoryPath = offlineMapPath
)
// start the job
offlineMapJob.start()
with (coroutineScope) {
// collect the progress of the job
launch {
offlineMapJob.progress.collect {
val progressPercentage = offlineMapJob.progress.value
}
}
// display map if job succeeds
launch {
offlineMapJob.result().onSuccess { generateOfflineMapResult ->
map = generateOfflineMapResult.offlineMap
}.onFailure { throwable ->
showMessage(throwable.message.toString())
}
}
}

Geometry and geometry builders

There are several changes to the usage of geometries and geometry builders to improve readability and ease of use.

  • Geometry builders like PolylineBuilder and PolygonBuilder now takes a function parameter with the builder as the receiver type, allowing you to add geometries to the builder in a Kotlin idiomatic way.

  • A part can be created with segments using MutablePart.createWithSegments()

  • The names of mutable and immutable geometry collection types are aligned with Kotlin idioms. The list below shows the geometry types defined in ArcGIS Maps SDK for Kotlin 200.x and their analogues in ArcGIS Runtime API for Android 100.x.

    ArcGIS Maps SDK for Kotlin 200.xArcGIS Runtime API for Android v100.x
    MutablePartPart
    PartImmutablePart
    MutablePartCollectionPartCollection
    PartCollectionImmutablePartCollection
  • A Part and MutablePart may be viewed as a collection of points by accessing its points property.

  • GeometryEngine methods are generic to provide greater type safety hence an identical geometry is returned as shown below.

    ArcGIS Maps SDK for Kotlin v200.x
    fun <T : Geometry> projectOrNull(geometry: T, outputSpatialReference: SpatialReference, datumTransformation: DatumTransformation?): T?
    fun <T : Geometry> projectOrNull(geometry: T, spatialReference: SpatialReference): T?
    fun <T : Geometry> simplifyOrNull(geometry: T): T?
    fun <T : Geometry> createWithM(geometry: T, m: Double?): T
    fun <T : Geometry> createWithZ(geometry: T, z: Double?): T
    fun <T : Geometry> createWithZAndM(geometry: T, z: Double?, m: Double?): T
// Make a polyline geometry
val polylineGeometry = PolylineBuilder(spatialReference) {
addPoint(x = -10e5, y = 40e5)
addPoint(x = 20e5, y = 50e5)
}.toGeometry()
// Make a part geometry using segments
val partGeometry = MutablePart.createWithSegments(
segments = listOf(leftCurve, leftArc, rightArc, rightCurve),
spatialReference = spatialReference
)
val polygon = Polygon(listOf(partGeometry))

Gestures

The MapView MapView and SceneView composables have gesture events instead of overriding DefaultMapViewOnTouchListener. The events are represented as SharedFlows and can be collected in a coroutine.

MapView(
modifier = Modifier.fillMaxSize(),
arcGISMap = map,
onSingleTapConfirmed = { singleTapConfirmedEvent ->
val mapPoint = singleTapConfirmedEvent.mapPoint
val screenCoordinate = singleTapConfirmedEvent.screenCoordinate
// . . .
}
)

Authentication

See the Migrate authentication from 100.x to 200.x topic for instructions on migrating authentication code in your app.

Custom location dataSource

ArcGIS Maps SDK for Kotlin 200.x introduces a CustomLocationDataSource that can be driven by a user-defined location data provider. This can be useful if you have location data from a custom source and would like that data in the form of a LocationDataSource so that it can interface with other parts of the API.

ArcGIS Maps SDK for Kotlin v200.x
fun createCustomLocationDataSource() {
// create a custom location emitter
val customLocationProvider = { SingleLocationEmitter() }
// add the location provider to the datasource
val customLocationDataSource = CustomLocationDataSource(customLocationProvider)
coroutineScope.launch {
customLocationDataSource.start().onSuccess {
// custom location datasource successfully started
}.onFailure { throwable ->
// error starting location datasource
}
customLocationDataSource.locationChanged.collect {
// get changes in the location
}
}
// add the custom location datasource to the location display
return customLocationDataSource
}
// ... in an external class, create a custom location provider
class SingleLocationEmitter() : CustomLocationDataSource.LocationProvider {
override val locations: Flow<Location> = flow {
emit(
Location.create(
position = point,
horizontalAccuracy = horizontalAccuracy,
verticalAccuracy = verticalAccuracy,
speed = speed,
course = course,
lastKnown = lastKnown,
timestamp = Instant.now()
)
)
}
override val headings: Flow<Double> = flow {
emit(0.0)
}
}

The custom location data source is assigned to the dataSource property of the location display. One way to create the location display and pass it to the MapView composable is the following.

ArcGIS Maps SDK for Kotlin v200.x
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
val locationDisplay = rememberLocationDisplay {
dataSource = createCustomLocationDataSource()
}
MapView(
modifier = Modifier.fillMaxSize(),
arcGISMap = map,
locationDisplay = locationDisplay
)

ApplicationContext requirement

ArcGIS Maps SDK for Kotlin 200.x differs from the ArcGIS Runtime SDK for Android 100.x in that you now set the ArcGISEnvironment.applicationContext property in a few parts of the API. Context is required for LocationDataSource, CustomLocationDataSource, SystemLocationDataSource, IndoorsLocationDataSource, RouteTask, ServiceAreaTask, ClosestFacilityTask, or AuthenticationManager. This property can be set at the beginning of the activity as follows:

ArcGIS Maps SDK for Kotlin v200.x
ArcGISEnvironment.applicationContext = this.applicationContext

Library-specific data types

Color: ArcGIS Maps SDK for Kotlin 200.x replaces android.graphics.color with com.arcgismaps.Color. This color library comes with default colors out of the box. You can also create custom RGB colors and use colors from the app’s resources.

ArcGIS Maps SDK for Kotlin v200.x - Color
import com.arcgismaps.Color
var accentColor = Color.green
// var accentColor = Color.fromRgba(r = 0, g = 255, b = 0, a = 255)
// var accentColor = 0xFF00FF00.toInt()
// val accentColor = Color(colorResource(id = R.color.colorAccent).toArgb())
val selectionProperties = remember {
SelectionProperties().apply {
color = accentColor
}
}
MapView(
modifier = Modifier.fillMaxSize(),
arcGISMap = map,
selectionProperties = selectionProperties
)

ArcGIS Maps GUIDs: ArcGIS Maps SDK for Kotlin 200.x introduces its own data type, Guid, which represents a 128-bit globally unique identifier to represent GlobalID and GUID fields from ArcGIS services and geodatabases.

ArcGIS Maps SDK for Kotlin v200.x - Guid
val utilityElement = utilityNetwork.createElementOrNull(
assetType = utilityAssetType,
globalId = Guid("<Unique-Identifier-Here>"),
terminal = utilityTerminal
)