From a89434b5b52b87f99ad34e92a09bb27b9e302a96 Mon Sep 17 00:00:00 2001 From: Oleksii Bulenok Date: Tue, 20 Oct 2026 13:47:24 +0200 Subject: [PATCH] fix(android): keep the pager's Compose state out of the host's saved state The pager's internal ComposeView has no view id, so Compose saved its rememberSaveable state (the pager's current page, the page hosts' view state) in the host Fragment's SavedStateRegistry under the shared key "SaveableStateRegistry:-1". When react-native-screens destroys the Fragment view of a covered screen, that state is saved, and a pager mounted afterwards restores it: it starts on the old page and ignores initialPage. Give the ComposeView a SavedStateRegistryOwner from the same private owner that already provides its Lifecycle. Its registry starts empty and is never saved, so nothing is persisted or restored. React owns the pager state, so nothing is lost. A unique view id would also avoid the collision, but every remount would leave an orphaned entry in the Fragment's saved state. Co-Authored-By: Claude Opus 5.5 --- .../reactnativepagerview/ComposePagerView.kt | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt b/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt index 814b7df3..0426f522 100644 --- a/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt +++ b/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt @@ -33,6 +33,10 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.NativeGestureUtil @@ -99,6 +103,13 @@ class ComposePagerView(context: Context) : FrameLayout(context) { // resolves it instead of walking up to the ambient (and repeatedly // destroyed) Fragment view-tree Lifecycle - see the field comment above. setViewTreeLifecycleOwner(composeLifecycleOwner) + // Same for saved state. Compose would otherwise store the pager's + // rememberSaveable state (current page, page view state) in the host + // Fragment's SavedStateRegistry under a key derived from this view's + // id. It has none, so every pager shares one key and a remounted pager + // restores the previous pager's page instead of using initialPage. + // React owns the pager state, so the owner's registry is never saved. + setViewTreeSavedStateRegistryOwner(composeLifecycleOwner) setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnLifecycleDestroyed(composeLifecycleOwner) ) @@ -687,18 +698,29 @@ class ComposePagerView(context: Context) : FrameLayout(context) { // A stable Lifecycle that follows the currently attached host through // CREATED/STARTED/RESUMED, but only moves to DESTROYED when destroy() is // called explicitly. This lets it survive react-native-screens replacing the -// ambient Fragment view-tree owner (see the field comment above). -internal class ComposeViewLifecycleOwner : LifecycleOwner, LifecycleEventObserver { - private val registry = LifecycleRegistry(this).apply { - currentState = Lifecycle.State.CREATED - } +// ambient Fragment view-tree owner (see the field comment above). It also +// provides an empty SavedStateRegistry that is never saved, so nothing in the +// composition is persisted or restored. +internal class ComposeViewLifecycleOwner : + LifecycleOwner, SavedStateRegistryOwner, LifecycleEventObserver { + private val registry = LifecycleRegistry(this) + private val savedStateController = SavedStateRegistryController.create(this) private var hostLifecycle: Lifecycle? = null private var isAttached = false private var isDestroyed = false + init { + // Must run while the Lifecycle is still INITIALIZED. + savedStateController.performRestore(null) + registry.currentState = Lifecycle.State.CREATED + } + override val lifecycle: Lifecycle get() = registry + override val savedStateRegistry: SavedStateRegistry + get() = savedStateController.savedStateRegistry + fun attach(lifecycle: Lifecycle?) { if (isDestroyed) { return