fix(node): restore view-tree owners on map dispose so node-list popups aren't invisible (#5699)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
James Rich
2026-06-01 05:31:58 -07:00
committed by GitHub
parent d264b40862
commit 63ff12dac3
2 changed files with 34 additions and 7 deletions
@@ -24,8 +24,10 @@ 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
@@ -58,16 +60,31 @@ fun NodeClusterMarkers(
// 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 {}
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
@@ -26,8 +26,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
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
@@ -55,20 +57,28 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
else -> ComposeMapColorScheme.LIGHT
}
// Workaround for maps-compose issue where MarkerComposable's internal ComposeView
// cannot find ViewTreeLifecycleOwner, causing crash on bitmap rendering.
// Workaround for a maps-compose issue where MarkerComposable's internal ComposeView cannot find a
// ViewTreeLifecycleOwner, causing a crash on bitmap rendering. We attach the current owners to the
// window root view for the lifetime of the map.
//
// 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 every subsequently opened Popup/DropdownMenu 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)
if (view !== root) {
view.setViewTreeLifecycleOwner(lifecycleOwner)
view.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
onDispose {
root.setViewTreeLifecycleOwner(prevRootLifecycleOwner)
root.setViewTreeSavedStateRegistryOwner(prevRootSavedStateRegistryOwner)
}
onDispose {}
}
key(node.num) {