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 LaunchedEffect 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, LaunchedEffect loads a mobile map package, ensuring the map is ready for display as soon as the MapView appears.
@Composablefun 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 animateValueAsState within a LaunchedEffect, you can update a graphic’s geometry for every frame.
For example, when a user taps the map, the targetGraphicLocation state is updated, which in turn drives the animateToTappedCoordinate animation. The LaunchedEffect is triggered for each coordinate change, which then animates the graphic’s position across the map:
51 collapsed lines
package com.example.guide.programmingpatterns.kotlinprogrammingpractices.snips.screens.mapwithanimatablegraphic
import androidx.compose.animation.core.AnimationVector2Dimport androidx.compose.animation.core.FastOutSlowInEasingimport androidx.compose.animation.core.TwoWayConverterimport androidx.compose.animation.core.animateValueAsStateimport androidx.compose.animation.core.tweenimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport com.arcgismaps.Colorimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
@Composablefun 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 } )}9 collapsed lines
/** * 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 ViewModel. These coroutines are automatically managed by the Android framework, ensuring that any running tasks are cancelled when the ViewModel 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 MapViewProxy and SceneViewProxy to perform reactive updates on a GeoView, 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 ViewModel and @Composable 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.
59 collapsed lines
package com.example.guide.programmingpatterns.kotlinprogrammingpractices.snips.screens.managinglongrunningjobs
import android.app.Applicationimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.material3.Buttonimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.ArcGISEnvironment.applicationContextimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.portal.Portalimport com.arcgismaps.tasks.offlinemaptask.GenerateOfflineMapJobimport com.arcgismaps.tasks.offlinemaptask.GenerateOfflineMapParametersimport com.arcgismaps.tasks.offlinemaptask.OfflineMapTaskimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.esri.arcgismaps.sample.sampleslib.components.JobLoadingDialogimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport java.io.File
@Composablefun 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 } } } }11 collapsed lines
/** * 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.