Effective Map Composables: Non-Draggable Markers

bubenheimer

Uli Bubenheimer

Posted on May 26, 2024

Effective Map Composables: Non-Draggable Markers

This article is the first in a series exploring effective patterns and best practices for the android-maps-compose GitHub library, with a focus on map markers. android-maps-compose is a Jetpack Compose wrapper around the Google Play services Maps SDK for Android, providing a toolkit for adding interactive maps to your Android application with ease.

Each post in the series elaborates on a different example from the android-maps-compose 5.0.3 release. Later posts build on earlier ones. I authored the underlying library examples and made other recent contributions to the android-maps-compose GitHub project; I have been using the library in my own apps.

android-maps-compose GitHub project screenshot

This post introduces a streamlined Composable for non-draggable Markers, supporting marker position updates from a model. The post also serves to establish common terminology. The project's UpdatingNoDragMarkerWithDataModelActivity example has the complete code.


TL;DR: do this:



@Composable
fun SimpleMarker(position: LatLng) {
   val state = rememberUpdatedMarkerState(position)
   Marker(state = state)
}

@Composable
fun rememberUpdatedMarkerState(newPosition: LatLng) =
   remember { MarkerState(position = newPosition) }
       .apply { position = newPosition }


 


Read on to see what is behind this approach. For clarity I will focus on position as a Marker's primary, stateful property. Adding properties does not alter the general approach.

While getting better acquainted with the android-maps-compose project in the past half year I came across suboptimal Marker usage patterns, in the project itself and in the community. Here is the starting point for this post:



@Composable
fun SimpleMarker(position: LatLng) {
    Marker(state = MarkerState(position = position)) // bad
}


Enter fullscreen mode Exit fullscreen mode

This snippet displays a Marker and keeps its position updated from a model. It looks convenient, but: MarkerState is a hoisted state type, or state holder. It encapsulates state of a Marker, in particular its position. An android-maps-compose Marker is a wrapper around a Maps SDK Marker.



class MarkerState(position: LatLng) {
    var position: LatLng by mutableStateOf(position)
    //...
}


Enter fullscreen mode Exit fullscreen mode

The earlier snippet is essentially the following, a state object without remember:



Marker(state = mutableStateOf(latLng)) // bad pseudo code


Enter fullscreen mode Exit fullscreen mode

Starting with the 6.0.0 release, the IDE generally flags the problem.

The core problem is that every recomposition creates a new state object. At best, this may imply a performance penalty; at worst, it might cause incorrect behavior, depending on the API's internal behaviors.

To fix it, our next step might be:



@Composable
fun SimpleMarker(position: LatLng) {
    val state =
        remember(position) { MarkerState(position = position) }  // bad
    Marker(state = state)
}


Enter fullscreen mode Exit fullscreen mode

This version is a little better. Recomposition will not recreate the state object each time, but only if the position parameter changes. (In this simplistic example, recomposition would not occur otherwise anyway, but that is beside the point.)

The pattern still needs improvement: recomposition replaces the state object instead of updating it; we need another fix to hoist state correctly. (A close look at the Marker implementation shows replacing the state object in the above fashion does not work quite right.)

Let's try again:



@Composable
fun SimpleMarker(position: LatLng) {
    val state = remember { MarkerState(position = position) }
    LaunchedEffect(position) {
        state.position = position
    }
    Marker(state = state)
}


Enter fullscreen mode Exit fullscreen mode

This version shows a familiar Compose pattern that does the right thing. It may be what many Compose developers would choose naturally. Are we done yet? A concern is that this code defers moving the Marker to a new position until the next recomposition; LaunchedEffect runs at the very end of a composition cycle. The code also guarantees to add that extra, costly recomposition. What to do?



@Composable
fun SimpleMarker(position: LatLng) {
    val state = remember { MarkerState(position = position) }
    state.position = position // ?!
    Marker(state = state)
}


Enter fullscreen mode Exit fullscreen mode

This approach may look sketchy, but it is valid:

The assignment looks like a side effect of composition. In fact, it is not a side effect because it updates snapshot state. If the composition were canceled, the update to snapshot state would disappear along with the composition.

However, this still writes to state in composition, which can be dicey: the problem is backward writes, changing state after it has been read.

In the above case there is no backward write. The code updates position state before reading it in the Marker Composable. You can verify that all this happens within a single composition, without triggering recomposition. The pattern is what we want for decent code logic and performance.

If still in doubt, look at the implementation of rememberUpdatedState from the Compose runtime:



@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }


Enter fullscreen mode Exit fullscreen mode

The above code does the same thing, but for plain MutableState.

It is a good idea to encapsulate the MarkerState pattern in the same way to address the risk of accidentally moving the assignment down and introducing a backward write:



@Composable
fun SimpleMarker(position: LatLng) {
    val state = rememberUpdatedMarkerState(position)
    Marker(state = state)
}

@Composable
fun rememberUpdatedMarkerState(newPosition: LatLng): MarkerState =
    remember { MarkerState(position = newPosition) }
        .apply { position = newPosition }


Enter fullscreen mode Exit fullscreen mode

What we have arrived at is a general purpose Composable SimpleMarker(position: LatLng) that encapsulates the Marker's statefulness. Convenient whenever we deal with non-draggable Markers that may change their position; naturally, the Composable is equally applicable for Markers that never move:



@Composable
fun FlightTracker(
    musk: LatLng,
    zuck: LatLng,
    cook: LatLng
) {
    SimpleMarker(musk)
    SimpleMarker(zuck)
    SimpleMarker(cook)
}


Enter fullscreen mode Exit fullscreen mode

Be aware that rememberUpdatedMarkerState(LatLng) above is not to be confused with rememberMarkerState(LatLng) from the android-maps-compose API. The latter is a strange beast that uses rememberSaveable to remember and persist MarkerState, without updating for model-driven changes. rememberSaveable introduces an additional source of truth. I do not see a use case for rememberMarkerState outside of small demos without a model, so I recommend ignoring it.


It may seem odd that we ended up with a function that mirrors rememberUpdatedState from the Compose runtime. rememberUpdatedState is generally used to access the most recent value of a stream of updating values from inside a long-running lambda. We do not have a long-running lambda in the simple Marker examples above. However, this similarity to rememberUpdatedState is just coincidence; the pattern is applicable in other contexts as well.

Here is what sets the Marker situation apart from typical Compose UI APIs: the android-maps-compose Markers API hoists state (MarkerState) to model the statefulness of the underlying Maps SDK Marker API. Hoisting state makes the Compose API essentially stateless, but it does not offer a corresponding stateful API, as is common in Compose development. It is somewhat like using BasicTextField for both input and display, instead of choosing BasicText for simplified text display. The SimpleMarker Composable is the equivalent of the stateful BasicText API surface.


This post focused on the use case of non-draggable Marker display, outlining a stateful Composable pattern to complement the stateless Maps Compose Marker API. The stateful Composable supports model-driven Marker position updates with a streamlined API surface and efficient implementation. In this case state only flows down, i.e. the model is the singular source of truth, without state-changing events flowing back up.

The next post in the series will explore the converse use case: a draggable Marker updating state, with state-changing events bubbling up. rememberUpdatedMarkerState is no longer helpful in this case: MarkerState becomes the primary source of truth, supplanting the model.


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 May 26, 2024

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

Sign up to receive the latest update from our blog.

Related