Skip to content

Android: NullPointerException in ButtonViewGroup.animatePressOut when view is detached (getHandler() == null) #4293

Description

@tomekzaw

Description

On Android, ButtonViewGroup.animatePressOut() can crash with a NullPointerException when it dereferences the view's Handler, which is null while the view is detached from the window.

handler here is android.view.View.getHandler(), which returns null when the view is not attached to a window (mAttachInfo == null). A cancel touch event can be delivered to a button that has just been detached: the orchestrator calls NativeViewGestureHandler.onCanceldispatchCancelEventToViewbutton.onTouchEvent(CANCEL)setPressed(false)animatePressOut(), where handler.removeCallbacks(...) throws on the null handler.

The handler receiver is dereferenced unguarded at four sites (animatePressIn, animatePressOut, and onDetachedFromWindow). On main these are lines 710, 769, 797 and 914 of RNGestureHandlerButtonViewManager.kt. Guarding them with handler?. fixes the crash (a detached view has nothing meaningful to schedule/cancel and shows no visible animation, so a no-op is correct).

NullPointerException: Attempt to invoke virtual method 'void android.os.Handler.removeCallbacks(java.lang.Runnable)' on a null object reference
  at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.animatePressOut(RNGestureHandlerButtonViewManager.kt:641)
  at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.setPressed(RNGestureHandlerButtonViewManager.kt:935)
  at android.view.View.onTouchEvent(View.java:19360)
  at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.onTouchEvent(RNGestureHandlerButtonViewManager.kt:523)
  at com.swmansion.gesturehandler.core.NativeViewGestureHandler$NativeViewGestureHandlerHook$DefaultImpls.sendTouchEvent(NativeViewGestureHandler.kt:303)
  at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.access$getDummyLongClickListener$cp(RNGestureHandlerButtonViewManager.kt:356)
  at com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager$ButtonViewGroup.sendTouchEvent(RNGestureHandlerButtonViewManager.kt:356)
  at com.swmansion.gesturehandler.core.NativeViewGestureHandler.dispatchCancelEventToView(NativeViewGestureHandler.kt:171)
  at com.swmansion.gesturehandler.core.NativeViewGestureHandler.onCancel(NativeViewGestureHandler.kt:175)
  at com.swmansion.gesturehandler.core.GestureHandler.cancel(GestureHandler.kt:749)
  at com.swmansion.gesturehandler.core.GestureHandlerOrchestrator.deliverEventToGestureHandler(GestureHandlerOrchestrator.kt:299)
  at com.swmansion.gesturehandler.core.GestureHandlerOrchestrator.deliverEventToGestureHandlers(GestureHandlerOrchestrator.kt:276)
  at com.swmansion.gesturehandler.core.GestureHandlerOrchestrator.onTouchEvent(GestureHandlerOrchestrator.kt:60)
  at com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper.dispatchTouchEvent(RNGestureHandlerRootHelper.kt:130)
  at com.swmansion.gesturehandler.react.RNGestureHandlerRootView.dispatchTouchEvent(RNGestureHandlerRootView.kt:58)
  at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3371)
  at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:3047)
  ... (framework frames)
  at android.app.Activity.dispatchTouchEvent(Activity.java:4776)
  at android.view.View.dispatchPointerEvent(View.java:17527)
  at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:9847)
  ... (ViewRootImpl input dispatch frames)
  at android.os.Handler.handleCallback(Handler.java:995)
  at android.os.Looper.loop(Looper.java:363)
  at android.app.ActivityThread.main(ActivityThread.java:10060)

(Line numbers in the trace are from v3.0.2; the same unguarded calls are present on main.)

Steps to reproduce

This is a race between window detachment and touch cancellation, caught in production via Sentry (single occurrence, not deterministically reproducible). It occurs when:

  1. A button (Pressable/BaseButton/RectButton) is pressed.
  2. The button is unmounted / detached from the window (e.g. a navigation transition or list re-render) while still in the pressed state.
  3. A cancel event is delivered to the now-detached button, so setPressed(false)animatePressOut() runs with getHandler() == null.

Gesture Handler version

3.0.2 (also present on main)

React Native version

0.86.0

Platforms

Android

JavaScript runtime

Hermes

Workflow

Using Expo Prebuild or an Expo development build

Architecture

New Architecture (Fabric)

Build type

Release mode

Device

Real device

Device model

Samsung Galaxy (SM-M346B1), Android 16

Additional device info

  • Device: samsung SM-M346B1, device class: medium
  • OS: Android 16, build BP2A.250605.031.A3.M346B1DXSCEZD8, kernel 5.10.237
  • Chipset: Samsung s5e8825, 8 cores, arm64-v8a
  • Screen: 1080x2340, density 2.625 (420 dpi)
  • Not rooted, not an emulator, installed from Google Play

Acknowledgements

Yes

Metadata

Metadata

Assignees

No one assigned

    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