Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions android/src/main/java/com/reactnativepagerview/ComposePagerView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions e2e/tests/issues/pr-1194-remount-initial-page.e2e.ts
Original file line number Diff line number Diff line change
@@ -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();
}
);
1,238 changes: 1,238 additions & 0 deletions example/dist/main.android.jsbundle

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions example/dist/res/raw/keep.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@drawable/assets_urbanears_blue,@drawable/assets_urbanears_pink,@drawable/assets_urbanears_grey,@drawable/assets_urbanears_mint,@drawable/assets_ue_black_logo,@drawable/assets_tabview_avatar1,@drawable/assets_tabview_book,@drawable/assets_tabview_avatar2,@drawable/assets_tabview_albumart1,@drawable/assets_tabview_albumart2,@drawable/assets_tabview_albumart3,@drawable/assets_tabview_albumart4,@drawable/assets_tabview_albumart5,@drawable/assets_tabview_albumart6,@drawable/assets_tabview_albumart7,@drawable/assets_tabview_albumart8,@drawable/_node_modules_reactnavigation_elements_lib_module_assets_searchicon,@drawable/_node_modules_reactnavigation_elements_lib_module_assets_backicon,@drawable/_node_modules_reactnavigation_elements_lib_module_assets_backiconmask,@drawable/_node_modules_reactnavigation_elements_lib_module_assets_clearicon,@drawable/_node_modules_reactnavigation_elements_lib_module_assets_closeicon" />
12 changes: 12 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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',
},
],
},
];
Expand Down Expand Up @@ -308,6 +316,10 @@ export function Navigation() {
name="Issue #1142 Pager List"
component={Issue1142PagerListScreen}
/>
<NavigationStack.Screen
name="PR #1194 Remount InitialPage Cover"
component={PR1194RemountInitialPageCoverScreen}
/>
</NavigationStack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
Expand Down
105 changes: 105 additions & 0 deletions example/src/gh-issues/PR1194RemountInitialPageRepro.tsx
Original file line number Diff line number Diff line change
@@ -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<PagerView>(null);
const [mount, setMount] = React.useState({ key: 0, initialPage: 0 });
const navigation =
useNavigation<
NativeStackNavigationProp<
PR1194RemountInitialPageParamList,
'PR #1194 Remount InitialPage Repro'
>
>();

return (
<SafeAreaView style={styles.flex}>
<View style={styles.controls}>
<Text testID="pr-1194-label" style={styles.label}>
Mount {mount.key}, initialPage {mount.initialPage}
</Text>
<Button
testID="pr-1194-go-to-2"
title="Go to page 2"
onPress={() => pagerRef.current?.setPage(2)}
/>
<Button
testID="pr-1194-open-cover"
title="Open cover screen"
onPress={() =>
navigation.navigate('PR #1194 Remount InitialPage Cover')
}
/>
<Button
testID="pr-1194-remount"
title="Remount with initialPage 1"
onPress={() =>
setMount(({ key }) => ({ key: key + 1, initialPage: 1 }))
}
/>
</View>
<PagerView
key={mount.key}
ref={pagerRef}
style={styles.flex}
initialPage={mount.initialPage}
>
{PAGES.map((label, i) => (
<View
key={label}
testID={`pr-1194-page-${i}`}
style={[styles.page, { backgroundColor: PAGE_COLORS[i] }]}
>
<Text style={styles.pageLabel}>{label}</Text>
</View>
))}
</PagerView>
</SafeAreaView>
);
}

export function PR1194RemountInitialPageCoverScreen() {
const navigation = useNavigation();

return (
<SafeAreaView style={styles.flex}>
<View style={styles.controls}>
<Text style={styles.label}>Cover screen</Text>
<Button
testID="pr-1194-close-cover"
title="Go back"
onPress={() => navigation.goBack()}
/>
</View>
</SafeAreaView>
);
}

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' },
});