diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt index 65c6e304da..b6af7755ef 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt @@ -316,7 +316,8 @@ open class ScreenFragment : override fun onDestroy() { super.onDestroy() val container = screen.container - if (container == null || !container.hasScreen(this.screen.fragmentWrapper)) { + val isScreenDismissed = container == null || !container.hasScreen(this.screen.fragmentWrapper) + if (isScreenDismissed) { // we only send dismissed even when the screen has been removed from its container val screenContext = screen.context if (screenContext is ReactContext) { @@ -325,6 +326,16 @@ open class ScreenFragment : .getEventDispatcherForReactTag(screenContext, screen.id) ?.dispatchEvent(ScreenDismissedEvent(surfaceId, screen.id)) } + // Break the Screen->fragmentWrapper retain cycle so Fabric's + // SurfaceMountingManager.mTagToViewState doesn't keep the destroyed + // ScreenFragment alive. Gated on isScreenDismissed since onDestroy + // can also fire in non-dismissal lifecycle paths where the wrapper + // must remain reachable. Identity guard avoids clobbering a wrapper + // that has been re-assigned to a different fragment. + // See: https://github.com/software-mansion/react-native-screens/issues/3755 + if (this.screen.fragmentWrapper === this) { + this.screen.fragmentWrapper = null + } } childScreenContainers.clear() } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt index 2987072537..c0664540ff 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt @@ -95,6 +95,19 @@ open class ScreenViewManager : view.onFinalizePropsUpdate() } + // Belt-and-braces fragmentWrapper cleanup, in case Fabric drops the view + // without ScreenFragment.onDestroy firing on schedule. Breaks the + // Screen->fragmentWrapper retain cycle so SurfaceMountingManager doesn't + // keep the destroyed fragment alive. Guarded so we only null when no + // fragment is currently attached. + // See: https://github.com/software-mansion/react-native-screens/issues/3755 + override fun onDropViewInstance(view: Screen) { + if (view.fragmentWrapper?.fragment?.isAdded != true) { + view.fragmentWrapper = null + } + super.onDropViewInstance(view) + } + fun setActivityState( view: Screen, activityState: Int,