Effective Map Composables: Collections of Non-Draggable Markers
Uli Bubenheimer
Posted on September 3, 2024
This post continues exploring effective patterns and best practices for the android-maps-compose GitHub library. See the first article for an introduction to the series and for establishing a common foundation. The complete, runnable example for this post is available in the current android-maps-compose release on GitHub.
After covering individual Markers in prior posts, this article shifts the focus to collections of Markers, starting with non-draggable Markers. The next post will cover collections of draggable Markers.
TL;DR: use this with strong skipping enabled:
class MarkerKey
val markersModel: SnapshotStateMap<MarkerKey, LatLng> =
mutableStateMapOf()
@Composable
fun Markers(
keyedPositions: Map<MarkerKey, LatLng>
) = keyedPositions.forEach { (markerKey, position) ->
key(markerKey) {
SimpleMarker(position = position)
}
}
Read on for the discussion.
In this post I want to focus on stateful collections of stateful, non-draggable Markers. Collection state can change by adding/inserting and removing Markers, plus potentially changing iteration order for use cases with a meaningful ordering of Markers. In addition, the state of each Marker can change by updating the Marker position from a model. The model only stores positional data here, using the immutable LatLng type.
The first post of this series developed a convenient pattern to represent individual non-draggable Markers, where MarkerState is fully encapsulated. This simplifies the code by focusing on the LatLng
position data and largely ignoring the presence of MarkerState:
@Composable
fun SimpleMarker(position: LatLng) {
val state = rememberUpdatedMarkerState(position)
Marker(state = state)
}
@Composable
fun rememberUpdatedMarkerState(newPosition: LatLng): MarkerState =
remember { MarkerState(position = newPosition, draggable = false) }
.apply { position = newPosition }
To represent a mutable collection, I will start with a simple mutable list and incrementally refine the pattern to make it more suitable for Compose:
val markersModel: MutableList<LatLng> =
// Do not do this!
mutableListOf(/* ...initial model marker positions... */)
Sadly, using mutableListOf is invalid in a Compose world because its result is not observable—composition will not be notified when the list changes, so the displayed Markers will not update from recomposition. Instead, use one of these:
// List is ok if not mutated, but it is neither @Immutable nor @Stable
val markersModelState: MutableState<List<LatLng>> =
mutableStateOf(listOf(/* ...initial model marker positions... */)
// kotlinx.collections.immutable.PersistentList is considered @Stable,
// but still alpha:
// https://android-review.googlesource.com/c/platform/frameworks/support/+/2328593
val markersModelState: MutableState<PersistentList<LatLng>> =
persistentListOf(/* ...initial model marker positions... */)
// @Stable and released;
// androidx.compose.runtime.snapshots.SnapshotStateList implements the
// MutableList interface and mimics PersistentList under the hood
val markersModel: SnapshotStateList<LatLng> =
mutableStateListOf(/* ...initial model marker positions... */)
I will use mutableStateListOf for now; it returns SnapshotStateList, which implements the MutableList
interface, so the API is very close to the initial code above. Here is a first attempt at displaying the Markers:
// Only suitable for simple needs, will not scale well
@Composable
fun Markers(
positions: SnapshotStateList<LatLng>
) = positions.forEach { position ->
SimpleMarker(position = position)
}
This simple solution satisfies simple requirements. However, it does not scale well. The SimpleMarker
Composables corresponding to the list items are prone to recompose recursively whenever a Marker is inserted, removed, or repositioned.
The remedy is to align Composables with the structure of the model via the key Composable. True to its name, key requires some key object. There is no ready-made key object available in the examples here; the form of the key can be just about anything, so for simplicity, an empty class or Any will work:
class MarkerKey
The key must be stored with the data and referenced for all types of changes (insert, update, move, delete). A map instead of a list can be a good choice for storage, assuming Markers are not ordered (an ordered collection does not have to change the overall approach much). SnapshotStateMap is a stable MutableMap, i.e. observable by Compose:
// Alternatively: MutableState<Map> or
// MutableState<kotlinx.collections.immutable.PersistentMap>;
// SnapshotStateMap has no iteration order guarantees.
val markersModel: SnapshotStateMap<MarkerKey, LatLng> =
mutableStateMapOf()
This leads to:
@Composable
fun Markers(
keyedPositions: SnapshotStateMap<MarkerKey, LatLng>
) = keyedPositions.forEach { (markerKey, position) ->
key(markerKey) {
SimpleMarker(position = position)
}
}
Modifying the SnapshotStateMap causes this function to recompose and update the displayed Markers due to composition observing changes to snapshot state for the map's usage here. To ensure that the SimpleMarker
Composable will skip recomposition for unchanged Marker positions: either rely on strong skipping or, perhaps preferably, inform the Compose compiler that LatLng is stable via a stability configuration file. The android-maps-compose project has an example of the latter. LatLng is not stable by default (at this time) because it comes from the legacy GoogleMap SDK, which is unaware of Compose.
It is not essential to use the very specific, stable SnapshotStateMap type in the function parameter declaration. Using the Map
interface keeps the Composable more reusable, supporting other suitable argument types like PersistentMap. However, Map lacks Compose stability, so make sure strong skipping is enabled, which is the default from Kotlin 2.0.20 onward. With strong skipping, performance between the two alternatives is virtually identical, but without there could be additional expensive recompositions.
@Composable
fun Markers(
keyedPositions: Map<MarkerKey, LatLng>
) = keyedPositions.forEach { (markerKey, position) ->
key(markerKey) {
SimpleMarker(position = position)
}
}
Follow me via my profile link to stay in the loop about future posts in this series and other development topics.
I would love to hear your thoughts—consider leaving a comment below. Composable maps APIs are still in their infancy, with much uncharted territory.
If you need professional assistance with your Compose project, please reach out through my profile. With deep knowledge of Maps Compose APIs and their deficits, I can help overcome challenges and make your Compose-based mapping solution a success.
Attribution: cover image at the top of the post generated with DALL-E
Posted on September 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.