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.
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
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)
}
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.
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.
Attribution: cover image at the top of the post generated with DALL-E
Top comments (0)