I currently have an Android application built with Jetpack Compose on the Play Store with android-maps-compose librairie. For several days now, I've been receiving reports of recurring crashes from the Play Store, but I'm unable to reproduce the issue on my end.
Here's the StackTrace:
Exception java.lang.IllegalStateException:
at com.google.maps.android.compose.MarkerState.setMarker$maps_compose_release (Marker.kt:29)
at com.google.maps.android.compose.MarkerNode.onAttached (Marker.kt:5)
at com.google.maps.android.compose.MapApplier.insertBottomUp (MapApplier.kt:13)
at com.google.maps.android.compose.MapApplier.insertBottomUp (MapApplier.kt:13)
at androidx.compose.runtime.changelist.Operation$PostInsertNodeFixup.execute (Operation.kt:26)
at androidx.compose.runtime.changelist.Operations.executeAndFlushAllPendingOperations (Operations.kt:23)
at androidx.compose.runtime.changelist.FixupList.executeAndFlushAllPendingFixups (FixupList.java:36)
at androidx.compose.runtime.changelist.Operation$InsertSlotsWithFixups.execute (Operation.kt:36)
at androidx.compose.runtime.changelist.Operations.executeAndFlushAllPendingOperations (Operations.kt:23)
at androidx.compose.runtime.changelist.ChangeList.executeAndFlushAllPendingChanges (ChangeList.kt:3)
at androidx.compose.runtime.CompositionImpl.applyChangesInLocked (Composition.kt:50)
at androidx.compose.runtime.CompositionImpl.applyChanges (Composition.kt:6)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke (Recomposer.kt:174)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke (Recomposer.kt:174)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame (AndroidUiFrameClock.android.kt:7)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch (AndroidUiDispatcher.java:48)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch (AndroidUiDispatcher.java:48)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame (AndroidUiDispatcher.android.kt:48)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1648)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1659)
at android.view.Choreographer.doCallbacks (Choreographer.java:1129)
at android.view.Choreographer.doFrame (Choreographer.java:1045)
at android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:1622)
at android.os.Handler.handleCallback (Handler.java:958)
at android.os.Handler.dispatchMessage (Handler.java:99)
at android.os.Looper.loopOnce (Looper.java:230)
at android.os.Looper.loop (Looper.java:319)
at android.app.ActivityThread.main (ActivityThread.java:8893)
at java.lang.reflect.Method.invoke
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:608)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1103)
Below is the code for my screen used to display the map (simplified to include only the map part):
@Composable
fun MyMapsView(
cameraPositionState: CameraPositionState,
) {
val viewModel: AdaptedMapViewModel = koinViewModel()
val darkTheme = appIsInDarkMode()
val markerPositionStates = remember {
mutableMapOf<Long, MarkerState>()
}
LaunchedEffect(Unit) {
viewModel.init(darkTheme)
}
LaunchedEffect(viewModel.markers.value) {
viewModel.updateMarkerToDisplay()
}
LaunchedEffect(viewModel.groups.value) {
viewModel.updateMarkerToDisplay()
}
LaunchedEffect(viewModel.sortMode.value) {
viewModel.updateMarkerToDisplay()
}
Box(Modifier.fillMaxSize()) {
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState,
uiSettings = MapUiSettings(
myLocationButtonEnabled = false, zoomControlsEnabled = false, mapToolbarEnabled = false
),
properties = MapProperties(
mapStyleOptions = MapStyleOptions.loadRawResourceStyle(
LocalContext.current, viewModel.mapStyle.value.rawResourceId
),
mapType = viewModel.mapType.value
)
) {
MapMarkersAndCircles(viewModel, markerPositionStates)
}
}
}
@Composable
private fun MapMarkersAndCircles(
viewModel: AdaptedMapViewModel, markerPositionStates: MutableMap<Long, MarkerState>
) {
viewModel.markersToDisplay.value.forEach { marker ->
if (markerPositionStates.containsKey(marker.id)) {
markerPositionStates[marker.id]!!.position = marker.latLng()!!
} else {
markerPositionStates.putIfAbsent(
marker.id, rememberMarkerState(
key = "${marker.id}",
position = marker.latLng()!!
)
)
}
MarkerComposable(
keys = arrayOf(marker.id),
tag = marker.id,
state = markerPositionStates[marker.id]!!,
anchor = if (marker.emoji.isNotEmpty()) Offset(0.5F, 0.5F) else Offset(0.5F, 1F),
visible = viewModel.groups.value.find { it.id == marker.groupId }?.enable ?: true && marker.active,
) {
Icon(
painter = painterResource(id = R.drawable.ic_marker_fill),
contentDescription = null,
tint = markerColor(viewModel.mapStyle.value),
modifier = Modifier.size(40.dp)
)
}
marker.circles.sortedByDescending { it.radius() }.forEach { circle ->
MapCircle(viewModel, marker, circle)
}
}
}
@Composable
private fun MapCircle(viewModel: AdaptedMapViewModel, marker: FullDataMarker, circle: FullDataCircle) {
return Circle(
center = marker.latLng()!!,
clickable = true,
visible = viewModel.groups.value.find { it.id == marker.groupId }?.enable ?: true && marker.active && circle.active,
fillColor = if (viewModel.circleStyle.value == CircleStyle.OUTLINE) Color.Transparent
else Color(circle.color).copy(alpha = 0.3f),
radius = circle.radius(),
)
}
fun moveToLocation(
coroutineScope: CoroutineScope, cameraPositionState: CameraPositionState, latLng: LatLng
) {
coroutineScope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLng(latLng), durationMs = 1000
)
}
}
And here's the code for the ViewModel (simplified to include only the map, markers, etc.):
class AdaptedMapViewModel(
private val userPreferencesManager: UserPreferencesManager,
private val appDataBaseManager: AppDataBaseManager,
) : ViewModel() {
private val _markersToDisplay = mutableStateOf((listOf<FullDataMarker>()))
val markersToDisplay: State<List<FullDataMarker>> = _markersToDisplay
private val _mapType = mutableStateOf(userPreferencesManager.defaultAppMapType())
val mapType: State<MapType> = _mapType
private val _mapStyle = mutableStateOf(MapStyle.STANDARD)
val mapStyle: State<MapStyle> = _mapStyle
private val _initiated = mutableStateOf(false)
val sortMode: State<SortOrder> = userPreferencesManager.sortOrder
val circleStyle = userPreferencesManager.circleStyle
val markers = appDataBaseManager.markers
val groups = appDataBaseManager.markerGroups
fun init(darkTheme: Boolean) {
if (_initiated.value) return
_mapStyle.value = userPreferencesManager.defaultAppMapStyle(darkTheme)
updateMarkerToDisplay()
_initiated.value = true
}
fun updateMarkerToDisplay() {
_markersToDisplay.value = markers.value
.generalOrder(null, groups.value, sortMode.value)
.filter { it.active }
}
}
I suspect the issue may arise from the fact that the list of markers to display evolves over time, but I've already tried several approaches, and this one seems to "work best".
I added a tag and a key, used a list of MarketState: markerPositionStates to avoid having multiple states per marker (which was previously the case). I'm not sure what else to test at this point.
After performing an update (for a completely different topic), the returned stack trace is no longer the same. (I also updated some implementations, but not android-maps-compose directly) Here it is:
Exception java.lang.IllegalStateException: MarkerState may only be associated with one Marker at a time.
at com.google.maps.android.compose.MarkerState.setMarker$maps_compose_release (Marker.kt:29)
at com.google.maps.android.compose.MarkerNode.onAttached (Marker.kt:5)
at com.google.maps.android.compose.MapApplier.insertBottomUp (MapApplier.kt:13)
at com.google.maps.android.compose.MapApplier.insertBottomUp (MapApplier.kt:13)
at androidx.compose.runtime.changelist.Operation$PostInsertNodeFixup.execute (Operation.kt:26)
at androidx.compose.runtime.changelist.Operations.executeAndFlushAllPendingOperations (Operations.kt:23)
at androidx.compose.runtime.changelist.FixupList.executeAndFlushAllPendingFixups (FixupList.java:36)
at androidx.compose.runtime.changelist.Operation$InsertSlotsWithFixups.execute (Operation.kt:36)
at androidx.compose.runtime.changelist.Operations.executeAndFlushAllPendingOperations (Operations.kt:23)
at androidx.compose.runtime.changelist.ChangeList.executeAndFlushAllPendingChanges (ChangeList.kt:3)
at androidx.compose.runtime.CompositionImpl.applyChangesInLocked (Composition.kt:50)
at androidx.compose.runtime.CompositionImpl.applyChanges (Composition.kt:6)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke (Recomposer.kt:174)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke (Recomposer.kt:174)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame (AndroidUiFrameClock.android.kt:7)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch (AndroidUiDispatcher.java:48)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch (AndroidUiDispatcher.java:48)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame (AndroidUiDispatcher.android.kt:48)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1648)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1659)
at android.view.Choreographer.doCallbacks (Choreographer.java:1129)
at android.view.Choreographer.doFrame (Choreographer.java:1045)
at android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:1622)
at android.os.Handler.handleCallback (Handler.java:958)
at android.os.Handler.dispatchMessage (Handler.java:99)
at android.os.Looper.loopOnce (Looper.java:230)
at android.os.Looper.loop (Looper.java:319)
at android.app.ActivityThread.main (ActivityThread.java:8893)
at java.lang.reflect.Method.invoke
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:608)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1103)
I'm not sure if the two are ultimately the same or not, but so far I've never had both at the same time. What I don't understand is that the only modification in the update has nothing to do with this part. So, I don't see why there would be a change in the stack trace.
The main problem is that I never manage to reproduce the bug on my side. Therefore, it's impossible for me to test before I find its origin (or else I deploy to a small portion of my audience to see if the problem persists).
If you're trying to change the state of a marker after it has been removed from the map or after the relevant Compose UI element has been disposed of, you'll encounter this exception. Ensure that you're not trying to modify the marker state after the marker or the Compose UI element has been destroyed.
You could try using key()
that is
viewModel.markersToDisplay.value.forEach { marker -> key(markerState) { //rest of code }}
but if that's not working then you could create a new MarkerState from existing one
state = MarkerState(position = LatLng(markerPositionStates[marker.id].latitude, markerPositionStates[marker.id].longitude))
So that it will create a unique MarkerState each time