android-maps-compose
android-maps-compose copied to clipboard
Maps: Markers occasionally flicker when updating content while using zIndex
Environment details
Reproduced on multiple API levels on physical devices.
googleMapsCompose = "6.4.1"
google-maps-compose = { group = "com.google.maps.android", name = "maps-compose", version.ref = "googleMapsCompose" }
Steps to reproduce
When using custom markers with zIndex, updating them based on a state makes them flicker at the wrong position (lat/long) occasionally. If not using the zIndex paramater the problems disappears.
Code example
package com.example.poc.googlemapflickering
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.example.poc.googlemapflickering.ui.theme.GoogleMapFlickeringTheme
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MarkerComposable
import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberMarkerState
// Copenhagen city center coordinates
val copenhagenCenter = LatLng(55.6761, 12.5683)
// List of 10 random locations in Copenhagen
val randomLocations = listOf(
LatLng(55.6842, 12.5774),
LatLng(55.6718, 12.5613),
LatLng(55.6692, 12.5901),
LatLng(55.6768, 12.5528),
LatLng(55.6885, 12.5723),
LatLng(55.6629, 12.5639),
LatLng(55.6745, 12.5877),
LatLng(55.6807, 12.5652),
LatLng(55.6783, 12.5796),
LatLng(55.6702, 12.5534)
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
GoogleMapFlickeringTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
var selectedLocationIndex by remember { mutableIntStateOf(-1) }
Map(selectedLocationIndex, innerPadding)
Pager(
selectedIndex = selectedLocationIndex,
onSelectIndex = {
selectedLocationIndex = it.mod(randomLocations.size - 1)
},
modifier = Modifier
.fillMaxSize()
.wrapContentHeight(Alignment.Bottom)
)
}
}
}
}
}
@Composable
private fun Map(
selectedLocationIndex: Int,
innerPadding: PaddingValues
) {
val cameraState = rememberCameraPositionState(selectedLocationIndex.toString()) {
this.position = CameraPosition.fromLatLngZoom(
randomLocations.getOrNull(selectedLocationIndex) ?: copenhagenCenter,
10f
)
}
GoogleMap(
cameraPositionState = cameraState,
contentPadding = innerPadding,
modifier = Modifier.fillMaxSize()
) {
for ((index, location) in randomLocations.withIndex()) {
MarkerComposable(
selectedLocationIndex,
state = rememberMarkerState(position = location),
zIndex = if (index == selectedLocationIndex) 1f else 0f
) {
val color = if (index == selectedLocationIndex) Color.Red else Color.Blue
val size = if (index == selectedLocationIndex) 64.dp else 32.dp
Spacer(
Modifier
.background(color)
.size(size)
)
}
}
}
}
@Composable
private fun Pager(
selectedIndex: Int,
onSelectIndex: (Int) -> Unit,
modifier: Modifier = Modifier
) {
Row(modifier = modifier.padding(32.dp), horizontalArrangement = Arrangement.Center) {
FilledIconButton(
onClick = { onSelectIndex(selectedIndex - 1) },
modifier = Modifier.size(64.dp)
) {
Icon(Icons.Default.ArrowBack, contentDescription = null)
}
Spacer(modifier = Modifier.size(32.dp))
FilledIconButton(
onClick = { onSelectIndex(selectedIndex + 1) },
modifier = Modifier.size(64.dp)
) {
Icon(Icons.Default.ArrowForward, contentDescription = null)
}
}
}
Demo of the issue: https://github.com/user-attachments/assets/444b7ffb-1d3b-45ff-a6b3-d90b16daa1ed In this demo project the flickers only happens 2 or 3 times, but in our actual project, the flickering is very frequent.
Demo of the same code without zIndex (no issue)
https://github.com/user-attachments/assets/589e6a31-c13a-4b1f-8f8b-5579a7e54574
There are a few problems with this example. In particular ~you need to~ I would rememberUpdatedState(selectedLocationIndex). See if that addresses the issue. Others: 'key(index)', LaunchedEffect(pagerState.currentPage). There may be a few more. It may help to have the code reviewed by someone.
Not saying the problem does not exist. Just hard to say if it does based on the example.
Also, use remember { MarkerState(...) }, but unlikely to make a difference here
Actually, for the purpose of troubleshooting you could pass the MutableState into the Map() Composable; there is a small issue in Compose with using rememberUpdatedState() crossing nested composition boundaries; there is no issue if using the State directly.
Thanks for the comment @bubenheimer. I've removed the key(..) { ... } block, that was leftover from a sample. And I think the LaunchedEffect(pagerState.currentPage) would be irrelevant to the issue as that's just a way to trigger the change, I've updated the code with a simpler version that just uses buttons.
Issue can still be reproduced:
https://github.com/user-attachments/assets/91efc2bc-45f6-463d-a896-811562ae3007
Can you expand on how you would use rememberUpdatedState here?
As bad as my Compose state-handling might be, commenting the zIndex parameter out fixes the issue, which suggests there's an underlying issue here anwyays 🤔
I'd just pass the MutableState into the Map() composable instead to work around a Compose bug with rememberUpdatedState() in this case. It's just that you recompose almost everything every time selectedLocationIndex changes when you pass the raw value. You will likely have fewer problems with android-maps-compose if you make its life a little easier, a.k.a. reduce recompositions.
I doubt it should be LaunchedEffect(pagerState.currentPage), but LaunchedEffect(pagerState) or even LaunchedEffect(Unit). Don't restart the LaunchedEffect based on a state change, you might get extra recompositions, etc.
I guess what I suggested about selectedLocationIndex may not be enough, as it's accessed everywhere anyway. You could create a boolean MutableState somewhere outside all those composables, outermost loop element, to track state of the current index to reduce recompositions. Mostly a potential workaround.
It's a derivedStateOf for index == selectedLocationIndex, but that only works if selectedLocationIndex accesses a State
I should stop commenting and leave it to the project maintainers. Your LaunchedEffect(pagerState.currentPage) was fine, actually, I just would instead do LaunchedEffect(pagerState) {...} with a snapshotFlow in the lambda. It's a bit unorthodox to recompose & re-launch a LaunchedEffect because of a state change.
This issue still persist, it appeared only when using new renderer
Hi @GSala . If you force the previous renderer, are you still experiencing the same issue? Some reports are mentioning the same flickering with the new renderer.