Skip to content

fix(android): show pages added to an empty pager - #1197

Open
Luccacvb wants to merge 1 commit into
callstack:mainfrom
Luccacvb:fix/android-empty-pager-blank
Open

Luccacvb wants to merge 1 commit into
callstack:mainfrom
Luccacvb:fix/android-empty-pager-blank

Conversation

@Luccacvb

Copy link
Copy Markdown
Contributor

Summary

Fixes the Android 9.x regression of #219.

On Android 9.x, a pager that mounts with no pages stays blank when its pages arrive later, for example after a fetch. onPageSelected fires, so the app can show "Photo 1 of 4", but nothing is drawn until the user touches the pager, and setPage does nothing until then. The same happens when all pages are removed and added back. 8.x laid the ViewPager2 out again when pages were added or removed (refreshViewChildrenLayout, from #408 for #219 and from #929), and the Compose rewrite (#1092) dropped it.

reproduction

pager-view-219-before-after.mp4

What happened

  • When the first page arrives, Compose adds the pager under its root node, so AndroidComposeView asks for a layout pass with requestLayout() instead of just redrawing.
  • The request goes up to the pager's React parent, where ReactViewGroup.requestLayout() is a no-op, and Fabric only calls layout() on the pager when its frame changes. So the pass never runs.
  • While that request is pending, AndroidComposeView doesn't schedule any other layout or redraw (scheduleMeasureAndLayout returns early when isLayoutRequested). A touch runs Compose's own measure and layout, which is why the page shows up after one.
  • In a screen pushed with the native stack, the pager already has an unanswered request from when it was attached, so it goes blank on the first load. A pager mounted into a visible screen can show its first pages, but stays blank once its pages are removed and added back.

Fix

  • Override requestLayout() in ComposePagerView and post a measure and layout at the pager's current size, like 8.x's refreshViewChildrenLayout. react-native-screens does the same in ScreenContainer, StackHost and TabsHost.
  • Do the same in forceLayout(). A configuration change handled in place, like dark mode with the template's configChanges, calls it on every view instead of requestLayout(). Without it, empty pagers went blank again after the first dark mode switch. TabsHost overrides it for the same reason.
  • It runs from requestLayout() and not from addPage(), because Compose only asks for layout after it recomposes, on the next frame. A measure posted from addPage() ran too early, and the pager stayed blank.
  • The pass is skipped while the pager has no size, so the zero-width case in [Android] v9: pager starts on the last page (initialPage ignored) when first laid out with zero width, e.g. inside display: 'none' #1191 doesn't change. It's also skipped after dispose(), which requests a layout too: measuring then would create a new composition on the dropped view, the same layout-after-unmount case fix(android): fix crash on pager unmount #1001 handled in 8.x.
  • Extra posts are no-ops once the pager is laid out (same size, no pending request), so they aren't deduplicated.

Behavior notes

Test Plan

What's required for testing (prerequisites)?

Android emulator or device, with TalkBack and other accessibility services off.

What are the steps to reproduce (after prerequisites)?

Added Issue #219 Empty Pager Repro to the example app and e2e/tests/issues/issue-219-empty-pager.e2e.ts. The pager mounts with no pages, and the status shows the page count, the selected page and how many onPageSelected events arrived:

Action Before After
Add pages Pages: 4 | Selected: 0 | Events: 1, blank Page A shown
Remove pages, add them back (twice) blank Page A shown
Add pages, then Select C blank, still Selected: 0 Page C shown, Selected: 2

Verified

  • On main all 3 new tests fail: the status shows Pages: 4 | Selected: 0 | Events: 1, but Page A never appears, and Select C leaves it at Selected: 0. 9.0.9 fails the same way, so it isn't from fix(android): keep the visible page when pages change before it #1188. With the fix they pass on every run (3 runs, plus the full suite).
  • Full Android e2e suite with the fix: 29/29 tests pass. Android 16 emulator (arm64), release build.
  • Checked by hand, on main and with the fix: dark mode switched before pages arrive (blank on main, and with only the requestLayout() override), orientation="vertical", pages arriving while a native-stack screen covers the pager, a nested pager that starts empty, and removing the last page while on it.
  • format:check, lint (no new warnings), typecheck, prepare, test, example typescript and e2e typecheck pass.

Compatibility

OS Implemented
iOS n/a
Android ✅

Checklist

  • I have tested this on a device and a simulator
  • I added the documentation in README.md: not applicable
  • I updated the typed files (TS and Flow): not applicable

@troZee
troZee requested a balanced review from Copilot October 25, 2026 17:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@troZee

troZee commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

@Luccacvb could you fix the CI? 🙏

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.

3 participants