Skip to content

fix(Android): don't run header update for a screen detached from its stack - #4498

Open
nsingh-ut wants to merge 1 commit into
software-mansion:mainfrom
nsingh-ut:fix/header-config-detached-screen-crash
Open

fix(Android): don't run header update for a screen detached from its stack#4498
nsingh-ut wants to merge 1 commit into
software-mansion:mainfrom
nsingh-ut:fix/header-config-detached-screen-crash

Conversation

@nsingh-ut

Copy link
Copy Markdown

Description

ScreenStackHeaderConfig.onUpdate() and ScreenStackFragment.canNavigateBack() disagree about the same condition, so a screen detached from its stack while its header config is still attached crashes the app.

onUpdate() computes:

val stack = screenStack                                   // screen?.container as? ScreenStack
val isTop = stack == null || stack.topScreen == parent
if (!isAttachedToWindow || !isTop || isDestroyed) return

A null screenStack is treated as "is top", so execution continues. Further down it reaches:

newActionBar.setDisplayHomeAsUpEnabled(
    screenFragment?.canNavigateBack() == true && !isBackButtonHidden,
)

and canNavigateBack() asserts on exactly the state the first guard let through:

val container: ScreenContainer? = screen.container
check(container is ScreenStack) { "ScreenStackFragment added into a non-stack container" }

Both read the same Screen.container field. If it is not a ScreenStack, the permissive check passes and the strict one throws.

This is what users hit in #4429: an intermittent IllegalStateException on Android/Fabric during ordinary navigation, when a props update or an attach reaches a header config after its screen has left the stack.

Closes #4429.

Changes

  • legacy/ScreenStackHeaderConfig.onUpdate() returns early when screenStack is null, instead of treating it as "is top".

Scoped to the legacy implementation deliberately: the newer stack/ code passes canNavigateBack to StackScreenFragment as a constructor value and has no equivalent assertion, so it is unaffected.

Test plan

The crash is a race and does not reproduce on demand, so I made the transient state permanent for one screen and did an A/B.

In onUpdate(), before the guard, I detached one screen from its stack. That is the same field both accessors read, so it reproduces the real end state:

if (title == "Security") {
    screen?.container = null
}

Built a debug app (RN 0.86, Expo 57, Fabric, react-native-screens 4.25.2, Android 16 emulator) and navigated to that screen.

1. Without this change: crashes every time.

java.lang.IllegalStateException: ScreenStackFragment added into a non-stack container
    at com.swmansion.rnscreens.ScreenStackFragment.canNavigateBack(ScreenStackFragment.kt:499)
    at com.swmansion.rnscreens.ScreenStackHeaderConfig.onUpdate(ScreenStackHeaderConfig.kt:268)
    at com.swmansion.rnscreens.ScreenStackHeaderConfig.onAttachedToWindow(ScreenStackHeaderConfig.kt:183)
    at android.view.View.dispatchAttachedToWindow(View.java:23105)

This matches the trace in #4429, which arrives via onAfterUpdateTransaction instead of onAttachedToWindow. Both call onUpdate(), so the guard covers both entry points.

2. With this change, detach still forced: no crash, screen renders. Its header is skipped, which is correct, since a screen with no stack has no stack header to configure.

3. With this change, detach removed: normal behaviour, title and back button render as before.

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change. (No visual change: this removes a crash.)
  • For API changes, updated relevant public types. (No API change.)
  • Ensured that CI passes

…stack

`ScreenStackHeaderConfig.onUpdate()` computed `isTop` as
`stack == null || stack.topScreen == parent`, so a null `screenStack` was
treated as "is top" and execution continued. It then reached
`ScreenStackFragment.canNavigateBack()`, which does
`check(container is ScreenStack)` and throws on exactly the state the first
guard allowed through.

A screen detached from its stack while its header config is still attached
therefore crashed with `IllegalStateException: ScreenStackFragment added
into a non-stack container`.

Return early when there is no stack, so both checks agree.

Closes software-mansion#4429
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] IllegalStateException in ScreenStackFragment.canNavigateBack: fragment added into a non-stack container

1 participant