Skip to content

fix(Android): do not require an activity to register sheet insets observer - #4531

Open
Bram-dc wants to merge 1 commit into
software-mansion:mainfrom
Bram-dc:fix/sheet-insets-observer-detached-activity
Open

fix(Android): do not require an activity to register sheet insets observer#4531
Bram-dc wants to merge 1 commit into
software-mansion:mainfrom
Bram-dc:fix/sheet-insets-observer-detached-activity

Conversation

@Bram-dc

@Bram-dc Bram-dc commented Aug 20, 2026

Copy link
Copy Markdown

Description

SheetDelegate resolves the decor view through checkNotNull(screen.reactContext.currentActivity) in the host fragment's ON_START:

private fun requireDecorView(): View =
    checkNotNull(screen.reactContext.currentActivity) { "[RNScreens] Attempt to access activity on detached context" }
        .window.decorView

private fun handleHostFragmentOnStart() {
    InsetsObserverProxy.registerOnView(requireDecorView())
}

The fragment can legitimately reach ON_START while React holds no current activity, and then this crashes the app:

java.lang.IllegalStateException: [RNScreens] Attempt to access activity on detached context
  com.swmansion.rnscreens.bottomsheet.SheetDelegate.requireDecorView(SheetDelegate.kt:54)
  com.swmansion.rnscreens.bottomsheet.SheetDelegate.onStateChanged(SheetDelegate.kt:90)

The ordering that produces it:

  1. a form sheet is open, so its SheetDelegate is observing the host fragment's lifecycle
  2. the activity hosting it is stopped
  3. a second ReactActivity is destroyed while it is the current activity, so ReactContext.currentActivity is cleared
  4. the user returns to the app, and the host fragment is restarted

ReactActivity.onResume is what calls onHostResume and restores currentActivity, but fragment ON_START is dispatched before onResume. So at ON_START there is reliably no current activity, and checkNotNull turns an ordinary return-to-app into a fatal exception.

Apps with a second ReactActivity are the ones that hit this, but nothing in the sheet actually needs the decor view that early, and preserveBackgroundFocus a few lines below already treats a missing current activity as an ordinary condition:

private fun preserveBackgroundFocus() {
    val activity = screen.reactContext.currentActivity ?: return

So the delegate is already inconsistent with itself about whether a missing activity is fatal.

Changes

  • Replaced requireDecorView() with a nullable decorViewOrNull.
  • Extracted registerInsetsObserver(): it registers when a decor view is available, otherwise logs a warning and sets a pending flag.
  • ON_RESUME retries the registration when it is pending. That is the right place because onHostResume has run by then.

InsetsObserverProxy.registerOnView just calls ViewCompat.setOnApplyWindowInsetsListener and overwrites its weak reference, so re-entering it is safe. The normal path is unchanged: when an activity is present at ON_START, registration happens exactly as before and the ON_RESUME branch never runs.

Before & after - visual documentation

No visual changes. The insets observer ends up registered on the same decor view either way; it just no longer throws when it has to wait one lifecycle step for it.

Test plan

There are no Kotlin unit tests in the repo, so this was verified by hand.

To reproduce you need a second ReactActivity in the app, since that is what clears currentActivity independently of the one hosting the sheet:

  1. open a screen presented with presentation: 'formSheet'
  2. with the sheet still open, send the app to the background so the hosting activity is stopped
  3. have the second ReactActivity come up and then be destroyed while it is the current activity
  4. return to the app

On main step 4 crashes with the IllegalStateException above every time. With this change the app returns to the sheet, and if the activity is not yet available you get:

W/SheetDelegate: [RNScreens] No current activity while registering the sheet insets observer, deferring to ON_RESUME

followed by normal registration a moment later, with insets and keyboard handling behaving as usual.

I have been running this patched into 4.26.2 in production for several weeks and the crash is gone. In our case the second activity is an overlay screen driven by an AccessibilityService, which is what makes the ordering reproducible rather than rare.

I have not attached a standalone reproducer repository, because reproducing it needs a second ReactActivity being destroyed at the right moment rather than anything expressible in a Snack. The lifecycle ordering argument stands on its own from the code, and I am happy to build a minimal two-activity sample if that would help review.

Formatting checked with ktlint 1.3.0 (the version android/spotless.gradle pins) against the changed file.

Checklist

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

…erver

SheetDelegate resolves the decor view through checkNotNull(currentActivity)
in the host fragment's ON_START. The fragment can reach ON_START while React
holds no current activity: the sheet is open, the activity hosting it is
stopped, another ReactActivity is destroyed as the current one, and the user
then returns to the app. Fragment ON_START is dispatched before
ReactActivity.onResume calls onHostResume, so currentActivity is still null
at that point and every such return became a fatal IllegalStateException.

Resolve the decor view leniently and retry the registration in ON_RESUME,
which runs after onHostResume has restored the activity. registerOnView is
idempotent, and preserveBackgroundFocus in the same class already treats a
missing current activity as an ordinary condition.
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.

1 participant