fix(Android, Stack v4): Add heuristic for applying inset only on top-level header#3793
fix(Android, Stack v4): Add heuristic for applying inset only on top-level header#3793
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses excessive whitespace on Android when stack navigators are nested by ensuring the top inset is consumed only by the topmost visible header. It coordinates inset consumption across nested stacks via a React context, adds pre-layout inset handling using DecorView to reduce layout jumps, and introduces an experimental flag to opt out.
Changes:
- Add
TopInsetConsumptionContextand wire it throughScreenStackItem/ScreenStackHeaderConfigto decide which header consumes the top inset. - Introduce
androidLegacyTopInsetBehaviorexperimental feature flag to opt out and retain legacy behavior. - Update Android native header config/toolbar to support
consumeTopInset+ pre-layout DecorView inset application; remove older AppBarLayout workaround; add a new regression test (Test3793).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/flags.ts |
Adds experimental flag androidLegacyTopInsetBehavior. |
src/fabric/ScreenStackHeaderConfigNativeComponent.ts |
Extends native props with consumeTopInset / legacyTopInsetBehavior. |
src/components/contexts/TopInsetConsumptionContext.ts |
Introduces context to track top inset consumption across nested stacks. |
src/components/ScreenStackItem.tsx |
Provides inset-consumption context to nested children based on header visibility/consumption. |
src/components/ScreenStackHeaderConfig.tsx |
Computes and forwards inset-consumption props to native header config. |
android/.../ScreenStackHeaderConfigViewManager.kt |
Wires new props to ScreenStackHeaderConfig on Android. |
android/.../ScreenStackHeaderConfig.kt |
Stores new props and triggers inset re-application when they change. |
android/.../CustomToolbar.kt |
Adds DecorView pre-layout inset application and consumption-aware inset handling. |
android/.../ScreenStackFragment.kt |
Removes usage of CustomAppBarLayout and falls back to AppBarLayout. |
android/.../Screen.kt |
Removes prior manual inset-based shadow state correction. |
android/.../CustomAppBarLayout.kt |
Deletes the old AppBarLayout correction workaround. |
apps/src/tests/issue-tests/Test3793.tsx |
Adds regression test for nested stacks and toggling header visibility. |
apps/src/tests/issue-tests/index.ts |
Exports the new test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
This is really needed, currently the inset on the header intermittently disappears, and is gone in landscape but back in portrait, there's no consistency so there's no way to manually set anything when it's not there. This seems like it should make all the cases less fragile. |
5ac64dd to
f0eaaeb
Compare
|
|
||
| const nextContextValue = React.useMemo( | ||
| () => ({ | ||
| isTopInsetConsumed: insetContext.isTopInsetConsumed || consumesTopInset, |
There was a problem hiding this comment.
Please see BottomTabsAndStack example. The inset is repeated due to JS header in tabs.
This seems like a good example where we should allow the user to disable the top inset maunally, right? This functionality is missing in this PR but I think that we discussed it. Is this going to be handled in separate PR?
But if we're going to add it, we need to think about propagating that information.
In regular case, if the outer header is hidden, then the inner header should handle the inset as is right now in this PR.
Another situation: imagine if we had 2 native stacks in BottomTabsAndStack. If the user manually disables outer native header inset, then the inner header shouldn't apply the padding as well, right? The user override indicates that we applied inset where we shouldn't and any header below it shouldn't apply the inset as well? On the other hand, setting the same prop to all stacks isn't that hard and it would simplify our approach.
Just something to think about.
There was a problem hiding this comment.
let's make that in the follow-up PR, ticket for that: https://github.com/software-mansion/react-native-screens-labs/issues/1096
I think that just exposing a prop is fine in that case, because the prop will be living per screen instance, so the downstream might propagate that information as well
There was a problem hiding this comment.
I think that initially - especially in stack v4, we should settle with allowing for manual control.
Describe somewhere how does the heuristic work and when it needs assistance (cases like described above).
Let's keep as simple as possible for stack v4.
kligarski
left a comment
There was a problem hiding this comment.
Is this a regression in this PR or does it happen on main as well (look at the navigation bar inset for bottom navigation view)?
Screen_recording_20260407_083008.mp4
Known issue with delayed insets when native tabs are nested in stack: #3747 (comment) |
|
Any reason this hasn't been merged yet? |
|
Oh, another use case that needs accounting for, likely an option for manual control of disabling the header top inset entirely: If the user wants a top banner: Right now when adding a or at the very top of the app, since the header always applies the margin no matter what, that extra margin is added under the top banner. Has this been tested in landscape? Currently the header doesn't add any inset in landscape mode, and only does in portrait. Also, wouldn't have never had adding it to the header at all have been the easiest, then it's left to the dev to handle it and it could be managed, having it auto added is what made it unmanagable. |
will be realized in a follow-up PR: #3835
landscape: |
|
Hi, when is this going to be released? |
|
We still have a few important changes to merge for the native tabs, native stack memory leak fixes and a follow-up PR for the header changes before we release 4.25. If this update is important for you right now, I recommend using the nightly release. |
Thank you, could you tell me which nightly release has this specific change? I see that the last one was 8 days ago. |
|
Sorry, we didn't notice that the nightly releases started failing last week - we're fixing it now #3878 |


Description
This PR fixes multiple applications of top insets when stack navigators are nested, leading to excessive whitespace. We introduced a
TopInsetConsumptionContextto coordinate top inset consumption across nested navigators. The topmost visible, non-hidden header consumes the inset and notifies nested screens that the inset has already been handled.Additionally, we implemented a pre-layout inset handling mechanism using
DecorViewto prevent layout jumps before the window insets are dispatched - this is an updated approach relative to: #3442. Additionally, all workarounds that were related to manual frame corrections from that PR were removed, because now we can apply proper layout to the Toolbar earlier, and we don't need to make any manual corrections before the insets come.We're adding an option to opt out of this behavior with an experimental feature flag
androidLegacyTopInsetBehavior.Changes
TopInsetConsumptionContextto track whether the inset was already consumed higher up in the hierarchyScreenStackItemnow calculates whether its nested children should consider the top inset consumedScreenStackHeaderConfigreads the context and determines if it should consume the inset.androidLegacyTopInsetBehaviorwas added to allow opt-out from this approachBefore & after - visual documentation
Test3793
test3793-before.mov
test3793-after.mov
Test3006
Note: issues with jumping content in Tabs->Stack(with header)->Stack(with header) combo are still present, don't see an option to resolve that now, because the
DummyLayoutHelperwould need to be aware which header is top-level, for now I'm treating that specific setup as uncommon.test3006-after.mov
Test plan
Added
Test3793, performed regression testing onTest3006.Checklist