Effective Map Composables: Non-Draggable Markers

Effective Map Composables: Non-Draggable Markers

Cover image generated with DALL-E

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 recent android-maps-compose 5.1.0 release. Later posts build on earlier ones. I authored the underlying library examples and made other recent contributions to the GitHub project; I have been using the library in my own apps.


googlemaps
/
android-maps-compose

Jetpack Compose composables for the Maps SDK for Android




Maps Compose đŸ—ș

Description

This repository contains Jetpack Compose components for the Maps SDK for Android.

Requirements

Kotlin-enabled project
Jetpack Compose-enabled project (see releases for the required version of Jetpack Compose)
An API key

API level 21+

Installation

You no longer need to specify the Maps SDK for Android or its Utility Library as separate dependencies, since maps-compose and maps-compose-utils pull in the appropriate versions of these respectively.

dependencies {
implementation com.google.maps.android:maps-compose:5.0.1

// Optionally, you can include the Compose utils library for Clustering,
// Street View metadata checks, etc.
implementation com.google.maps.android:maps-compose-utils:5.0.1

// Optionally, you can include the widgets library for ScaleBar, etc.
implementation com.google.maps.android:maps-compose-widgets:5.0.1
}

Sample App

This repository includes a sample app.

To run it:

Get a Maps API key

Create a file in the root directory named local.properties with a single line that looks like this, replacing YOUR_KEY with the


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
}

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)
//…
}

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

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.

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)
}

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)
}

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)
}

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 }

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 }

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)
}

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.

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.

Please follow and like us:
Pin Share