From 82c65a6ab3f90eca274fa952218173a71e635506 Mon Sep 17 00:00:00 2001 From: Mad Dinh Date: Fri, 21 Aug 2026 12:45:44 +0700 Subject: [PATCH] Fix nullable float props crashing ViewManagers on Android A codegen spec that declares an optional float with `WithDefault` makes codegen emit a `@Nullable Float` setter on the generated ViewManager interface. If the ViewManager implements that setter with `@ReactProp`, the app crashes during startup with: RuntimeException: Unrecognized type: class java.lang.Float for method: MyNativeViewManager#setTestFloatNullable `ViewManagersPropertyCache.createPropSetter` maps `java.lang.Boolean` and `java.lang.Integer` to boxed prop setters, but has no branch for `java.lang.Float`, so it falls through to the `else` and throws. Those three plus `@Nullable Integer` for colors are the only boxed types codegen can emit, which makes Float the single gap. Add the missing `BoxedFloatPropSetter`, mirroring `BoxedIntPropSetter`: unbox the `Double` that every JS number arrives as, and pass `null` through untouched. `DoubleTypeAnnotation` needs no equivalent because codegen always emits it as a primitive `double`. Fixes #55350 --- .../uimanager/ViewManagersPropertyCache.kt | 13 +++++++++++ .../ReactPropAnnotationSetterTest.kt | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.kt index 9e8c8bcf8438..116ed4ce00df 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.kt @@ -259,6 +259,18 @@ internal object ViewManagersPropertyCache { } } + private class BoxedFloatPropSetter(prop: ReactProp, setter: Method) : + PropSetter(prop, "number", setter) { + + override fun getValueOrDefault(value: Any?, context: Context): Any? { + if (value != null) { + // All numbers from JS are Doubles which can't be simply cast to Float + return if (value is Double) value.toFloat() else value as Float + } + return null + } + } + private class BoxedIntPropSetter : PropSetter { constructor(prop: ReactProp, setter: Method) : super(prop, "number", setter) @@ -391,6 +403,7 @@ internal object ViewManagersPropertyCache { DoublePropSetter(annotation, method, annotation.defaultDouble) String::class.java -> StringPropSetter(annotation, method) java.lang.Boolean::class.java -> BoxedBooleanPropSetter(annotation, method) + java.lang.Float::class.java -> BoxedFloatPropSetter(annotation, method) java.lang.Integer::class.java -> if ("Color" == annotation.customType) { BoxedColorPropSetter(annotation, method) diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterTest.kt index 0813c85acaa6..c04465036908 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterTest.kt @@ -39,6 +39,8 @@ class ReactPropAnnotationSetterTest { fun onBoxedBooleanSetterCalled(value: Boolean?) + fun onBoxedFloatSetterCalled(value: Float?) + fun onBoxedIntSetterCalled(value: Int?) fun onArraySetterCalled(value: ReadableArray?) @@ -128,6 +130,11 @@ class ReactPropAnnotationSetterTest { viewManagerUpdatesReceiver.onBoxedIntSetterCalled(value) } + @ReactProp(name = "boxedFloatProp") + fun setBoxedFloatProp(v: View?, value: Float?) { + viewManagerUpdatesReceiver.onBoxedFloatSetterCalled(value) + } + @ReactProp(name = "arrayProp") fun setArrayProp(v: View?, value: ReadableArray?) { viewManagerUpdatesReceiver.onArraySetterCalled(value) @@ -314,6 +321,22 @@ class ReactPropAnnotationSetterTest { Mockito.reset(updatesReceiverMock) } + @Test + fun testBoxedFloatSetter() { + viewManager.updateProperties(targetView, buildStyles("boxedFloatProp", 3.5)) + Mockito.verify(updatesReceiverMock).onBoxedFloatSetterCalled(3.5f) + Mockito.verifyNoMoreInteractions(updatesReceiverMock) + Mockito.reset(updatesReceiverMock) + viewManager.updateProperties(targetView, buildStyles("boxedFloatProp", -7.0)) + Mockito.verify(updatesReceiverMock).onBoxedFloatSetterCalled(-7.0f) + Mockito.verifyNoMoreInteractions(updatesReceiverMock) + Mockito.reset(updatesReceiverMock) + viewManager.updateProperties(targetView, buildStyles("boxedFloatProp", null)) + Mockito.verify(updatesReceiverMock).onBoxedFloatSetterCalled(null) + Mockito.verifyNoMoreInteractions(updatesReceiverMock) + Mockito.reset(updatesReceiverMock) + } + @Test fun testArraySetter() { val array: ReadableArray = JavaOnlyArray()