fix(map): remove manual ViewTree lifecycle owner workarounds (#5704)

This commit is contained in:
James Rich
2026-06-01 14:36:03 -05:00
committed by GitHub
parent 1854bc8c2b
commit be6f2cfb71
2 changed files with 0 additions and 88 deletions
@@ -17,18 +17,7 @@
package org.meshtastic.app.map.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.currentStateAsState
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.view.DefaultClusterRenderer
import com.google.maps.android.compose.Circle
@@ -47,50 +36,6 @@ fun NodeClusterMarkers(
navigateToNodeDetails: (Int) -> Unit,
onClusterClick: (Cluster<NodeClusterItem>) -> Boolean,
) {
val view = LocalView.current
val lifecycleOwner = LocalLifecycleOwner.current
val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState()
// Workaround for https://github.com/googlemaps/android-maps-compose/issues/858
// and https://github.com/googlemaps/android-maps-compose/issues/875
// The maps clustering library creates an internal ComposeView to snapshot markers.
// If that view is not attached to the hierarchy (which it often isn't during rendering),
// it fails to find the Lifecycle and SavedState owners. We propagate them to the root view
// so the internal snapshot view can find them when walking up the tree.
// DisposableEffect runs at composition time (not post-composition like SideEffect),
// ensuring owners are set before the Clustering composable triggers marker rendering.
// Capture and restore the previous owners on dispose. The owners here are NavEntry-scoped
// (transient) lifecycles; leaving one attached to the activity root view after this screen is
// destroyed makes subsequently opened Popups/DropdownMenus inherit a DESTROYED lifecycle and
// render at 0x0 (invisible). See the node-list popup regression and InlineMap.
DisposableEffect(lifecycleOwner, savedStateRegistryOwner) {
val root = view.rootView
val prevRootLifecycleOwner = root.findViewTreeLifecycleOwner()
val prevRootSavedStateRegistryOwner = root.findViewTreeSavedStateRegistryOwner()
root.setViewTreeLifecycleOwner(lifecycleOwner)
root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
// Also set on the view itself in case the internal renderer walks from a child
val prevViewLifecycleOwner = view.findViewTreeLifecycleOwner()
val prevViewSavedStateRegistryOwner = view.findViewTreeSavedStateRegistryOwner()
if (view !== root) {
view.setViewTreeLifecycleOwner(lifecycleOwner)
view.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
}
onDispose {
root.setViewTreeLifecycleOwner(prevRootLifecycleOwner)
root.setViewTreeSavedStateRegistryOwner(prevRootSavedStateRegistryOwner)
if (view !== root) {
view.setViewTreeLifecycleOwner(prevViewLifecycleOwner)
view.setViewTreeSavedStateRegistryOwner(prevViewSavedStateRegistryOwner)
}
}
}
// Guard against the cluster renderer's async Handler trying to render markers
// after the lifecycle has stopped — the internal ComposeView requires an active lifecycle.
if (!lifecycleState.isAtLeast(Lifecycle.State.STARTED)) return
Clustering(
items = nodeClusterItems,
onClusterClick = onClusterClick,
@@ -18,17 +18,9 @@ package org.meshtastic.app.node.component
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.Circle
@@ -52,31 +44,6 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
true -> ComposeMapColorScheme.DARK
else -> ComposeMapColorScheme.LIGHT
}
// Defensive workaround: propagate ViewTreeLifecycleOwner to the root view so that
// any internal maps-compose ComposeView (e.g., info windows) can find the lifecycle
// when walking up the view tree.
//
// IMPORTANT: capture and restore the previous owners on dispose. This InlineMap is hosted inside the
// node-detail NavEntry, whose LocalLifecycleOwner is a transient, entry-scoped lifecycle. Leaving it
// attached to the activity root view after the entry is destroyed (e.g. navigating back to the node
// list) would make subsequently opened Popups/DropdownMenus inherit a DESTROYED lifecycle and
// render at 0x0 (invisible). See the node-list popup regression.
val view = LocalView.current
val lifecycleOwner = LocalLifecycleOwner.current
val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
DisposableEffect(lifecycleOwner, savedStateRegistryOwner) {
val root = view.rootView
val prevRootLifecycleOwner = root.findViewTreeLifecycleOwner()
val prevRootSavedStateRegistryOwner = root.findViewTreeSavedStateRegistryOwner()
root.setViewTreeLifecycleOwner(lifecycleOwner)
root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
onDispose {
root.setViewTreeLifecycleOwner(prevRootLifecycleOwner)
root.setViewTreeSavedStateRegistryOwner(prevRootSavedStateRegistryOwner)
}
}
key(node.num) {
val location = LatLng(node.latitude, node.longitude)
val cameraState = rememberCameraPositionState {