Hi, question regarding the inner workings of `Andr...
# compose-android
f
Hi, question regarding the inner workings of
AndroidView
. I'm using it to wrap a maplibre (mapbox v9 fork) map in a composable
Map
. If I store the current camera position of the map as a
mutableStateOf
in
Map
, add a position listener that updates the state in the
factory
and set the
position
in the
update
block of the
AndroidView
, all is fine: the position updates when the state changes and the
AndroidView
itself does not recompose. But if that state is hoisted outside of
Map
and passed in as a param,
AndroidView
itself recomposes and the performance is bad. I checked that the
factory
is only called once though and
update
is called correctly. I'm trying to figure out 2 things, why does
AndroidView
itself recompose in this case and not just call
update
and why is the performance bad, it seems like
factory
is not called again but only
updated
which seems like what is supposed to happen.
j
Did you check the compiler metrics for the state you are passing? Maybe the function is not restartable?
that's the first thing i'd check - make sure your state is stable/immutable
e
f
I did not check that yet, will do thanks! I saw about the overload with
onReset
but thought this was more appropriate for lazy layouts
z
It would be easier to figure this out with code, but it sounds like you might be pulling the state read up outside of the update function. If that’s not what’s going on, can you post some code?
f
sure, thanks for chipping in
Copy code
@Composable
fun HoistedStateMap(
    position: MapPosition,
    onPositionChange: (MapPosition) -> Unit,
    modifier: Modifier = Modifier,
) {
    AndroidView(
        factory = { context ->
            MapView(context).also { mapView ->
                mapView.getMapAsync { map ->
                    map.apply {
                        addOnCameraMoveListener { onPositionChange(cameraPosition.toMapPosition()) }
                        setStyle(STYLE_URL)
                    }
                }
            }
        },
        update = { mapView ->
            mapView.getMapAsync { map ->
                position.toCameraPosition().let {
                    if (it != map.cameraPosition) {
                        map.cameraPosition = it
                    }
                }
            }
        },
        modifier = modifier,
    )
}
this is slow
Copy code
@Composable
fun InternalStateMap(
    modifier: Modifier = Modifier,
) {
    var position by remember { mutableStateOf(initialPosition) }
    AndroidView(
        factory = { context ->
            MapView(context).also { mapView ->
                mapView.getMapAsync { map ->
                    map.apply {
                        addOnCameraMoveListener { position = cameraPosition.toMapPosition() }
                        setStyle(STYLE_URL)
                    }
                }
            }
        },
        update = { mapView ->
            mapView.getMapAsync { map ->
                position.toCameraPosition().let {
                    if (it != map.cameraPosition) {
                        map.cameraPosition = it
                    }
                }
            }
        },
        modifier = modifier,
    )
}
this is fast
I still need to look at the compiler metrics though
Copy code
data class MapPosition(val coordinates: Coordinates, val zoom: Double)
e
is
MapPosition
@Stable
or
@Immutable
or neither?
f
Copy code
data class Coordinates(val lat: Latitude, val lon: Longitude)

@JvmInline
value class Latitude(val value: Double)

@JvmInline
value class Longitude(val value: Double)
If it only contains primitives or other classes with only primitives, it wouldn't need it, no?
annotating
MapPosition
with Stable doesn't change anything
It's getting very late here. If you guys have any ideas/suggestions, please just dump it and I'll have a look tomorrow! I'll also have a look at the compiler metrics. Thanks!
So I finally looked at the compiler metric and
MapPosition
was indeed not stable because it came from a module without compose. I adjusted that and now it is stable and the composable is skippable. However, nothing changed in terms of performance. Obviously,
HoistedStateMap
can't be skipped because
position
actually changes. In that sense, it also makes sense that this change didn't have any effect.
Interestingly though, the following code is performant as well
Copy code
@Composable
fun HoistedStateMap(
    position: MapPosition,
    onPositionChange: (MapPosition) -> Unit,
    modifier: Modifier = Modifier,
) {
    val mapPosition = rememberUpdatedState(position)
    AndroidView(
        factory = { context ->
            MapView(context).also { mapView ->
                mapView.getMapAsync { map ->
                    map.apply {
                        addOnCameraMoveListener { onPositionChange(cameraPosition.toMapPosition()) }
                        setStyle(STYLE_URL)
                    }
                }
            }
        },
        update = { mapView ->
            mapView.getMapAsync { map ->
                mapPosition.value.toCameraPosition().let {
                    if (it != map.cameraPosition) {
                        map.cameraPosition = it
                    }
                }
            }
        },
        modifier = modifier,
    )
}
I guess this is because
mapPosition
from
rememberUpdatedState(position)
is an object that itself doesn't change
but then I'm wondering how the
update
function of
AndroidView
is supposed to be used if this is required?
@Zach Klippenstein (he/him) [MOD] any ideas what I'm doing wrong here?
z
What sort of performance issues are you seeing? Dropped frames? Or lag between input and update?
f
lag between input and update
but I don't really understand why the is any kind of difference at all
z
The difference between the first two snippets you posted is that the one with the callback forces at least one frame of delay, assuming the caller is well-behaved. The third snippet also introduces that frame delay though, so if that isn’t affected by the lag then it’s probably not caused by that frame delay. Based on this code it does seem like the problem is caused by AndroidView not skipping, although I’m not sure why that would be. Have you attached a profiler?
f
I see, thanks! I have not yet attached a profiler, let me do that later tonight, I'll post my findings here, thanks!