Description
On Android, NativeTabs (the gamma native bottom tabs, via expo-router/unstable-native-tabs) crashes with a ClassCastException when a tab is selected after a third-party SDK assigns a view id to the (id-less) CustomBottomNavigationView.
Real-world trigger: the Pendo Mobile SDK (rn-pendo-sdk). Its codeless analytics walks the view hierarchy and assigns its own small sequential ids to views that have none (View.NO_ID), as part of a screen-scoped dynamic-element scan. react-native-screens never assigns CustomBottomNavigationView an id at construction, so it is View.NO_ID by default — and Pendo was observed stamping id=2 onto it, colliding with menu-item id 2. Confirmed with an instrumented setId override; the caller was sdk.pendo.io.x6.h (stack trace below). This is not Pendo-specific — any SDK or app code that tags id-less views the same way (a common codeless-analytics pattern) will hit the same collision.
A minimal, runnable reproduction (drops in the real Pendo SDK, no fakes) is here: https://github.com/ekrapfl/rn-screens-nativetabs-repro
Stack trace:
java.lang.ClassCastException: com.swmansion.rnscreens.gamma.tabs.container.CustomBottomNavigationView cannot be cast to com.google.android.material.navigation.NavigationBarItemView
at com.swmansion.rnscreens.gamma.tabs.host.TabsHostA11yCoordinator.setA11yPropertiesToTabItem(TabsHostA11yCoordinator.kt:19)
at com.swmansion.rnscreens.gamma.tabs.host.TabsHostA11yCoordinator.setA11yPropertiesToAllTabItems(TabsHostA11yCoordinator.kt:34)
at com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.updateBottomNavigationViewAppearanceIfNeeded(TabsContainer.kt:468)
at com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.performPostSelectedTabUpdateActions(TabsContainer.kt:447)
at com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.performContainerUpdate(TabsContainer.kt:439)
at com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.flushPendingUpdates(TabsContainer.kt:194)
at com.swmansion.rnscreens.gamma.tabs.host.TabsHost.didMountItems(TabsHost.kt:197)
...
Root cause (confirmed with on-device instrumentation). In TabsHostA11yCoordinator.setA11yPropertiesToTabItem:
val menuView = bottomNavigationView.findViewById<NavigationBarItemView>(menuItem.itemId)
View.findViewById(id) returns the receiver itself when receiver.id == id.
- Menu-item ids are small ints (
fragmentIndex + 1, i.e. 1..N, per MenuHelpers.menuItemIdForFragmentAtIndex).
CustomBottomNavigationView is never given an id (see CustomBottomNavigationView.kt — no id = ... anywhere), unlike TabsContainer, which generates one for itself via ViewIdGenerator (TabsContainer.kt:56).
- Once something else assigns the bar an id that happens to fall in
1..N, findViewById(n) returns the bar itself instead of the n-th menu item, and the inline cast to NavigationBarItemView throws.
setA11yPropertiesToAllTabItems() runs on every selection change (invalidateOnSelectedTabChanged() → updateBottomNavigationViewAppearanceIfNeeded()), so the next tab tap after the id is stamped crashes.
Instrumented setId override + logcat (app with rn-pendo-sdk 3.13.1 / Pendo Android SDK 3.13.7):
W RNS_SETID: CBNV.setId(2)
W RNS_SETID: java.lang.Exception: who-set-the-bnv-id
W RNS_SETID: at com.swmansion.rnscreens...CustomBottomNavigationView.setId(CustomBottomNavigationView.kt:16)
W RNS_SETID: at sdk.pendo.io.x6.h.a(SourceFile:5)
W RNS_SETID: at sdk.pendo.io.x6.h$f.invokeSuspend(SourceFile:18)
W RNS_SETID: at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
...
Corresponding before/after diagnostic (bnv.id = CustomBottomNavigationView.id, logged from TabsHostA11yCoordinator):
# before Pendo tags the bar
bnv.id=-1 itemId=1 found=BottomNavigationItemView # no collision
bnv.id=-1 itemId=2 found=BottomNavigationItemView
# after Pendo's scan stamps id=2 on the bar
bnv.id=2 itemId=1 found=BottomNavigationItemView
bnv.id=2 itemId=2 found=CustomBottomNavigationView # collision → crash on next select
Expected: selecting a tab should not crash.
Suggested fix — two independent hardenings:
-
Look the item view up as a View and type-check before use (and set contentDescription, which does not need the view, unconditionally):
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
menuItem.contentDescription = tabsScreen.tabBarItemAccessibilityLabel
}
val menuView = bottomNavigationView.findViewById<View>(menuItem.itemId)
if (menuView is NavigationBarItemView) {
menuView.tag = tabsScreen.tabBarItemTestID
}
-
Assign CustomBottomNavigationView a generated id at construction, the same way TabsContainer already does for itself (ViewIdGenerator), so third-party id-tagging can never collide with a menu-item id lookup.
Happy to open a PR for either or both.
Steps to reproduce
Full instructions (including native-build steps) are in the repro repo; summary:
NativeTabs (expo-router/unstable-native-tabs) with a couple of triggers, each a nested Stack route.
- Wire in
rn-pendo-sdk (PendoSDK.setup() + WithPendoExpoRouter for screen tracking) — any Pendo app key works, it's a public send-only value.
- Run on Android (New Architecture), start a Pendo session, then switch tabs a few times (a short pause between taps — Pendo's scan is debounced ~750ms after a screen change).
- → Crash.
Snack or a link to a repository
https://github.com/ekrapfl/rn-screens-nativetabs-repro
Screens version
4.25.2
React Native version
0.85.3
Platforms
Android
JavaScript runtime
Hermes
Workflow
Expo managed workflow
Architecture
Fabric (New Architecture)
Build type
Debug mode
Device
Android emulator
Device model
Pixel 10 Pro XL emulator, Android 17 (API 37)
Acknowledgements
Yes
Description
On Android,
NativeTabs(the gamma native bottom tabs, viaexpo-router/unstable-native-tabs) crashes with aClassCastExceptionwhen a tab is selected after a third-party SDK assigns a view id to the (id-less)CustomBottomNavigationView.Real-world trigger: the Pendo Mobile SDK (
rn-pendo-sdk). Its codeless analytics walks the view hierarchy and assigns its own small sequential ids to views that have none (View.NO_ID), as part of a screen-scoped dynamic-element scan.react-native-screensnever assignsCustomBottomNavigationViewan id at construction, so it isView.NO_IDby default — and Pendo was observed stampingid=2onto it, colliding with menu-item id 2. Confirmed with an instrumentedsetIdoverride; the caller wassdk.pendo.io.x6.h(stack trace below). This is not Pendo-specific — any SDK or app code that tags id-less views the same way (a common codeless-analytics pattern) will hit the same collision.A minimal, runnable reproduction (drops in the real Pendo SDK, no fakes) is here: https://github.com/ekrapfl/rn-screens-nativetabs-repro
Stack trace:
Root cause (confirmed with on-device instrumentation). In
TabsHostA11yCoordinator.setA11yPropertiesToTabItem:View.findViewById(id)returns the receiver itself whenreceiver.id == id.fragmentIndex + 1, i.e.1..N, perMenuHelpers.menuItemIdForFragmentAtIndex).CustomBottomNavigationViewis never given an id (seeCustomBottomNavigationView.kt— noid = ...anywhere), unlikeTabsContainer, which generates one for itself viaViewIdGenerator(TabsContainer.kt:56).1..N,findViewById(n)returns the bar itself instead of then-th menu item, and the inline cast toNavigationBarItemViewthrows.setA11yPropertiesToAllTabItems()runs on every selection change (invalidateOnSelectedTabChanged()→updateBottomNavigationViewAppearanceIfNeeded()), so the next tab tap after the id is stamped crashes.Instrumented
setIdoverride + logcat (app withrn-pendo-sdk3.13.1 / Pendo Android SDK 3.13.7):Corresponding before/after diagnostic (
bnv.id=CustomBottomNavigationView.id, logged fromTabsHostA11yCoordinator):Expected: selecting a tab should not crash.
Suggested fix — two independent hardenings:
Look the item view up as a
Viewand type-check before use (and setcontentDescription, which does not need the view, unconditionally):Assign
CustomBottomNavigationViewa generated id at construction, the same wayTabsContaineralready does for itself (ViewIdGenerator), so third-party id-tagging can never collide with a menu-item id lookup.Happy to open a PR for either or both.
Steps to reproduce
Full instructions (including native-build steps) are in the repro repo; summary:
NativeTabs(expo-router/unstable-native-tabs) with a couple of triggers, each a nestedStackroute.rn-pendo-sdk(PendoSDK.setup()+WithPendoExpoRouterfor screen tracking) — any Pendo app key works, it's a public send-only value.Snack or a link to a repository
https://github.com/ekrapfl/rn-screens-nativetabs-repro
Screens version
4.25.2
React Native version
0.85.3
Platforms
Android
JavaScript runtime
Hermes
Workflow
Expo managed workflow
Architecture
Fabric (New Architecture)
Build type
Debug mode
Device
Android emulator
Device model
Pixel 10 Pro XL emulator, Android 17 (API 37)
Acknowledgements
Yes