Skip to content

fix(Android, Stack v4+): transition RecyclerView as a unit on screen removal - #4555

Open
drhops wants to merge 1 commit into
software-mansion:mainfrom
drhops:fix/android-recyclerview-removal-transition
Open

fix(Android, Stack v4+): transition RecyclerView as a unit on screen removal#4555
drhops wants to merge 1 commit into
software-mansion:mainfrom
drhops:fix/android-recyclerview-removal-transition

Conversation

@drhops

@drhops drhops commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Note: this fix was discovered via callstack/react-native-pager-view#1005 (comment) and all credit goes to @mozzius

Description

When a screen is removed, Screen.startTransitionRecursive calls startViewTransition on every descendant so the outgoing screen keeps drawing through the exit animation. That works by delaying ViewGroup's detachment contract and the transitioning view keeps its parent when it is removed:

// ViewGroup.removeFromArray
if (!(mTransitioningViews != null && mTransitioningViews.contains(children[index]))) {
    children[index].mParent = null;      // skipped while transitioning
}

RecyclerView owns the attach/detach lifecycle of its children, and rejects any holder whose parent is still set:

// RecyclerView.Recycler.recycleViewHolderInternal
if (holder.isScrap() || holder.itemView.getParent() != null) {
    throw new IllegalArgumentException("Scrapped or attached views may not be recycled. ...");
}

So once a screen removal has marked a ViewPager2 page, the next recycle throws:

java.lang.IllegalArgumentException: Scrapped or attached views may not be recycled.
  isScrap:false isAttached:true androidx.viewpager2.widget.ViewPager2$RecyclerViewImpl{...},
  adapter:com.reactnativepagerview.ViewPagerAdapter@...

  at ...RecyclerView$Recycler.recycleViewHolderInternal(RecyclerView.java:7071)
  at ...RecyclerView$LayoutManager.removeAndRecycleViewAt(RecyclerView.java:9741)
  at ...LinearLayoutManager.recycleChildren(LinearLayoutManager.java:1464)
  at ...LinearLayoutManager.fill(LinearLayoutManager.java:1613)
  at ...RecyclerView.scrollStep(RecyclerView.java:2047)
  at ...RecyclerView$ViewFlinger.run(RecyclerView.java:5835)

Repro Steps:

Setup: a RecyclerView inside of a native stack:

  1. A native stack. ScreenStackViewManager.removeViewAt is the only caller of startRemovalTransition()
  2. A RecyclerView anywhere in the popped screen's subtree. Depth is irrelevant and <PagerView> is the example in our app.
  3. ≥3 pages, so a page actually leaves the retained window and recycleChildren runs.

Minimal test screen

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import { Button, Text, View } from 'react-native';
import PagerView from 'react-native-pager-view';

const Stack = createNativeStackNavigator();

function Home({ navigation }) {
  return <Button title="Open pager" onPress={() => navigation.navigate('Pager')} />;
}

function Pager() {
  return (
    <PagerView style={{ flex: 1 }} initialPage={0}>
      {Array.from({ length: 6 }, (_, i) => (
        <View key={i} style={{ flex: 1, backgroundColor: i % 2 ? '#cde' : '#edc' }}>
          <Text style={{ fontSize: 64, textAlign: 'center', marginTop: 240 }}>{i}</Text>
        </View>
      ))}
    </PagerView>
  );
}

export default function Test2461() {
  return (
    <NavigationContainer independent>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={Home} />
        <Stack.Screen name="Pager" component={Pager} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

On Android: open the pager screen, drag horizontally and release mid-page, then immediately press Back while it's still gliding. Use a back button rather than back gesture. Repeat a couple times given the animation settle window is only a few hundred milliseconds.

Notes

Fixes the crash reported in #2461 (closed without a fix).
Downstream: callstack/react-native-pager-view#1005.

Changes

  • legacy/Screen.kt: skip recursion into a RecyclerView in startTransitionRecursive, and mirror
    the skip in endTransitionRecursive.
  • android/build.gradle: declare androidx.recyclerview:recyclerview explicitly. It was already on
    the compile classpath transitively via Material, but the new import should not rely on that. Pinned
    at 1.1.0, which is what Material already resolves.

…removal

`startTransitionRecursive` marks every descendant of an outgoing screen
with `startViewTransition`. RecyclerView owns the attach/detach lifecycle
of its children and rejects a holder whose parent is still set, so a
ViewPager2 that recycles a page while the exit animation runs crashes
with "Scrapped or attached views may not be recycled".

Skip recursion into a RecyclerView. It is still marked by its own parent,
so the subtree keeps drawing and stays attached; only the per-child
marking goes away.
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a5502953-d3fe-445f-9146-31897161534c

📥 Commits

Reviewing files that changed from the base of the PR and between 2f0e68f and 9333fb2.

📒 Files selected for processing (2)
  • android/build.gradle
  • android/src/main/java/com/swmansion/rnscreens/legacy/Screen.kt

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Changes

RecyclerView transition handling

Layer / File(s) Summary
Atomic RecyclerView transitions
android/build.gradle, android/src/main/java/com/swmansion/rnscreens/legacy/Screen.kt
The library adds AndroidX RecyclerView 1.1.0. Legacy transition start and end recursion now excludes RecyclerView children and processes each RecyclerView as a single unit.

Estimated code review effort: 2 (Simple) | ~5 minutes

Merge Risk: ⚪ Minimal · up to 9333f

This localized Android change adjusts screen-removal transitions and declares the required RecyclerView dependency; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: kmichalikk, t0maboro

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 1 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main Android fix: transitioning RecyclerView as a unit when removing a screen.
Description check ✅ Passed The description directly explains the RecyclerView recycling crash, provides reproduction details, and documents the code and dependency changes.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 1 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kkafar
kkafar requested review from kkafar and a balanced review from Copilot August 26, 2026 12:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an Android native-stack crash by treating RecyclerView as a single transition unit during screen removal.

Changes:

  • Stops transition recursion into RecyclerView children.
  • Adds an explicit RecyclerView dependency.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
android/src/main/java/com/swmansion/rnscreens/legacy/Screen.kt Skips RecyclerView descendants during transition start and end.
android/build.gradle Declares the RecyclerView dependency explicitly.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@kkafar kkafar self-assigned this Aug 26, 2026
@kkafar kkafar added the action:backport-to-v4 Add this label to any issue or PR that should be backported to the v4 line of the library. label Sep 2, 2026
Comment thread android/build.gradle
implementation 'androidx.fragment:fragment-ktx:1.8.9'
implementation 'androidx.transition:transition-ktx:1.7.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is less than perfect and what actually stops me from landing this change.

Additional dependency just for a symbol is a lot.

What's the impact of this on package size?
Also why outdated 1.1.0, when newer versions are available?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kkafar this dependency is already included transitively, so this change just makes it direct/explicit given we now rely on it in the code. 1.1.0 because that's what was already being included transitively, and it shouldn't affect package size at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action:backport-to-v4 Add this label to any issue or PR that should be backported to the v4 line of the library.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants