Skip to content

Manage GeoView state

Jetpack Compose is a declarative, state-driven UI framework that requires you to manage maps, scenes, and associated data for your GeoView composables. You can preserve state using Compose State properties and ViewModels to separate application logic from the UI layer.

State management with Compose

The ArcGIS Maps SDK for Kotlin Toolkit provides MapView and SceneView composables that display and interact with map and scene data. These composables follow Compose principles and automatically recompose when their state changes.

For simple applications, you can use remember and mutableStateOf directly in your composables:

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
@Composable
fun MapStateScreen(modifier: Modifier = Modifier) {
    // Initialize ArcGISMap with BasemapStyle.
    val map by remember { mutableStateOf(ArcGISMap(BasemapStyle.ArcGISStreets)) }

    // Observe the map's load status as a state using lifecycle-aware collection.
    val loadStatus by map.loadStatus.collectAsStateWithLifecycle()

    // Define status messages based on the current LoadStatus.
    val statusMessage = when (loadStatus) {
        LoadStatus.NotLoaded -> "Map is not loaded."
        LoadStatus.Loading -> "Map is loading..."
        LoadStatus.Loaded -> "Map has successfully loaded!"
        is LoadStatus.FailedToLoad -> "Failed to load map: ${(loadStatus as LoadStatus.FailedToLoad).error.message}"
    }

    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(text = statusMessage)
        MapView(
            arcGISMap = map,
            modifier = Modifier.fillMaxSize().weight(1f)
        )
        // Refresh the map by calling retryLoad in a coroutine scope.
        val scope = rememberCoroutineScope()
        Button(
            onClick = {
                scope.launch {
                    map.retryLoad()
                }
            }
        ) {
            Text("Refresh Map")
        }
    }
}

Why remember and mutableStateOf matter:

  • If you don’t use remember: The value will be recalculated every time the composable recomposes. For example, creating an ArcGISMap on every recomposition can cause unnecessary work or unexpected behavior.

  • If you mutate a regular variable instead of using mutableStateOf: Compose won’t know the value changed, so the UI won’t update. State objects like mutableStateOf notify Compose when their value changes, triggering recomposition when needed.

  • Best practice: Use remember { mutableStateOf(...) } for UI state that should survive recompositions, and use a rememberSavable/ViewModel for state that should survive configuration updates.

ViewModel pattern

For more complex applications, use Android's ViewModel architecture to manage state across configuration changes and separate business logic from the UI:

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
class MapStateViewModel : ViewModel() {
    // Initialize ArcGISMap with BasemapStyle.
    private val _map by mutableStateOf(ArcGISMap(BasemapStyle.ArcGISStreets))
    val map: ArcGISMap get() = _map

    // Observe map loading status via StateFlow.
    private val _statusMessage = MutableStateFlow("Map is initializing...")
    val statusMessage: StateFlow<String> get() = _statusMessage.asStateFlow()

    init {
        observeLoadStatus()
    }

    // Maps load status to a message.
    private fun observeLoadStatus() {
        viewModelScope.launch {
            _map.loadStatus.collect { loadStatus: LoadStatus ->
                _statusMessage.value = when (loadStatus) {
                    LoadStatus.NotLoaded -> "Map is not loaded."
                    LoadStatus.Loading -> "Map is loading..."
                    LoadStatus.Loaded -> "Map has successfully loaded!"
                    is LoadStatus.FailedToLoad -> "Failed to load map: ${loadStatus.error.message}"
                }
            }
        }
    }

    // Refreshes the map by retrying load.
    fun refreshMap() {
        viewModelScope.launch { _map.retryLoad()
        Log.e("TAG", "Refresh Map button clicked")
        }
    }
}

Using the ViewModel in a Composable

Integrate the ViewModel into a Composable function to simplify UI logic and ensure lifecycle-aware state updates.

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
@Composable
fun MapStateScreen(
    modifier: Modifier = Modifier,
    viewModel: MapStateViewModel
) {
    // Observe the message as state using lifecycle-aware collection.
    val statusMessage by viewModel.statusMessage.collectAsStateWithLifecycle()

    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(text = statusMessage)

        MapView(
            arcGISMap = viewModel.map,
            modifier = Modifier.fillMaxSize().weight(1f)
        )

        Button(onClick = { viewModel.refreshMap() }) {
            Text("Refresh Map")
        }
    }
}

Custom state preservation

For scenarios where preserving specific properties of your GeoView composables is critical during configuration changes, you can use Compose’s rememberSaveable with a custom Saver. While Compose's remember function holds state only in memory, rememberSaveable allows you to save and restore state automatically using a custom Saver. This ensures the map's properties, like basemap style, are retained seamlessly across recompositions.

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
/**
 * A composable function that provides an ArcGISMap instance whose state is preserved across
 * configuration changes (e.g., screen rotations) using Jetpack Compose's `rememberSaveable`.
 *
 * This function utilizes a custom Saver to serialize the map's state to JSON when the map is
 * fully loaded, ensuring that it can be restored later without losing user-applied configurations.
 *
 * @param defaultMap The initial ArcGISMap instance to use if no saved state exists or restoration fails.
 *                   Defaults to a map with the `BasemapStyle.ArcGISTopographic` style.
 *
 * @return An ArcGISMap instance that persists its state across configuration changes.
 *
 * - **Usage Example**:
 *
 * ```
 * @Composable
 * fun MapScreen(modifier: Modifier, viewmodel: MapViewModel = viewModel()) {
 *     MapView(
 *          arcGISMap = rememberSavableArcGISMap(defaultMap = viewmodel.map),
 *          modifier = modifier.fillMaxSize()
 *     )
 * }
 * ```
 */

@Composable
fun rememberSavableArcGISMap(defaultMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic)): ArcGISMap {
    // Define a custom saver for serializing and deserializing the ArcGISMap's state.
    val arcGISMapSaver = Saver<ArcGISMap, String>(
        save = { map: ArcGISMap ->
            // Serialize the map only if it has been successfully loaded.
            if (map.loadStatus.value == LoadStatus.Loaded) {
                map.toJson()
            } else {
                null  // Avoid saving anything if the map isn't in a valid loaded state.
            }
        },
        restore = { jsonString: String ->
            // Restore the map from its serialized JSON string, or return null if invalid.
            ArcGISMap.fromJsonOrNull(jsonString)
        }
    )

    // Use the custom saver to persist the ArcGISMap's state.
    return rememberSaveable(saver = arcGISMapSaver) { defaultMap }
}

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