Hi! 👋
Firstly, thanks for all your work on this project! 🙂
Today I used patch-package to patch react-native-screens@4.24.0 for the project I'm working on.
We are getting these error reports on Crashlytics:
Fatal Exception: java.lang.ClassCastException: android.widget.ImageView cannot be cast to android.view.ViewGroup
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:557)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:286)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2214)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2115)
at androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:2002)
at androidx.fragment.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:323)
at com.swmansion.rnscreens.ScreenStack.onUpdate(ScreenStack.kt:303)
at com.swmansion.rnscreens.ScreenContainer.performUpdates(ScreenContainer.kt:376)
at com.swmansion.rnscreens.ScreenContainer.onScreenChanged$lambda$8(ScreenContainer.kt:359)
at com.swmansion.rnscreens.ScreenContainer.$r8$lambda$_XN8QBWatVwLAw6wNpLgWT68aTo(ScreenContainer.kt)
at com.swmansion.rnscreens.ScreenContainer$$ExternalSyntheticLambda0.run(D8$$SyntheticClass)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.kt:21)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8762)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:604)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)
So I asked Claude to take a look 😄 this is what he found out
Root cause
This is a view-ID collision between React Native's Fabric tags and native views, detonated by how react-native-screens attaches fragments.
ScreenStack/ScreenContainer attach screen fragments with transaction.add(id, fragment) (ScreenContainer.kt:221), where id is the container's React tag.
When the transaction executes, androidx FragmentStateManager.createView resolves that ID with a hierarchy-wide findViewById and blindly casts the result: container = (ViewGroup) fragmentContainer.onFindViewById(mContainerId).
Fabric assigns React tags as small even integers (2, 4, 6, … — confirmed in ReactFabric-prod.js, nextReactTag = 2; += 2) and sets them as the Android view ID. Meanwhile native views use equally small programmatic IDs — the Google Maps SDK's compass/my-location buttons are ImageViews with hardcoded literal IDs (1, 2, 5…), and View.generateViewId() (used by e.g. react-native-pager-view) also counts from 1. In a map app, a collision is a matter of time: when a duplicate ID sits earlier in traversal order, findViewById returns the map's ImageView instead of the ScreenStack, and the cast crashes. That also explains why it's intermittent — it depends on which tag number the navigator happens to get.
Fix
Since fragment 1.5, FragmentTransaction.add(ViewGroup container, Fragment, tag) sets fragment.mContainer by reference, and createView uses that field first — the findViewById path is never taken. We ship fragment 1.8.9, so I patched all three attach sites in react-native-screens 4.24.0 to use it (upstream main still has the bug, so upgrading wouldn't help):
patches/react-native-screens+4.24.0.patch — ScreenContainer.attachScreen plus the two transaction.add(id, …) calls in ScreenStack.onUpdate now pass this instead of id.
Verified with :react-native-screens:compileDebugKotlin — builds clean. Behavior is otherwise identical: the overload delegates to the same add(container.getId(), …) bookkeeping, it just pins the container object up front.
Here is the diff that solved my problem:
diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt
index 7055a0d..88d637a 100644
--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt
+++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt
@@ -218,7 +218,11 @@ open class ScreenContainer(
transaction: FragmentTransaction,
fragment: Fragment,
) {
- transaction.add(id, fragment)
+ // Pass the container by reference instead of by id: resolving the container via
+ // findViewById can hit an unrelated view when a native view's id (e.g. the Google Maps
+ // compass/my-location ImageViews use small literal ids) collides with our React tag,
+ // crashing with "ImageView cannot be cast to ViewGroup" in FragmentStateManager.
+ transaction.add(this, fragment, null)
}
fun attachBelowTop() {
diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
index bffd9c7..95f5937 100644
--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
+++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
@@ -271,7 +271,7 @@ class ScreenStack(
.dropWhile { it !== visibleBottom } // ignore all screens beneath the visible bottom
.forEach { wrapper ->
// TODO: It should be enough to dispatch this on commit action once.
- transaction.add(id, wrapper.fragment).runOnCommit {
+ transaction.add(this@ScreenStack, wrapper.fragment, null).runOnCommit {
top?.screen?.bringToFront()
}
}
@@ -279,7 +279,7 @@ class ScreenStack(
if (newTop.screen.requiresEnterTransitionPostponing()) {
newTop.fragment.postponeEnterTransition()
}
- transaction.add(id, newTop.fragment)
+ transaction.add(this@ScreenStack, newTop.fragment, null)
}
topScreenWrapper = newTop as? ScreenStackFragmentWrapper
react-native-screens+4.24.0.patch
This issue body was partially generated by patch-package.
Hi! 👋
Firstly, thanks for all your work on this project! 🙂
Today I used patch-package to patch
react-native-screens@4.24.0for the project I'm working on.We are getting these error reports on Crashlytics:
So I asked Claude to take a look 😄 this is what he found out
Root cause
This is a view-ID collision between React Native's Fabric tags and native views, detonated by how react-native-screens attaches fragments.
ScreenStack/ScreenContainer attach screen fragments with transaction.add(id, fragment) (ScreenContainer.kt:221), where id is the container's React tag.
When the transaction executes, androidx FragmentStateManager.createView resolves that ID with a hierarchy-wide findViewById and blindly casts the result: container = (ViewGroup) fragmentContainer.onFindViewById(mContainerId).
Fabric assigns React tags as small even integers (2, 4, 6, … — confirmed in ReactFabric-prod.js, nextReactTag = 2; += 2) and sets them as the Android view ID. Meanwhile native views use equally small programmatic IDs — the Google Maps SDK's compass/my-location buttons are ImageViews with hardcoded literal IDs (1, 2, 5…), and View.generateViewId() (used by e.g. react-native-pager-view) also counts from 1. In a map app, a collision is a matter of time: when a duplicate ID sits earlier in traversal order, findViewById returns the map's ImageView instead of the ScreenStack, and the cast crashes. That also explains why it's intermittent — it depends on which tag number the navigator happens to get.
Fix
Since fragment 1.5, FragmentTransaction.add(ViewGroup container, Fragment, tag) sets fragment.mContainer by reference, and createView uses that field first — the findViewById path is never taken. We ship fragment 1.8.9, so I patched all three attach sites in react-native-screens 4.24.0 to use it (upstream main still has the bug, so upgrading wouldn't help):
patches/react-native-screens+4.24.0.patch — ScreenContainer.attachScreen plus the two transaction.add(id, …) calls in ScreenStack.onUpdate now pass this instead of id.
Verified with :react-native-screens:compileDebugKotlin — builds clean. Behavior is otherwise identical: the overload delegates to the same add(container.getId(), …) bookkeeping, it just pins the container object up front.
Here is the diff that solved my problem:
react-native-screens+4.24.0.patch
This issue body was partially generated by patch-package.