diff --git a/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt b/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt
index 068b4e1d..59e02eba 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)
)
@@ -692,18 +703,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
diff --git a/e2e/tests/issues/pr-1194-remount-initial-page.e2e.ts b/e2e/tests/issues/pr-1194-remount-initial-page.e2e.ts
new file mode 100644
index 00000000..df7facac
--- /dev/null
+++ b/e2e/tests/issues/pr-1194-remount-initial-page.e2e.ts
@@ -0,0 +1,30 @@
+import { freshAppBeforeEach, expect, test } from '../support/test.ts';
+import { openExample } from '../support/app.ts';
+
+freshAppBeforeEach();
+
+test(
+ 'a pager remounted after its screen was covered starts on its own initialPage',
+ { tags: ['regression', 'pr-1194'] },
+ async ({ app, screen }) => {
+ await openExample(app, screen, 'PR #1194 Remount InitialPage Repro');
+ await expect(screen.getByTestId('pr-1194-page-0')).toBeVisible();
+
+ await screen.getByTestId('pr-1194-go-to-2').tap();
+ await expect(screen.getByTestId('pr-1194-page-2')).toBeVisible();
+
+ // Covering the screen makes react-native-screens save its Fragment view
+ // state, which used to include the pager's current page.
+ await screen.getByTestId('pr-1194-open-cover').tap();
+ await expect(screen.getByText('Cover screen')).toBeVisible();
+ await screen.getByTestId('pr-1194-close-cover').tap();
+ await expect(screen.getByTestId('pr-1194-page-2')).toBeVisible();
+
+ await screen.getByTestId('pr-1194-remount').tap();
+ await expect(screen.getByTestId('pr-1194-label')).toHaveText(
+ 'Mount 1, initialPage 1'
+ );
+ await expect(screen.getByTestId('pr-1194-page-1')).toBeVisible();
+ await expect(screen.getByTestId('pr-1194-page-2')).toBeHidden();
+ }
+);
diff --git a/example/src/App.tsx b/example/src/App.tsx
index c86465f1..15a173df 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -55,6 +55,10 @@ import { Issue1154PageChangeFocusRepro } from './gh-issues/Issue1154PageChangeFo
import { Issue1156NestedHorizontalScrollRepro } from './gh-issues/Issue1156NestedHorizontalScrollRepro';
import { Issue424SwipeCancelsPressRepro } from './gh-issues/Issue424SwipeCancelsPressRepro';
import { Issue965PagesBeforeCurrentRepro } from './gh-issues/Issue965PagesBeforeCurrentRepro';
+import {
+ PR1194RemountInitialPageCoverScreen,
+ PR1194RemountInitialPageRepro,
+} from './gh-issues/PR1194RemountInitialPageRepro';
import { colors, radius } from './theme';
function BasicPagerViewExampleScreen() {
@@ -179,6 +183,10 @@ const sections: { title: string; data: Example[] }[] = [
component: Issue965PagesBeforeCurrentRepro,
name: 'Issue #965 Pages Before Current Repro',
},
+ {
+ component: PR1194RemountInitialPageRepro,
+ name: 'PR #1194 Remount InitialPage Repro',
+ },
],
},
];
@@ -308,6 +316,10 @@ export function Navigation() {
name="Issue #1142 Pager List"
component={Issue1142PagerListScreen}
/>
+
diff --git a/example/src/gh-issues/PR1194RemountInitialPageRepro.tsx b/example/src/gh-issues/PR1194RemountInitialPageRepro.tsx
new file mode 100644
index 00000000..19d741f6
--- /dev/null
+++ b/example/src/gh-issues/PR1194RemountInitialPageRepro.tsx
@@ -0,0 +1,105 @@
+/**
+ * Repro for #1194: the Android pager restored a previous pager's page
+ * instead of using initialPage. The pager's ComposeView saved its
+ * rememberSaveable state in the host Fragment's SavedStateRegistry under a key
+ * shared by every pager. Covering the screen with a native-stack push saves
+ * that state, and the next pager mounted on the screen restored it.
+ *
+ * Steps: go to page 2, open the cover screen, come back, remount. The remounted
+ * pager must start on page 1, its initialPage.
+ */
+import * as React from 'react';
+import { Button, SafeAreaView, StyleSheet, Text, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { type NativeStackNavigationProp } from '@react-navigation/native-stack';
+import PagerView from 'react-native-pager-view';
+
+export type PR1194RemountInitialPageParamList = {
+ 'PR #1194 Remount InitialPage Repro': undefined;
+ 'PR #1194 Remount InitialPage Cover': undefined;
+};
+
+const PAGES = ['Page 0', 'Page 1', 'Page 2'];
+const PAGE_COLORS = ['#fde2e2', '#e2fde6', '#e2e8fd'];
+
+export function PR1194RemountInitialPageRepro() {
+ const pagerRef = React.useRef(null);
+ const [mount, setMount] = React.useState({ key: 0, initialPage: 0 });
+ const navigation =
+ useNavigation<
+ NativeStackNavigationProp<
+ PR1194RemountInitialPageParamList,
+ 'PR #1194 Remount InitialPage Repro'
+ >
+ >();
+
+ return (
+
+
+
+ Mount {mount.key}, initialPage {mount.initialPage}
+
+
+
+ {PAGES.map((label, i) => (
+
+ {label}
+
+ ))}
+
+
+ );
+}
+
+export function PR1194RemountInitialPageCoverScreen() {
+ const navigation = useNavigation();
+
+ return (
+
+
+ Cover screen
+ navigation.goBack()}
+ />
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ flex: { flex: 1 },
+ controls: { padding: 16, gap: 8 },
+ label: { fontSize: 14, color: '#444' },
+ page: { flex: 1, alignItems: 'center', justifyContent: 'center' },
+ pageLabel: { fontSize: 32, fontWeight: '600' },
+});