Skip to content

【Android】RNScreensFragmentFactory classname check breaks when AGP android.r8.optimizedResourceShrinking flattens package hierarchy, causing "Screen fragments should never be restored" crash #4505

Description

@manakuro

Description

RNScreensFragmentFactory fails to prevent Screen fragments should never be restored crashes when the app is built with AGP's android.r8.optimizedResourceShrinking=true. This flag causes R8 to flatten/repackage the com.swmansion.rnscreens package hierarchy for classes that aren't explicitly kept, which breaks the startsWith(BuildConfig.LIBRARY_PACKAGE_NAME) check that the factory relies on to detect its own Fragment classes.

Environment

  • react-native-screens: 4.25.0
  • AGP: 8.12.0 (also reproduced on 8.11.1 once optimizedResourceShrinking is enabled; not present with the flag disabled)
  • android.r8.optimizedResourceShrinking=true in gradle.properties
  • isMinifyEnabled = true, isShrinkResources = true
  • New Architecture: enabled (Fabric)

Crash

Process: com.example.com, PID: 30828
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.com/com.example.example.MainActivity}
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4575)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4781)
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:224)
    at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:133)
    at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:103)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:80)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:3032)
    at android.os.Handler.dispatchMessageImpl(Handler.java:142)
    at android.os.Handler.dispatchMessage(Handler.java:125)
    at android.os.Looper.loopOnce(Looper.java:296)
    at android.os.Looper.loop(Looper.java:397)
    at android.app.ActivityThread.main(ActivityThread.java:9523)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:575)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:939)
Caused by: Ah.f: Unable to instantiate fragment kg.N: calling Fragment constructor caused an exception
    at androidx.fragment.app.P.instantiate(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:37)
    at og.b.instantiate(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:26)
    at androidx.fragment.app.n0.<init>(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:14)
    at androidx.fragment.app.h0.Y(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:228)
    at androidx.fragment.app.h0.b(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:212)
    at androidx.fragment.app.J.a(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:10)
    at androidx.activity.ComponentActivity.onCreate(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:31)
    at androidx.fragment.app.FragmentActivity.onCreate(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:1)
    at com.facebook.react.ReactActivity.onCreate(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:1)
    at com.example.example.MainActivity.onCreate(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:138)
    at android.app.Activity.performCreate(Activity.java:9340)
    at android.app.Activity.performCreate(Activity.java:9318)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1543)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4559)
    ... 14 more
Caused by: java.lang.reflect.InvocationTargetException
    at java.lang.reflect.Constructor.newInstance0(Native Method)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
    at androidx.fragment.app.P.instantiate(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:14)
    ... 27 more
Caused by: java.lang.IllegalStateException: Screen fragments should never be restored. Follow instructions from https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067 to properly configure your main activity.
    at kg.D.<init>(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:8)
    at kg.N.<init>(r8-map-id-95dd5983f8c8ed4c35abc98da7e709b544cd38ab4c99dec081068c93a2daf3b7:1)
    ... 30 more

The MainActivity is configured exactly per the official guidance:

override fun onCreate(savedInstanceState: Bundle?) {
    supportFragmentManager.fragmentFactory = RNScreensFragmentFactory()
    super.onCreate(savedInstanceState)
}

Problem

RNScreensFragmentFactory.instantiate() determines whether a Fragment being restored belongs to react-native-screens by checking:

if (className.startsWith(BuildConfig.LIBRARY_PACKAGE_NAME)) {
    // safely replace with a self-destructing fragment
} else {
    super.instantiate(...)  // normal restoration -> throws
}

BuildConfig.LIBRARY_PACKAGE_NAME is a compile-time string constant ("com.swmansion.rnscreens") and is therefore not affected by R8 renaming. However, the className argument passed in at restoration time reflects the actual (possibly obfuscated) runtime class name of the Fragment, as recorded by FragmentManager when the state was originally saved.

Under normal R8 obfuscation (isMinifyEnabled=true alone), class simple names are shortened but the package hierarchy is preserved, e.g.:

com.swmansion.rnscreens.ScreenFragment -> com.swmansion.rnscreens.E

In this case startsWith("com.swmansion.rnscreens") still succeeds and the factory works correctly.

However, when android.r8.optimizedResourceShrinking=true is enabled, R8 switches to a more aggressive, integrated code+resource optimization pipeline. As part of this, classes that are not explicitly kept get repackaged into a flattened, unrelated package (not just simple-name renaming):

com.swmansion.rnscreens.ScreenFragment -> kg.D

Since the resulting FQCN no longer starts with com.swmansion.rnscreens, the factory's check fails, execution falls through to super.instantiate(...), and the empty-constructor guard in ScreenFragment/ScreenStackFragment throws.

Verification

I confirmed this by diffing mapping.txt across three configurations, all other settings held constant:

AGP optimizedResourceShrinking ScreenFragment mapping
8.11.1 (unset) com.swmansion.rnscreens.ScreenFragment -> com.swmansion.rnscreens.E
8.12.0 true com.swmansion.rnscreens.ScreenFragment -> kg.D
8.12.0 false com.swmansion.rnscreens.ScreenFragment -> com.swmansion.rnscreens.E

Toggling only optimizedResourceShrinking reproduces and resolves the issue, isolating it as the trigger rather than the AGP version bump itself.

Workaround

Adding an explicit keep rule for react-native-screens Fragment subclasses restores the original FQCN and fixes the crash while keeping optimizedResourceShrinking enabled:

-keep class com.swmansion.rnscreens.** extends androidx.fragment.app.Fragment {
    <init>();
}

After adding this rule, mapping.txt shows the class name preserved:

com.swmansion.rnscreens.ScreenFragment -> com.swmansion.rnscreens.ScreenFragment
com.swmansion.rnscreens.ScreenStackFragment -> com.swmansion.rnscreens.ScreenStackFragment

Suggested fix

Since consumers of react-native-screens have no way to know in advance that this specific R8 flag will break Fragment restoration, I'd suggest either:

  1. Shipping the above keep rule (or an equivalent one covering all Fragment/DialogFragment subclasses under com.swmansion.rnscreens) in the library's consumer-rules.pro, so it's applied automatically for all consumers, or
  2. Making RNScreensFragmentFactory's class-identification logic independent of package-name string matching (e.g. by checking against a static allowlist of KClass references, or a marker interface/annotation) so it doesn't depend on R8 preserving package hierarchy — since ProGuard/R8 make no guarantee that package structure is preserved even under "normal" obfuscation, this may also be a latent risk for other repackaging-capable configurations beyond optimizedResourceShrinking.

Steps to reproduce

  1. Set up a bare RN app with react-native-screens 4.25.0 and native-stack navigation with at least 2 screens pushed.
  2. Enable release-build optimization in android/app/build.gradle / gradle.properties:
    android.r8.optimizedResourceShrinking=true
    
    with isMinifyEnabled = true and isShrinkResources = true.
  3. Configure MainActivity per the official RNScreensFragmentFactory instructions.
  4. Build a release APK, install on device.
  5. Navigate 2-3 screens deep in the stack, background the app (with "Don't keep activities" enabled in Developer Options, or by killing the backgrounded process via adb shell am kill <package>), then return to the app.
  6. Observe the crash.

Snack or a link to a repository

t0maboro/RNS4505#1

Screens version

4.25.0

React Native version

0.86.2

Platforms

Android

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Build type

Release mode

Device

Real device

Device model

Pixel8a

Acknowledgements

Yes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    platform:androidIssue related to Android part of the libraryrepro-providedA reproduction with a snack or repo is provided

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions