Skip to content

fix(Android): stop reading uninitialized state from requestLayout() so R8 optimization cannot crash it - #4574

Open
TaduJR wants to merge 2 commits into
software-mansion:mainfrom
TaduJR:fix/android-r8-nullable-layout-callbacks
Open

fix(Android): stop reading uninitialized state from requestLayout() so R8 optimization cannot crash it#4574
TaduJR wants to merge 2 commits into
software-mansion:mainfrom
TaduJR:fix/android-r8-nullable-layout-callbacks

Conversation

@TaduJR

@TaduJR TaduJR commented Aug 31, 2026

Copy link
Copy Markdown

Fixes #4586

Description

With R8 optimization enabled, the app crashes as soon as the first screen is mounted:

FATAL EXCEPTION: main
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Class java.lang.Object.getClass()' on a null object reference
	at com.facebook.react.modules.core.ReactChoreographer.postFrameCallback(SourceFile:4)
	at com.swmansion.rnscreens.ScreenContainer.requestLayout(SourceFile:21)
	at android.view.View.setFlags(View.java:18980)
	at android.view.ViewGroup.initViewGroup(ViewGroup.java:731)
	at android.view.ViewGroup.<init>(ViewGroup.java:724)
	at com.swmansion.rnscreens.ScreenContainer.<init>(SourceFile:1)
	at com.swmansion.rnscreens.ScreenStack.<init>(SourceFile:1)
	at com.swmansion.rnscreens.ScreenStackViewManager.createViewInstance(SourceFile:6)

ViewGroup's constructor calls requestLayout() (through initViewGroup and setFlags) before the subclass is initialized. ScreenContainer overrides requestLayout() and reads layoutCallback, which is still null at that point. The code guarded this with layoutCallback != null plus @Suppress("SENSELESS_COMPARISON"). R8's optimizer sees a final field written once with a non-null object, decides the check can never fail, and deletes it. Reproduced with R8 9.4.14. R8 8.12.14, which AGP 8.11 bundles, still keeps the check.

So the library only works with R8 optimization off. AGP 9 rejects proguard-android.txt by default, the file that carried -dontoptimize, and Google's guidance is proguard-android-optimize.txt, so apps following it hit this. CustomToolbar and TabsHost have the same pattern. TabsHost is not legacy code.

An earlier revision of this PR typed the fields as nullable. That only changes Kotlin metadata, not bytecode, and the post-R8 dex came out identical to the crashing build. This revision removes the fields instead.

No existing issue covers this.

Changes

requestLayout() no longer reads anything initialized after super(). It is gated by Booleans, whose default value is correct while the superclass constructor runs, and posts a fresh callback each time. StackHost already works this way.

  • legacy/ScreenContainer.kt: removed the layoutCallback field, its body is now forceSubtreeMeasureAndLayoutPass().
  • legacy/CustomToolbar.kt: same, as forceMeasureAndLayoutPass().
  • tabs/host/TabsHost.kt: took over the two pending flags from TabsHostLayoutCoordinator, which held no other state, and that class is deleted. Its two scheduling methods and their docs moved into TabsHost.

One behaviour difference: a requestLayout() during construction used to be skipped and now posts one deferred measure/layout pass, which runs on the fully constructed view. StackHost already does this. Kotlin emits no write for = false initializers, so a flag set during construction is not reset afterwards and nothing posts twice.

StackHost and StackHeaderSubview also override requestLayout() and are unaffected: one guards on a Boolean, the other uses a safe call.

Test plan

Reproduced on react-native-screens 4.25.0: release build, minifyEnabled true, proguard-android-optimize.txt, R8 9.4.14 via classpath("com.android.tools:r8:9.4.14") in settings.gradle, rendering a ScreenStack. The code is unchanged in 4.27.0 and on main.

Verified by reading the post-R8 dex of ScreenContainer.requestLayout with dexdump -d, R8 9.4.14, no keep rules for this library:

  • unchanged code: null check gone from the dex, app crashes on launch.
  • nullable fields (previous revision): dex identical to the above.
  • this revision: a Boolean test followed by postFrameCallback with a new callback, no field read left. The app launches and renders with no fatal exception.

