fix(android): key onNativeLayout guard on layoutHolder so transient startup heights self-heal#556
Open
foxojs wants to merge 2 commits into
Open
fix(android): key onNativeLayout guard on layoutHolder so transient startup heights self-heal#556foxojs wants to merge 2 commits into
foxojs wants to merge 2 commits into
Conversation
…tartup heights self-heal The layout-change listener guards re-reporting on the OUTER view's size (right - left / bottom - top) but reports the INNER layoutHolder's size to JS. During app startup these can disagree: the outer frame settles at its final size while layoutHolder is still mid-layout at a transient (roughly half-window) height. When the listener fires in that window, the transient layoutHolder height is reported to JS and the outer size is stored in lastReportedSize. Once layoutHolder finishes laying out, the outer size is unchanged, so the guard never allows a re-report: every tab screen stays styled with the transient height for the whole session (content clipped, dead space below; stack screens and absolutely-positioned siblings are unaffected). Observed persistently in production on a Pixel 6a / Android 16. Key the guard on layoutHolder's own dimensions - the same view whose size is reported - so any later correct layout is detected as a change and re-reported. Worst case the wrong height lives for a single frame before the next layout pass corrects it (measured 5 ms in a deterministic reproduction). Regression introduced by callstack#283, which changed the reported size from the outer view to layoutHolder without changing the guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
PR Description
Fixes #555 — a session-long Android layout latch.
The layout-change listener in
RCTTabView.ktguards re-reporting on the outer view's size (right - left/bottom - top) but reports the innerlayoutHolder's size to JS. During app startup these can disagree: the outer frame settles at its final size whilelayoutHolderis still mid-layout at a transient (roughly half-window) height. If the listener fires in that window:layoutHolderheight is reported to JS, andlastReportedSize.When
layoutHolderfinishes laying out, the outer size is unchanged, so the guard never permits a re-report. Every tab screen stays styled with the transient height for the entire session — content clipped to roughly half the screen with dead space below, while the tab bar, absolutely-positioned siblings, and pushed stack screens render normally. A force-close sometimes clears it (the race is per cold start). We observed this persistently in production on a Pixel 6a / Android 16.This is a regression from #283, which changed the reported size from the outer view to
layoutHolderwithout changing the guard.The fix: key the guard on
layoutHolder's own dimensions — the same view whose size is reported. Any later correct layout is then detected as a change and re-reported, so the view self-heals; worst case the wrong height lives for a single frame (measured 5 ms from restore to re-report in our reproduction).How to test?
The race is timing-dependent and hard to hit on demand, so we made it deterministic: temporarily force
layoutHolderto a transient non-final height at the moment the listener first fires, then restore it — simulating exactly what the race produces. Inside thepost { ... }block ininit, afteraddOnLayoutChangeListener:With a log line on each
onNativeLayoutListenerinvocation, currentmainproduces (Pixel 6a emulator):With this PR, identical perturbation:
Remove the perturbation after testing. Normal behaviour is unchanged: the guard fires on the same real size changes as before (rotation, window resize, keyboard), just keyed on the reported view.
Screenshots
Same app, same injected transient — only the guard differs.
main) — latched🤖 Generated with Claude Code