fix(Android, Stack v5): preventNativeDismiss survives activity restart - #4594
Draft
kkafar wants to merge 3 commits into
Draft
fix(Android, Stack v5): preventNativeDismiss survives activity restart#4594kkafar wants to merge 3 commits into
kkafar wants to merge 3 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…activity restart preventNativeDismiss silently stops intercepting the system back press after the activity goes through a stop/start cycle (backgrounding, lock screen). Root cause: PreventNativeDismissCallback is registered once with the non-lifecycle addCallback overload and keeps its dispatcher deque position forever, while every FragmentManager re-inserts its internal callback at the deque tail on each ON_START - after one cycle the FragmentManager callbacks leapfrog every prevent callback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WoagDFFRUJArVSTaMW83X
…vity restarts Each StackContainer now owns a single veto-only OnBackPressedCallback, registered with the lifecycle owner of its FragmentManager (the hosting fragment when nested, the FragmentActivity host when at the root). Both callbacks are lifecycle-owned, so on every activity stop/start they are re-inserted into the dispatcher in a fixed order: FragmentManager's pop callback first, the veto right after it. Previously each StackScreenFragment registered its own callback with the non-lifecycle overload, which kept its deque slot forever while every FragmentManager callback moved to the tail on ON_START - one background cycle was enough for the FragmentManager to leapfrog the prevent callback and pop the screen. System back now resolves through the same recursive wantsToPreventStackNativeDismiss() query as the header chevron. The per-fragment callback, its change observer and the top-fragment bookkeeping (updateTopFragment, OnCommitCallbackOp) are removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AqrHqNcfhAe6Epx5XGhMWB
The task of a StackContainer is not to emit an react event, it is just to "handle" the prevention. New name does not leak implementation details.
kkafar
force-pushed
the
@kkafar/prevent-native-dismiss-system-back
branch
from
September 4, 2026 09:27
f7996d7 to
74d6a69
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Stacked on #4578 (header chevron scoped to its own stack).
preventNativeDismisson Android (Stack v5) silently stops intercepting the system back press after the activity goes through one stop/start cycle (app switch, lock screen, home button). From then on the screen pops as if the flag were off, until its fragment is recreated.Root cause (verified against
androidx.activity1.8.1 /androidx.fragment1.8.9):OnBackPressedDispatcherhands a back press to the last-added enabled callback. EveryFragmentManagerregisters its pop callback with the lifecycle-owner overload, which removes it on the owner'sON_STOPand re-appends it at the deque tail onON_START. Our per-fragmentPreventNativeDismissCallbackwas registered with the plain overload, so its slot never moved. One background cycle was enough for everyFragmentManagercallback to leapfrog every prevent callback.Fix: each
StackContainerowns a single veto-onlyOnBackPressedCallback, registered with the same lifecycle owner itsFragmentManageruses (the hosting fragment when nested,FragmentActivity's fragment host at the root).LifecycleRegistryre-dispatchesON_STARTin registration order, so after every restart the deque is rebuilt as[fm, veto]per level. Deeper fragments start after their parents, so a deeper stack with something to pop still beats a shallower veto.FragmentManagerkeeps executing pops, so predictive back is unaffected. System back now resolves through the same recursivewantsToPreventStackNativeDismiss()query as the header chevron from #4578.Note: the root owner is obtained through
FragmentManager.getHost(), which is@RestrictTo(LIBRARY)(lint-suppressed). There is no public accessor. The activity's own lifecycle cannot be used: it starts after every fragment and would put the root veto behind all nestedFragmentManagercallbacks.Closes https://github.com/software-mansion/react-native-screens-labs/issues/1775
Changes
StackContainer: newSystem back vetoregion. Lifecycle-owned veto callback (registered on attach, removed on detach), enabled predicate recomputed eagerly, re-dispatch to the next callback when the state went stale,emitNativeDismissPreventedshared with the chevron path, ancestor invalidation walk.FragmentManagerHelper:findFragmentManagerWithOwnerForViewreturns the manager together with its lifecycle owner and dispatcher.findFragmentManagerForViewis now non-null (it never returned null).StackScreen:preventNativeDismisschanges invalidate the owning containers instead of notifying a single observer.PreventNativeDismissCallback,PreventNativeDismissChangeObserver, top-fragment bookkeeping inStackScreenFragment,updateTopFragment+OnCommitCallbackOp.TabsContainer: null-safeselectedTabOrNullfor the veto query (reachable after the selected tab was removed).Test plan
Repro / regression:
apps/src/tests/issue-tests/TestNestedStackPreventSystemBack.tsx(launch directly viaapps/App.tsx): pushNestedStack→ system back intercepted (toast) → background + foreground → system back still intercepted. PushNestedA→ system back pops onlyNestedA, before and after backgrounding.Regression scenarios:
apps/src/tests/single-feature-tests/stack-v5/test-stack-prevent-native-dismiss-single-stack/scenario.mdand.../test-stack-prevent-native-dismiss-nested-stack/scenario.md(all steps, plus a background cycle after an interception), root-exit block with a single preventing screen, predictive back gesture where nothing vetoes, tabs smoke (test-stack-tabs-stack-in-tabs-base-navigation).Detox:
FabricExample/e2e/single-feature-tests/stack-v5/test-stack-prevent-native-dismiss-{single,nested}-stack.e2e.ts.Status: static checks green (
yarn lint-android,assembleDebug). On-device verification and the scenario/doc-comment updates are pending, hence draft.Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01AqrHqNcfhAe6Epx5XGhMWB