Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maps: Markers occasionally flicker when updating content while using zIndex #668

Open
GSala opened this issue Jan 7, 2025 · 9 comments
Open
Labels
triage me I really want to be triaged. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.

Comments

@GSala
Copy link

GSala commented Jan 7, 2025

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

@GSala GSala added triage me I really want to be triaged. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels Jan 7, 2025
@bubenheimer
Copy link
Contributor

bubenheimer commented Jan 7, 2025

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

@bubenheimer
Copy link
Contributor

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.

@GSala
Copy link
Author

GSala commented Jan 7, 2025

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:

Screen_recording_20250107_160116.mp4

Can you expand on how you would use rememberUpdatedState here?

@GSala
Copy link
Author

GSala commented Jan 7, 2025

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 🤔

@bubenheimer
Copy link
Contributor

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.

@bubenheimer
Copy link
Contributor

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.

@bubenheimer
Copy link
Contributor

It's a derivedStateOf for index == selectedLocationIndex, but that only works if selectedLocationIndex accesses a State

@bubenheimer
Copy link
Contributor

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.

@samuolis
Copy link

This issue still persist, it appeared only when using new renderer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
triage me I really want to be triaged. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Projects
None yet
Development

No branches or pull requests

3 participants