Concurrency is a core part of modern mobile development, allowing you to execute tasks in the background to keep your app's UI responsive. With the ArcGIS Maps SDK for Kotlin, you'll use coroutines to manage asynchronous workflows, such as loading resources, querying services, or downloading data for offline use.
In Jetpack Compose, you can execute these asynchronous tasks using side effects and ViewModel coroutine scopes.
Initial loading with LaunchedEffect
For tasks that should run once when a composable enters the composition, such as loading a map from a remote service or a local file, the Launched
composable is the ideal tool. It executes a block of code within a coroutine and automatically cancels the coroutine if the composable leaves the composition.
In this example, Launched
loads a mobile map package, ensuring the map is ready for display as soon as the Map
appears.
@Composable
fun MapWithInitialData() {
// Set up the properties to display a map
val mobileMapPackage = MobileMapPackage(path = "/path/to/map.mmpk")
var map by remember { mutableStateOf(ArcGISMap(BasemapStyle.ArcGISStreets)) }
var isMapLoaded by remember { mutableStateOf(false) }
var errorThrowable by remember { mutableStateOf<Throwable?>(null) }
// Load data when the composable enters composition
LaunchedEffect(Unit) {
// Load mobile map package
mobileMapPackage.load().onSuccess {
mobileMapPackage.maps.firstOrNull()?.let { loadedMap ->
// Update the state variable with the loaded map
map = loadedMap
// Update boolean to display MapView
isMapLoaded = true
}
}.onFailure { errorThrowable = it }
}
if (isMapLoaded) {
// Display the loaded mobile map package
MapView(
arcGISMap = map,
modifier = Modifier.fillMaxSize()
)
} else {
// Show progress indicator while mobile map package is loading
CircularProgressIndicator()
}
// Display any errors if load fails
if (errorThrowable != null) {
Dialog(onDismissRequest = { errorThrowable = null }) {
Text("${errorThrowable?.message}: ${errorThrowable?.cause}")
}
}
}
Animate a Graphic with LaunchedEffect
You can leverage Compose's value-based animation functions to create smooth custom animations for graphics on your map. By observing the reactive state provided by animate
within a Launched
, you can update a graphic's geometry for every frame.
For example, when a user taps the map, the target
state is updated, which in turn drives the animate
animation. The Launched
is triggered for each coordinate change, which then animates the graphic's position across the map:

@Composable
fun MapWithAnimatableGraphic() {
// Set up initial properties to display a map
var map by remember { mutableStateOf(ArcGISMap(BasemapStyle.ArcGISStreets)) }
val mapViewProxy = MapViewProxy()
var targetGraphicLocation by remember { mutableStateOf(ScreenCoordinate(0.0, 0.0)) }
// Use Compose animation functions with the ArcGISMaps SDK
val animateToTappedCoordinate by animateValueAsState(
typeConverter = screenCoordinateToVector,
targetValue = targetGraphicLocation,
label = "AnimateScreenCoordinate",
animationSpec = tween(easing = FastOutSlowInEasing, durationMillis = 300)
)
// Define the graphic used for animating to the tapped location
val graphic = remember {
Graphic(
geometry = mapViewProxy.screenToLocationOrNull(targetGraphicLocation),
symbol = SimpleMarkerSymbol(
style = SimpleMarkerSymbolStyle.Cross,
color = Color.red,
size = 20f
)
)
}
// Use the key to trigger changes on the graphic's location as it animates to the tapped location
LaunchedEffect(key1 = animateToTappedCoordinate) {
graphic.geometry = mapViewProxy.screenToLocationOrNull(animateToTappedCoordinate)
}
// Display the MapView and listen for tap events
MapView(
arcGISMap = map,
modifier = Modifier.fillMaxSize(),
mapViewProxy = mapViewProxy,
graphicsOverlays = listOf(remember { GraphicsOverlay(listOf(graphic)) }),
onSingleTapConfirmed = { tapEvent ->
targetGraphicLocation = tapEvent.screenCoordinate
}
)
}
/**
* Convert [ScreenCoordinate] to an animatable 2D vector type.
*/
private val screenCoordinateToVector: TwoWayConverter<ScreenCoordinate, AnimationVector2D> =
TwoWayConverter(
{ AnimationVector2D(v1 = it.x.toFloat(), v2 = it.y.toFloat()) },
{ ScreenCoordinate(x = it.v1.toDouble(), y = it.v2.toDouble()) }
)
Reactive updates with ViewModel scope
For tasks triggered by user interaction, like a button click or a tap gesture, use Kotlin coroutines with lifecycle-aware components like, viewModelScope provided by the View
. These coroutines are automatically managed by the Android framework, ensuring that any running tasks are cancelled when the View
is no longer needed. This prevents memory leaks and ensures your application remains stable.
See the Perform GeoView Operations to learn more about how to use Map
and Scene
to perform reactive updates on a Geo
, such as setting the viewpoint or identifying features.
Managing long-running jobs
For long-running, multi-step operations like downloading an offline map or performing a complex geoprocessing task, the ArcGIS Maps SDK provides Task and Job types. A Task is a class used to create a Job, which can be monitored, paused, resumed, and canceled. This is essential for operations that may take a long time and need to gracefully handle network changes or user interruptions.
This example showcases a workflow for taking a map offline, demonstrating how the View
and @
UI interact to manage the state of a long-running job. The UI displays a progress dialog and handles errors, while the ViewModel orchestrates the entire download job process.

