From 32e99546584c7e33697220373567f6b281cd660c Mon Sep 17 00:00:00 2001 From: Mad Dinh Date: Fri, 21 Aug 2026 13:00:43 +0700 Subject: [PATCH] Fix DimensionValue props crashing ViewManagers on Android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codegen maps a `DimensionValue` prop to the `DimensionPrimitive` reserved type and emits a `@Nullable YogaValue` setter on the generated ViewManager interface. If the ViewManager implements that setter with `@ReactProp`, the app crashes while collecting view manager constants: RuntimeException: Unrecognized type: class com.facebook.yoga.YogaValue for method: MyNativeViewManager#setMarginBack `ViewManagersPropertyCache.createPropSetter` has no branch for `YogaValue`, so it falls through to the `else` and throws. The conversion already exists: `DimensionPropConverter` handles the three shapes a dimension arrives in (null, a Double in points, a String such as "100%") and is unit tested. It was only ever wired into generated delegate code, never into the `@ReactProp` path — unlike `ColorPropConverter`, which is used by both. Add the missing `DimensionPropSetter`, modelled on `ColorPropSetter`, delegating to that converter. It reports `mixed` as its prop type since a dimension may be a number or a string, matching how colors are reported. This is the same class of gap as the `@Nullable Float` one. With both closed, every Java type `GeneratePropsJavaInterface` can emit is bindable through `@ReactProp`. --- .../uimanager/ViewManagersPropertyCache.kt | 12 ++++++++ .../ReactPropAnnotationSetterTest.kt | 30 +++++++++++++++++++ 2 files changed, 42 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..d52cca42ff1e 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 @@ -11,6 +11,7 @@ import android.content.Context import android.view.View import com.facebook.common.logging.FLog import com.facebook.react.bridge.ColorPropConverter +import com.facebook.react.bridge.DimensionPropConverter import com.facebook.react.bridge.Dynamic import com.facebook.react.bridge.DynamicFromObject import com.facebook.react.bridge.JSApplicationIllegalArgumentException @@ -18,6 +19,7 @@ import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.annotations.ReactPropGroup +import com.facebook.yoga.YogaValue import java.lang.reflect.Method /** @@ -192,6 +194,15 @@ internal object ViewManagersPropertyCache { } } + private class DimensionPropSetter(prop: ReactProp, setter: Method) : + PropSetter(prop, "mixed", setter) { + + // A DimensionValue arrives from JS as either a number (points) or a string + // (e.g. "100%"), and is nullable, so the conversion is delegated wholesale. + override fun getValueOrDefault(value: Any?, context: Context): Any? = + DimensionPropConverter.getDimension(value) + } + private class BooleanPropSetter( prop: ReactProp, setter: Method, @@ -399,6 +410,7 @@ internal object ViewManagersPropertyCache { } ReadableArray::class.java -> ArrayPropSetter(annotation, method) ReadableMap::class.java -> MapPropSetter(annotation, method) + YogaValue::class.java -> DimensionPropSetter(annotation, method) else -> throw RuntimeException( "Unrecognized type: $propTypeClass for method: ${method.declaringClass.name}#${method.name}", 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..0860a3133985 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 @@ -16,6 +16,8 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.annotations.ReactPropGroup +import com.facebook.yoga.YogaUnit +import com.facebook.yoga.YogaValue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -41,6 +43,8 @@ class ReactPropAnnotationSetterTest { fun onBoxedIntSetterCalled(value: Int?) + fun onDimensionSetterCalled(value: YogaValue?) + fun onArraySetterCalled(value: ReadableArray?) fun onMapSetterCalled(value: ReadableMap?) @@ -128,6 +132,11 @@ class ReactPropAnnotationSetterTest { viewManagerUpdatesReceiver.onBoxedIntSetterCalled(value) } + @ReactProp(name = "dimensionProp") + fun setDimensionProp(v: View?, value: YogaValue?) { + viewManagerUpdatesReceiver.onDimensionSetterCalled(value) + } + @ReactProp(name = "arrayProp") fun setArrayProp(v: View?, value: ReadableArray?) { viewManagerUpdatesReceiver.onArraySetterCalled(value) @@ -314,6 +323,27 @@ class ReactPropAnnotationSetterTest { Mockito.reset(updatesReceiverMock) } + @Test + fun testDimensionSetter() { + viewManager.updateProperties(targetView, buildStyles("dimensionProp", 10.5)) + Mockito.verify(updatesReceiverMock).onDimensionSetterCalled(YogaValue(10.5f, YogaUnit.POINT)) + Mockito.verifyNoMoreInteractions(updatesReceiverMock) + Mockito.reset(updatesReceiverMock) + viewManager.updateProperties(targetView, buildStyles("dimensionProp", "100%")) + Mockito.verify(updatesReceiverMock).onDimensionSetterCalled(YogaValue(100f, YogaUnit.PERCENT)) + Mockito.verifyNoMoreInteractions(updatesReceiverMock) + Mockito.reset(updatesReceiverMock) + viewManager.updateProperties(targetView, buildStyles("dimensionProp", null)) + Mockito.verify(updatesReceiverMock).onDimensionSetterCalled(null) + Mockito.verifyNoMoreInteractions(updatesReceiverMock) + Mockito.reset(updatesReceiverMock) + } + + @Test(expected = JSApplicationIllegalArgumentException::class) + fun testFailToUpdateDimensionPropWithArray() { + viewManager.updateProperties(targetView, buildStyles("dimensionProp", JavaOnlyArray())) + } + @Test fun testArraySetter() { val array: ReadableArray = JavaOnlyArray()