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:
- 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
- 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
- Set up a bare RN app with
react-native-screens 4.25.0 and native-stack navigation with at least 2 screens pushed.
- Enable release-build optimization in
android/app/build.gradle / gradle.properties:
android.r8.optimizedResourceShrinking=true
with isMinifyEnabled = true and isShrinkResources = true.
- Configure
MainActivity per the official RNScreensFragmentFactory instructions.
- Build a release APK, install on device.
- 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.
- 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
Description
RNScreensFragmentFactoryfails to preventScreen fragments should never be restoredcrashes when the app is built with AGP'sandroid.r8.optimizedResourceShrinking=true. This flag causes R8 to flatten/repackage thecom.swmansion.rnscreenspackage hierarchy for classes that aren't explicitly kept, which breaks thestartsWith(BuildConfig.LIBRARY_PACKAGE_NAME)check that the factory relies on to detect its own Fragment classes.Environment
react-native-screens: 4.25.0optimizedResourceShrinkingis enabled; not present with the flag disabled)android.r8.optimizedResourceShrinking=trueingradle.propertiesisMinifyEnabled = true,isShrinkResources = trueCrash
The MainActivity is configured exactly per the official guidance:
Problem
RNScreensFragmentFactory.instantiate()determines whether a Fragment being restored belongs toreact-native-screensby checking:BuildConfig.LIBRARY_PACKAGE_NAMEis a compile-time string constant ("com.swmansion.rnscreens") and is therefore not affected by R8 renaming. However, theclassNameargument passed in at restoration time reflects the actual (possibly obfuscated) runtime class name of the Fragment, as recorded byFragmentManagerwhen the state was originally saved.Under normal R8 obfuscation (
isMinifyEnabled=truealone), class simple names are shortened but the package hierarchy is preserved, e.g.:In this case
startsWith("com.swmansion.rnscreens")still succeeds and the factory works correctly.However, when
android.r8.optimizedResourceShrinking=trueis 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):Since the resulting FQCN no longer starts with
com.swmansion.rnscreens, the factory's check fails, execution falls through tosuper.instantiate(...), and the empty-constructor guard inScreenFragment/ScreenStackFragmentthrows.Verification
I confirmed this by diffing
mapping.txtacross three configurations, all other settings held constant:optimizedResourceShrinkingScreenFragmentmappingcom.swmansion.rnscreens.ScreenFragment -> com.swmansion.rnscreens.Etruecom.swmansion.rnscreens.ScreenFragment -> kg.Dfalsecom.swmansion.rnscreens.ScreenFragment -> com.swmansion.rnscreens.EToggling only
optimizedResourceShrinkingreproduces 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-screensFragment subclasses restores the original FQCN and fixes the crash while keepingoptimizedResourceShrinkingenabled:After adding this rule,
mapping.txtshows the class name preserved:Suggested fix
Since consumers of
react-native-screenshave no way to know in advance that this specific R8 flag will break Fragment restoration, I'd suggest either:Fragment/DialogFragmentsubclasses undercom.swmansion.rnscreens) in the library'sconsumer-rules.pro, so it's applied automatically for all consumers, orRNScreensFragmentFactory's class-identification logic independent of package-name string matching (e.g. by checking against a static allowlist ofKClassreferences, 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 beyondoptimizedResourceShrinking.Steps to reproduce
react-native-screens4.25.0 and native-stack navigation with at least 2 screens pushed.android/app/build.gradle/gradle.properties:isMinifyEnabled = trueandisShrinkResources = true.MainActivityper the officialRNScreensFragmentFactoryinstructions.adb shell am kill <package>), then return to the app.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