Compiles clean against React Native 0.87.0-rc.3 (:react-native-screens:compileDebugKotlin in FabricExample), zero errors, no new warnings on the changed files. No automated test is added because the failure only exists after R8 optimization runs.

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

@TaduJR
TaduJR marked this pull request as draft August 31, 2026 15:47
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: b35b2cd3-1c3b-402c-bb86-b4305339735e

📥 Commits

Reviewing files that changed from the base of the PR and between 0873001 and 55e1bf3.

📒 Files selected for processing (3)
  • android/src/main/java/com/swmansion/rnscreens/legacy/CustomToolbar.kt
  • android/src/main/java/com/swmansion/rnscreens/legacy/ScreenContainer.kt
  • android/src/main/java/com/swmansion/rnscreens/tabs/host/TabsHost.kt

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

Android layout scheduling no longer uses TabsHostLayoutCoordinator or stored frame-callback objects. Layout passes now run through direct message-queue or ReactChoreographer callbacks and private helper methods.

Changes

Android layout scheduling

Layer / File(s) Summary
Legacy layout pass extraction
android/src/main/java/com/swmansion/rnscreens/legacy/CustomToolbar.kt, android/src/main/java/com/swmansion/rnscreens/legacy/ScreenContainer.kt
requestLayout() posts inline ReactChoreographer callbacks. Private helpers perform the measure and layout passes previously held by Choreographer.FrameCallback objects.
TabsHost scheduling integration
android/src/main/java/com/swmansion/rnscreens/tabs/host/TabsHost.kt, android/src/main/java/com/swmansion/rnscreens/tabs/host/TabsHostLayoutCoordinator.kt
TabsHost selects message-queue scheduling before inset initialization and ReactChoreographer scheduling afterward. Separate flags prevent duplicate callbacks. TabsHostLayoutCoordinator is removed, and constructor-time requestLayout() behavior is documented.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 55e1b

The PR removes constructor-time reads that can crash optimized Android builds and preserves deferred layout scheduling; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: kmichalikk

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description clearly explains the Android R8 crash, the affected classes, the implementation changes, and the verification performed.
Title check ✅ Passed The title clearly summarizes the main change: preventing R8 from removing a required null check and causing an Android crash during requestLayout().
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TaduJR
TaduJR marked this pull request as ready for review August 31, 2026 15:57
@TaduJR
TaduJR marked this pull request as draft September 2, 2026 12:59
@TaduJR
TaduJR marked this pull request as ready for review September 2, 2026 13:47
@TaduJR TaduJR changed the title fix(Android): type layout callbacks as nullable so R8 keeps the construction-time guard fix(Android): stop reading uninitialized state from requestLayout() so R8 optimization cannot crash it Sep 2, 2026
@TaduJR
TaduJR force-pushed the fix/android-r8-nullable-layout-callbacks branch from 2a66437 to 0873001 Compare September 2, 2026 14:16
@TaduJR
TaduJR force-pushed the fix/android-r8-nullable-layout-callbacks branch from 0873001 to 55e1bf3 Compare September 2, 2026 14:20
Comment on lines -92 to +89
@Suppress("SENSELESS_COMPARISON") // mLayoutCallback can be null here since this method can be called in init
if (!isLayoutEnqueued && layoutCallback != null) {
if (!isLayoutEnqueued) {
isLayoutEnqueued = true
// we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
// looper loop instead of enqueueing the update in the next loop causing a one frame delay.
ReactChoreographer
.getInstance()
.postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
layoutCallback,
)
.postFrameCallback(ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE) {
isLayoutEnqueued = false
forceMeasureAndLayoutPass()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This won't fly that easily unfortunately.

It changes the timing when the callback is posted, e.g. it fires an additional callback, when previously layoutCallback == null.

This is a very fragile code that would require careful testing & there is lack of capacity on our side at the moment.

@kkafar
kkafar requested a review from LKuchno September 3, 2026 07:41
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.

Android: crash on first screen mount with R8 optimization enabled (null check in ScreenContainer.requestLayout removed)

2 participants