Effective Map Composables: Non-Draggable Markers
Tags: programming, kotlin, mobile, android
Published: 2024-05-26 22:00:28

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][android-maps-compose-marker-examples] 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9z3doml8g9dvebhzveym.png)

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.

---
><center>TL;DR: do this:</center><br/>
>
>```kotlin
>@Composable
>fun SimpleMarker(position: LatLng) {
>    val state = rememberUpdatedMarkerState(position)
>    Marker(state = state)
>}
>
>@Composable
>fun rememberUpdatedMarkerState(newPosition: LatLng) =
>    remember { MarkerState(position = newPosition) }
>        .apply { position = newPosition }
>```
>&nbsp;<br/>

---

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:

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

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].

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

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

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

In this case the IDE will generally flag the problem, but in the former example it would not, [until now][StateFactoryMarker PR].  (Or whenever the PR actually sees the light of day.)

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:

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

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][hoisted 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:

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

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?

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

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:

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

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:

```kotlin
@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 }
```

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:

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

Be aware that `rememberUpdatedMarkerState(LatLng)` above is not to be confused with [rememberMarkerState(LatLng)][rememberMarkerState] 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][rememberUpdatedState KDoc] 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][MapClickListeners usage] 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][stateful vs stateless]. 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][unidirectional data flow], 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.

---

Do _you_ have thoughts on this topic? Consider leaving a comment below. Composable maps APIs are still in their infancy, and there is much uncharted territory.

If you need professional help with your maps-related Compose project you can reach out to me through my profile. I have intimate knowledge of the Maps Compose API surface, its internals, and many ideas how to fix its deficiencies.

{% user bubenheimer %}

&nbsp;

> Attribution: _cover image at the top of the post generated with DALL-E_

[android-maps-compose]: https://github.com/googlemaps/android-maps-compose

[android-maps-compose-marker-examples]: https://github.com/googlemaps/android-maps-compose/tree/1a03452dee3d6cf6c59de506652daca32fb51156/app/src/main/java/com/google/maps/android/compose/markerexamples

[5.0.3 release]: https://github.com/googlemaps/android-maps-compose/releases/tag/v5.0.3

[Jetpack Compose]: https://developer.android.com/develop/ui/compose

[Maps SDK for Android]: https://developers.google.com/maps/documentation/android-sdk

[UpdatingNoDragMarkerWithDataModelActivity]: https://github.com/googlemaps/android-maps-compose/blob/1a03452dee3d6cf6c59de506652daca32fb51156/app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt

[unidirectional data flow]: https://developer.android.com/develop/ui/compose/architecture#udf

[hoisted state type]: https://github.com/androidx/androidx/blob/c007497116a6bae87089e0d46f5ba8b385582cf7/compose/docs/compose-api-guidelines.md#hoisted-state-types

[state holder]: https://developer.android.com/topic/architecture/ui-layer/stateholders

[StateFactoryMarker PR]: https://github.com/googlemaps/android-maps-compose/pull/516

[remember]: https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#remember(kotlin.Function0)

[backward writes]: https://developer.android.com/develop/ui/compose/performance/bestpractices#avoid-backwards

[rememberUpdatedState]: https://github.com/androidx/androidx/blob/8e8e0d86039298fb4816960398308b531eea734e/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotState.kt#L309-L312

[rememberUpdatedState KDoc]: https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#rememberUpdatedState(kotlin.Any)

[rememberMarkerState]: https://github.com/googlemaps/android-maps-compose/blob/1a03452dee3d6cf6c59de506652daca32fb51156/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt#L177-L183

[rememberSaveable]: https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/package-summary#rememberSaveable(kotlin.Array,androidx.compose.runtime.saveable.Saver,kotlin.String,kotlin.Function0)

[MarkerState]: https://github.com/googlemaps/android-maps-compose/blob/1a03452dee3d6cf6c59de506652daca32fb51156/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt#L78-L92

[Marker]: https://googlemaps.github.io/android-maps-compose/maps-compose/com.google.maps.android.compose/-marker.html

[Maps SDK Marker]: https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/Marker

[MutableState]: https://developer.android.com/reference/kotlin/androidx/compose/runtime/MutableState

[LaunchedEffect]: https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary?hl=en#LaunchedEffect(kotlin.Any,kotlin.coroutines.SuspendFunction1)

[hoisted state]: https://developer.android.com/develop/ui/compose/state#state-hoisting

[BasicText]: https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/package-summary#BasicText(androidx.compose.ui.text.AnnotatedString,androidx.compose.ui.Modifier,androidx.compose.ui.text.TextStyle,kotlin.Function1,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.collections.Map,androidx.compose.ui.graphics.ColorProducer)

[BasicTextField]: https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/package-summary#BasicTextField(androidx.compose.ui.text.input.TextFieldValue,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.text.TextStyle,androidx.compose.foundation.text.KeyboardOptions,androidx.compose.foundation.text.KeyboardActions,kotlin.Boolean,kotlin.Int,kotlin.Int,androidx.compose.ui.text.input.VisualTransformation,kotlin.Function1,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Brush,kotlin.Function1)

[stateful vs stateless]: https://developer.android.com/develop/ui/compose/state#stateful-vs-stateless

[MapClickListeners usage]: https://github.com/googlemaps/android-maps-compose/blob/1a03452dee3d6cf6c59de506652daca32fb51156/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt#L108-L116

[side effect]: https://developer.android.com/develop/ui/compose/side-effects