@Composable
fun GenerateOfflineMapScreen(mapViewModel: GenerateOfflineMapViewModel = viewModel()) {
Column(modifier = Modifier.fillMaxSize()) {
MapView(
modifier = Modifier
.fillMaxSize()
.weight(1f),
arcGISMap = mapViewModel.arcGISMap,
mapViewProxy = mapViewModel.mapViewProxy,
onViewpointChangedForBoundingGeometry = { viewpoint ->
mapViewModel.calculateDownloadOfflineJob(currentViewpoint = viewpoint)
}
)
Button(onClick = mapViewModel::runOfflineMapJob) {
Text("Take map offline")
}
// Display progress dialog while generating an offline map
if (mapViewModel.showJobProgressDialog) {
JobLoadingDialog(
title = "Generating offline map...",
progress = mapViewModel.offlineMapJobProgress,
cancelJobRequest = { mapViewModel.cancelOfflineMapJob() },
)
}
// ... other UI elements like buttons and dialogs
}
}
class GenerateOfflineMapViewModel(application: Application) : AndroidViewModel(application) {
// Proxy object for calling operations on the MapView from the ViewModel.
val mapViewProxy = MapViewProxy()
// The map is a mutable state, allowing the UI to react and update to a successful job result.
var arcGISMap by mutableStateOf(
ArcGISMap(
item = PortalItem(
portal = Portal(url = "https://www.arcgis.com"),
itemId = "acc027394bc84c2fb04d1ed317aac674"
)
)
)
// The job instance is held in a state variable to be managed by the ViewModel.
private var generateOfflineMapJob: GenerateOfflineMapJob? by mutableStateOf(null)
var showJobProgressDialog by mutableStateOf(false)
private set
var offlineMapJobProgress by mutableIntStateOf(0)
private set
/**
* Updates the [generateOfflineMapJob] for the given [currentViewpoint].
*/
fun calculateDownloadOfflineJob(currentViewpoint: Viewpoint) {
// Creates a new job with the specified area of interest.
generateOfflineMapJob = OfflineMapTask(onlineMap = arcGISMap).createGenerateOfflineMapJob(
parameters = GenerateOfflineMapParameters(areaOfInterest = currentViewpoint.targetGeometry),
downloadDirectoryPath = applicationContext?.getExternalFilesDir(null)?.path + File.separator + "utilityNetwork"
)
}
/**
* Runs the [GenerateOfflineMapJob] and displays the result.
*/
fun runOfflineMapJob() {
// Shows the progress dialog.
showJobProgressDialog = true
with(viewModelScope) {
// Launches a new coroutine to collect the progress updates on the main thread.
launch(Dispatchers.Main) {
generateOfflineMapJob?.progress?.collect { progress ->
// Updates the progress state, triggering a UI update.
offlineMapJobProgress = progress
}
}
// Launches another coroutine on a background thread to start the job.
launch(Dispatchers.IO) {
// Starts the long-running job.
generateOfflineMapJob?.start()
// Await for the job's result.
generateOfflineMapJob?.result()?.onSuccess { generateOfflineMapResult ->
// On success, the map state is updated, which triggers a UI recomposition.
arcGISMap = generateOfflineMapResult.offlineMap
// Hides the progress dialog.
showJobProgressDialog = false
// ... show result snackbar
}?.onFailure { throwable ->
// On failure, the progress dialog is hidden and an error is handled.
showJobProgressDialog = false
// ... show error dialog
}
}
}
}
/**
* Cancel the offline map job.
*/
fun cancelOfflineMapJob() {
// Launches a new coroutine to cancel the job on a background thread.
viewModelScope.launch(Dispatchers.IO) {
generateOfflineMapJob?.cancel()
}
}
}
Note:
- See the guide tutorial Display an offline map to learn how to download and display an offline map for a user-defined geographical area of a web map.
- See the sample app Navigate route with rerouting to learn how to navigate between two points and dynamically recalculate an alternate route when the original route is unavailable.
- See the sample app Show device location using fused location data source to learn how to use the Fused Location & Orientation Providers to implement an ArcGIS Maps SDK custom location provider.