Skip to content

[expo-router] NativeTabs: ClassCastException on Android when CustomBottomNavigationView is assigned a colliding view id #4285

Description

@ekrapfl

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:

  1. 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
    }
  2. 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:

  1. NativeTabs (expo-router/unstable-native-tabs) with a couple of triggers, each a nested Stack route.
  2. Wire in rn-pendo-sdk (PendoSDK.setup() + WithPendoExpoRouter for screen tracking) — any Pendo app key works, it's a public send-only value.
  3. 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).
  4. → 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

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

    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