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 mutable
directly in your composables:
@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 mutable State Of
matter:
-
If you don’t use remember: The value will be recalculated every time the composable recomposes. For example, creating an
ArcGIS
on every recomposition can cause unnecessary work or unexpected behavior.Map -
If you mutate a regular variable instead of using
mutable
: Compose won’t know the value changed, so the UI won’t update. State objects likeState Of mutable
notify Compose when their value changes, triggering recomposition when needed.State Of -
Best practice: Use
remember { mutable
for UI state that should survive recompositions, and use aState Of(...) } remember
/Savable View
for state that should survive configuration updates.Model
ViewModel pattern
For more complex applications, use Android's ViewModel architecture to manage state across configuration changes and separate business logic from the UI:
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.
@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 Geo
composables is critical during configuration changes, you can use Compose’s remember
with a custom Saver
. While Compose's remember
function holds state only in memory, remember
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.
/**
* 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 }
}