Skip to content

Crash on Android: "Fragment already added" in ScreenStack.onUpdate with transparentModal after background/resume #4156

@JacquesLeupin

Description

@JacquesLeupin

Description

We get a steady stream of these crashes in production (~150 events / 135 users over the last 30 days, Android only):

java.lang.IllegalStateException: Fragment already added: ScreenStackFragment{62f0b31} (e990c58a-0b75-420f-9387-1d7d25cde9d6 id=0x831)

It always happens with a transparentModal screen on top of a native stack, typically right after the app was backgrounded and resumed (crash telemetry shows onStoponSaveInstanceState → trim memory → onResume, then a navigation to the transparent screen a few seconds later). We have enableFreeze(true) set.

The crash comes from the re-attach branch in ScreenStack.onUpdate. When the top screen is translucent and visibleBottom's fragment is not added, every wrapper from visibleBottom upward gets re-added without checking isAdded:

if (visibleBottom != null && !visibleBottom.fragment.isAdded) {
    val top = newTop
    screenWrappers
        .asSequence()
        .dropWhile { it !== visibleBottom }
        .forEach { wrapper ->
            transaction.add(id, wrapper.fragment).runOnCommit { ... }
        }
}

So if the screen under the transparent modal got detached while the modal's own fragment stayed attached, the loop re-adds the still-attached fragment and FragmentStore.addFragment throws. The sibling newTop branch right below does guard with !newTop.fragment.isAdded.

Looks closely related to #2627 (same exception with formSheet, fixed in 4.7.0) — a commenter there reported the transparent-modal flavor still crashing afterwards, which matches what we see.

Full stack trace
java.lang.IllegalStateException: Fragment already added: ScreenStackFragment{62f0b31} (e990c58a-0b75-420f-9387-1d7d25cde9d6 id=0x831)
    androidx.fragment.app.FragmentStore.addFragment(FragmentStore.java:93)
    androidx.fragment.app.FragmentManager.addFragment(FragmentManager.java:1733)
    androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:389)
    androidx.fragment.app.FragmentManager.executeOps(FragmentManager.java:2280)
    androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2165)
    androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2115)
    androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:2002)
    androidx.fragment.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:323)
    com.swmansion.rnscreens.ScreenStack.onUpdate(ScreenStack.kt:298)
    com.swmansion.rnscreens.ScreenContainer.performUpdates(ScreenContainer.kt:376)
    com.swmansion.rnscreens.ScreensShadowNode.onBeforeLayout$lambda$0(ScreensShadowNode.kt:20)
    com.facebook.react.uimanager.UIViewOperationQueue$UIBlockOperation.execute(UIViewOperationQueue.java:538)
    com.facebook.react.uimanager.UIViewOperationQueue$1.run(UIViewOperationQueue.java:889)
    com.facebook.react.uimanager.UIViewOperationQueue.flushPendingBatches(UIViewOperationQueue.java:999)
    com.facebook.react.uimanager.UIViewOperationQueue$DispatchUIFrameCallback.doFrameGuarded(UIViewOperationQueue.java:1056)
    com.facebook.react.uimanager.GuardedFrameCallback.doFrame(GuardedFrameCallback.kt:25)
    com.facebook.react.modules.core.ReactChoreographer.frameCallback$lambda$1(ReactChoreographer.kt:59)
    android.view.Choreographer$CallbackRecord.run(Choreographer.java:1645)
    android.view.Choreographer.doCallbacks(Choreographer.java:1252)
    android.view.Choreographer.doFrame(Choreographer.java:1177)
    android.os.Handler.dispatchMessage(Handler.java:125)
    android.os.Looper.loop(Looper.java:367)
    android.app.ActivityThread.main(ActivityThread.java:9333)

Steps to reproduce

We haven't found a deterministic repro — it's a race around fragment attachment state. The shape from telemetry is consistently:

  1. Native stack with opaque screens, push a screen with presentation: 'transparentModal'
  2. Background the app (recent apps), wait long enough for onSaveInstanceState, resume
  3. Trigger navigation / state changes that re-run onUpdate shortly after resume

PR with a guard for the add loop incoming — happy to validate patches against our production app, since we can observe the crash rate there.

Snack or a link to a repository

No reliable minimal repro (race condition, see above)

Screens version

4.22.0 (the add loop is unchanged on current main)

React Native version

0.81.5

Platforms

Android

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Architecture

Paper (Old Architecture)

Build type

Release mode

Device

Real device

Device model

Various; e.g. Pixel 9 Pro (Android 16)

Acknowledgements

Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    missing-reproThis issue need minimum repro scenarioplatform:androidIssue related to Android part of the library

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions