Effective Map Composables: Collections of Non-Draggable Markers

bubenheimer

Uli Bubenheimer

Posted on September 3, 2024

Effective Map Composables: Collections of Non-Draggable Markers

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.

Markers Collection


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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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... */)
Enter fullscreen mode Exit fullscreen mode

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... */)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

This leads to:

@Composable
fun Markers(
    keyedPositions: SnapshotStateMap<MarkerKey, LatLng>
) = keyedPositions.forEach { (markerKey, position) ->
    key(markerKey) {
        SimpleMarker(position = position)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
bubenheimer
Uli Bubenheimer

Posted on September 3, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related