Skip to content

Fatal Exception: java.lang.ClassCastException: android.widget.ImageView cannot be cast to android.view.ViewGroup #4273

Description

@hdjvieira

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    missing-infoThe user didn't precise the problem enoughmissing-reproThis issue need minimum repro scenarioplatform:androidIssue related to Android part of the libraryplatform:webThis issue is specific to web

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions