Skip to content

Concurrent GeoView tasks

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.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@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 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:

Map with Animatable Graphic
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@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 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.

Long Running Job Demo
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
@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.

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.