From 91f776407401599a0057bfed55e864650166ef64 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Wed, 20 May 2026 10:23:03 -0700 Subject: [PATCH] Migrate ViewManagersPropertyCache to Kotlin Summary: Convert `ViewManagersPropertyCache` from Java to Kotlin as part of the ongoing React Native Android migration to 100% Kotlin. The class is package-private (now `internal`) and is only consumed by `ViewManagerPropertyUpdater`, so there is no public OSS-visible API change. The public `PropSetter` getters `getPropName()` / `getPropType()` are preserved as Kotlin `val` properties with the same JVM signatures, keeping byte-compatibility with the existing Kotlin caller. Adjacent changes in this revision: - Drop the `VIEW_MGR_ARGS` / `VIEW_MGR_GROUP_ARGS` / `SHADOW_ARGS` / `SHADOW_GROUP_ARGS` `ThreadLocal` caches. Their purpose in the Java version was to avoid the per-call `Object[]` allocation that `Method.invoke`'s varargs would otherwise force; in Kotlin, the spread operator (`*args`) emits a defensive array copy on every call, so the caches no longer save anything. Calling `setter.invoke(viewManager, viewToUpdate, ...)` with positional arguments has the same allocation cost and removes the dead complexity. - Restore the original non-null parameter contract on `getNativePropSettersForShadowNodeClass`: the recursion now bottoms out by reading `cls.superclass` once and only recursing when it is non-null, matching the Java behavior without widening the public/internal API. - Add `Suppress("DEPRECATION")` at the file level since `PropSetter.updateShadowNodeProp` legitimately consumes the deprecated `ReactShadowNode` API surface. - Remove `ViewManagersPropertyCache.java` from the `ReactNoNewJavaDetector` allow-list. Changelog: [Internal] Differential Revision: D105847022 --- .../uimanager/ViewManagersPropertyCache.java | 614 ------------------ .../uimanager/ViewManagersPropertyCache.kt | 527 +++++++++++++++ 2 files changed, 527 insertions(+), 614 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java deleted file mode 100644 index 09ae7a272ad0..000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java +++ /dev/null @@ -1,614 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.uimanager; - -import android.content.Context; -import android.view.View; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.react.bridge.ColorPropConverter; -import com.facebook.react.bridge.Dynamic; -import com.facebook.react.bridge.DynamicFromObject; -import com.facebook.react.bridge.JSApplicationIllegalArgumentException; -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 java.lang.reflect.Method; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** - * This class is responsible for holding view manager property setters and is used in a process of - * updating views with the new properties set in JS. - */ -/*package*/ class ViewManagersPropertyCache { - - private static final Map> CLASS_PROPS_CACHE = new HashMap<>(); - private static final Map EMPTY_PROPS_MAP = new HashMap<>(); - - public static void clear() { - CLASS_PROPS_CACHE.clear(); - EMPTY_PROPS_MAP.clear(); - } - - /*package*/ abstract static class PropSetter { - - protected final String mPropName; - protected final String mPropType; - protected final Method mSetter; - protected final @Nullable Integer mIndex; /* non-null only for group setters */ - - // The following Object arrays are used to prevent extra allocations from varargs when we call. - private static final ThreadLocal VIEW_MGR_ARGS = createThreadLocalArray(2); - private static final ThreadLocal VIEW_MGR_GROUP_ARGS = createThreadLocalArray(3); - private static final ThreadLocal SHADOW_ARGS = createThreadLocalArray(1); - private static final ThreadLocal SHADOW_GROUP_ARGS = createThreadLocalArray(2); - - private PropSetter(ReactProp prop, String defaultType, Method setter) { - mPropName = prop.name(); - mPropType = - ReactProp.USE_DEFAULT_TYPE.equals(prop.customType()) ? defaultType : prop.customType(); - mSetter = setter; - mIndex = null; - } - - private PropSetter(ReactPropGroup prop, String defaultType, Method setter, int index) { - mPropName = prop.names()[index]; - mPropType = - ReactPropGroup.USE_DEFAULT_TYPE.equals(prop.customType()) - ? defaultType - : prop.customType(); - mSetter = setter; - mIndex = index; - } - - public String getPropName() { - return mPropName; - } - - public String getPropType() { - return mPropType; - } - - public void updateViewProp(ViewManager viewManager, View viewToUpdate, Object value) { - try { - Object[] args; - if (mIndex == null) { - args = VIEW_MGR_ARGS.get(); - args[0] = viewToUpdate; - args[1] = getValueOrDefault(value, viewToUpdate.getContext()); - } else { - args = VIEW_MGR_GROUP_ARGS.get(); - args[0] = viewToUpdate; - args[1] = mIndex; - args[2] = getValueOrDefault(value, viewToUpdate.getContext()); - } - mSetter.invoke(viewManager, args); - Arrays.fill(args, null); - } catch (Throwable t) { - FLog.e(ViewManager.class, "Error while updating prop " + mPropName, t); - throw new JSApplicationIllegalArgumentException( - "Error while updating property '" - + mPropName - + "' of a view managed by: " - + viewManager.getName(), - t); - } - } - - public void updateShadowNodeProp(ReactShadowNode nodeToUpdate, Object value) { - try { - Object[] args; - if (mIndex == null) { - args = SHADOW_ARGS.get(); - args[0] = getValueOrDefault(value, nodeToUpdate.getThemedContext()); - } else { - args = SHADOW_GROUP_ARGS.get(); - args[0] = mIndex; - args[1] = getValueOrDefault(value, nodeToUpdate.getThemedContext()); - } - mSetter.invoke(nodeToUpdate, args); - Arrays.fill(args, null); - } catch (Throwable t) { - FLog.e(ViewManager.class, "Error while updating prop " + mPropName, t); - throw new JSApplicationIllegalArgumentException( - "Error while updating property '" - + mPropName - + "' in shadow node of type: " - + nodeToUpdate.getViewClass(), - t); - } - } - - protected abstract @Nullable Object getValueOrDefault(Object value, Context context); - } - - private static class DynamicPropSetter extends PropSetter { - - public DynamicPropSetter(ReactProp prop, Method setter) { - super(prop, "mixed", setter); - } - - public DynamicPropSetter(ReactPropGroup prop, Method setter, int index) { - super(prop, "mixed", setter, index); - } - - @Override - protected Object getValueOrDefault(Object value, Context context) { - if (value instanceof Dynamic) { - return value; - } else { - return new DynamicFromObject(value); - } - } - } - - private static class IntPropSetter extends PropSetter { - - private final int mDefaultValue; - - public IntPropSetter(ReactProp prop, Method setter, int defaultValue) { - super(prop, "number", setter); - mDefaultValue = defaultValue; - } - - public IntPropSetter(ReactPropGroup prop, Method setter, int index, int defaultValue) { - super(prop, "number", setter, index); - mDefaultValue = defaultValue; - } - - @Override - protected Object getValueOrDefault(Object value, Context context) { - // All numbers from JS are Doubles which can't be simply cast to Integer - return value == null ? mDefaultValue : (Integer) ((Double) value).intValue(); - } - } - - private static class DoublePropSetter extends PropSetter { - - private final double mDefaultValue; - - public DoublePropSetter(ReactProp prop, Method setter, double defaultValue) { - super(prop, "number", setter); - mDefaultValue = defaultValue; - } - - public DoublePropSetter(ReactPropGroup prop, Method setter, int index, double defaultValue) { - super(prop, "number", setter, index); - mDefaultValue = defaultValue; - } - - @Override - protected Object getValueOrDefault(Object value, Context context) { - return value == null ? mDefaultValue : (Double) value; - } - } - - private static class ColorPropSetter extends PropSetter { - - private final int mDefaultValue; - - public ColorPropSetter(ReactProp prop, Method setter) { - this(prop, setter, 0); - } - - public ColorPropSetter(ReactProp prop, Method setter, int defaultValue) { - super(prop, "mixed", setter); - mDefaultValue = defaultValue; - } - - public ColorPropSetter(ReactPropGroup prop, Method setter, int index, int defaultValue) { - super(prop, "mixed", setter, index); - mDefaultValue = defaultValue; - } - - @Override - protected Object getValueOrDefault(Object value, Context context) { - if (value == null) { - return mDefaultValue; - } - - return ColorPropConverter.getColor(value, context); - } - } - - private static class BooleanPropSetter extends PropSetter { - - private final boolean mDefaultValue; - - public BooleanPropSetter(ReactProp prop, Method setter, boolean defaultValue) { - super(prop, "boolean", setter); - mDefaultValue = defaultValue; - } - - @Override - protected Object getValueOrDefault(Object value, Context context) { - boolean val = value == null ? mDefaultValue : (boolean) value; - return val ? Boolean.TRUE : Boolean.FALSE; - } - } - - private static class FloatPropSetter extends PropSetter { - - private final float mDefaultValue; - - public FloatPropSetter(ReactProp prop, Method setter, float defaultValue) { - super(prop, "number", setter); - mDefaultValue = defaultValue; - } - - public FloatPropSetter(ReactPropGroup prop, Method setter, int index, float defaultValue) { - super(prop, "number", setter, index); - mDefaultValue = defaultValue; - } - - @Override - protected Object getValueOrDefault(Object value, Context context) { - // All numbers from JS are Doubles which can't be simply cast to Float - return value == null ? mDefaultValue : (Float) ((Double) value).floatValue(); - } - } - - private static class ArrayPropSetter extends PropSetter { - - public ArrayPropSetter(ReactProp prop, Method setter) { - super(prop, "Array", setter); - } - - @Override - protected @Nullable Object getValueOrDefault(Object value, Context context) { - return (ReadableArray) value; - } - } - - private static class MapPropSetter extends PropSetter { - - public MapPropSetter(ReactProp prop, Method setter) { - super(prop, "Map", setter); - } - - @Override - protected @Nullable Object getValueOrDefault(Object value, Context context) { - return (ReadableMap) value; - } - } - - private static class StringPropSetter extends PropSetter { - - public StringPropSetter(ReactProp prop, Method setter) { - super(prop, "String", setter); - } - - @Override - protected @Nullable Object getValueOrDefault(Object value, Context context) { - return (String) value; - } - } - - private static class BoxedBooleanPropSetter extends PropSetter { - - public BoxedBooleanPropSetter(ReactProp prop, Method setter) { - super(prop, "boolean", setter); - } - - @Override - protected @Nullable Object getValueOrDefault(Object value, Context context) { - if (value != null) { - return (boolean) value ? Boolean.TRUE : Boolean.FALSE; - } - return null; - } - } - - private static class BoxedIntPropSetter extends PropSetter { - - public BoxedIntPropSetter(ReactProp prop, Method setter) { - super(prop, "number", setter); - } - - public BoxedIntPropSetter(ReactPropGroup prop, Method setter, int index) { - super(prop, "number", setter, index); - } - - @Override - protected @Nullable Object getValueOrDefault(Object value, Context context) { - if (value != null) { - if (value instanceof Double) { - return ((Double) value).intValue(); - } else { - return (Integer) value; - } - } - return null; - } - } - - private static class BoxedColorPropSetter extends PropSetter { - - public BoxedColorPropSetter(ReactProp prop, Method setter) { - super(prop, "mixed", setter); - } - - public BoxedColorPropSetter(ReactPropGroup prop, Method setter, int index) { - super(prop, "mixed", setter, index); - } - - @Override - protected @Nullable Object getValueOrDefault(Object value, Context context) { - if (value != null) { - return ColorPropConverter.getColor(value, context); - } - return null; - } - } - - /*package*/ static Map getNativePropsForView( - Class viewManagerTopClass, - Class shadowNodeTopClass) { - Map nativeProps = new HashMap<>(); - - Map viewManagerProps = - getNativePropSettersForViewManagerClass(viewManagerTopClass); - for (PropSetter setter : viewManagerProps.values()) { - nativeProps.put(setter.getPropName(), setter.getPropType()); - } - - Map shadowNodeProps = - getNativePropSettersForShadowNodeClass(shadowNodeTopClass); - for (PropSetter setter : shadowNodeProps.values()) { - nativeProps.put(setter.getPropName(), setter.getPropType()); - } - - return nativeProps; - } - - /** - * Returns map from property name to setter instances for all the property setters annotated with - * {@link ReactProp} in the given {@link ViewManager} class plus all the setter declared by its - * parent classes. - */ - /*package*/ static Map getNativePropSettersForViewManagerClass( - Class cls) { - if (cls == ViewManager.class) { - return EMPTY_PROPS_MAP; - } - Map props = CLASS_PROPS_CACHE.get(cls); - if (props != null) { - return props; - } - // This is to include all the setters from parent classes. Once calculated the result will be - // stored in CLASS_PROPS_CACHE so that we only scan for @ReactProp annotations once per class. - props = - new HashMap<>( - getNativePropSettersForViewManagerClass( - (Class) cls.getSuperclass())); - extractPropSettersFromViewManagerClassDefinition(cls, props); - CLASS_PROPS_CACHE.put(cls, props); - return props; - } - - /** - * Returns map from property name to setter instances for all the property setters annotated with - * {@link ReactProp} (or {@link ReactPropGroup} in the given {@link ReactShadowNode} subclass plus - * all the setters declared by its parent classes up to {@link ReactShadowNode} which is treated - * as a base class. - */ - /*package*/ static Map getNativePropSettersForShadowNodeClass( - Class cls) { - if (cls == null) { - return EMPTY_PROPS_MAP; - } - ; - - for (Class iface : cls.getInterfaces()) { - if (iface == ReactShadowNode.class) { - return EMPTY_PROPS_MAP; - } - } - Map props = CLASS_PROPS_CACHE.get(cls); - if (props != null) { - return props; - } - // This is to include all the setters from parent classes up to ReactShadowNode class - props = - new HashMap<>( - getNativePropSettersForShadowNodeClass( - (Class) cls.getSuperclass())); - extractPropSettersFromShadowNodeClassDefinition(cls, props); - CLASS_PROPS_CACHE.put(cls, props); - return props; - } - - private static PropSetter createPropSetter( - ReactProp annotation, Method method, Class propTypeClass) { - if (propTypeClass == Dynamic.class) { - return new DynamicPropSetter(annotation, method); - } else if (propTypeClass == boolean.class) { - return new BooleanPropSetter(annotation, method, annotation.defaultBoolean()); - } else if (propTypeClass == int.class) { - if ("Color".equals(annotation.customType())) { - return new ColorPropSetter(annotation, method, annotation.defaultInt()); - } - return new IntPropSetter(annotation, method, annotation.defaultInt()); - } else if (propTypeClass == float.class) { - return new FloatPropSetter(annotation, method, annotation.defaultFloat()); - } else if (propTypeClass == double.class) { - return new DoublePropSetter(annotation, method, annotation.defaultDouble()); - } else if (propTypeClass == String.class) { - return new StringPropSetter(annotation, method); - } else if (propTypeClass == Boolean.class) { - return new BoxedBooleanPropSetter(annotation, method); - } else if (propTypeClass == Integer.class) { - if ("Color".equals(annotation.customType())) { - return new BoxedColorPropSetter(annotation, method); - } - return new BoxedIntPropSetter(annotation, method); - } else if (propTypeClass == ReadableArray.class) { - return new ArrayPropSetter(annotation, method); - } else if (propTypeClass == ReadableMap.class) { - return new MapPropSetter(annotation, method); - } else { - throw new RuntimeException( - "Unrecognized type: " - + propTypeClass - + " for method: " - + method.getDeclaringClass().getName() - + "#" - + method.getName()); - } - } - - private static void createPropSetters( - ReactPropGroup annotation, - Method method, - Class propTypeClass, - Map props) { - String[] names = annotation.names(); - if (propTypeClass == Dynamic.class) { - for (int i = 0; i < names.length; i++) { - props.put(names[i], new DynamicPropSetter(annotation, method, i)); - } - } else if (propTypeClass == int.class) { - for (int i = 0; i < names.length; i++) { - if ("Color".equals(annotation.customType())) { - props.put(names[i], new ColorPropSetter(annotation, method, i, annotation.defaultInt())); - } else { - props.put(names[i], new IntPropSetter(annotation, method, i, annotation.defaultInt())); - } - } - } else if (propTypeClass == float.class) { - for (int i = 0; i < names.length; i++) { - props.put(names[i], new FloatPropSetter(annotation, method, i, annotation.defaultFloat())); - } - } else if (propTypeClass == double.class) { - for (int i = 0; i < names.length; i++) { - props.put( - names[i], new DoublePropSetter(annotation, method, i, annotation.defaultDouble())); - } - } else if (propTypeClass == Integer.class) { - for (int i = 0; i < names.length; i++) { - if ("Color".equals(annotation.customType())) { - props.put(names[i], new BoxedColorPropSetter(annotation, method, i)); - } else { - props.put(names[i], new BoxedIntPropSetter(annotation, method, i)); - } - } - } else { - throw new RuntimeException( - "Unrecognized type: " - + propTypeClass - + " for method: " - + method.getDeclaringClass().getName() - + "#" - + method.getName()); - } - } - - private static void extractPropSettersFromViewManagerClassDefinition( - Class cls, Map props) { - Method[] declaredMethods = cls.getDeclaredMethods(); - for (int i = 0; i < declaredMethods.length; i++) { - Method method = declaredMethods[i]; - ReactProp annotation = method.getAnnotation(ReactProp.class); - if (annotation != null) { - Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length != 2) { - throw new RuntimeException( - "Wrong number of args for prop setter: " + cls.getName() + "#" + method.getName()); - } - if (!View.class.isAssignableFrom(paramTypes[0])) { - throw new RuntimeException( - "First param should be a view subclass to be updated: " - + cls.getName() - + "#" - + method.getName()); - } - props.put(annotation.name(), createPropSetter(annotation, method, paramTypes[1])); - } - - ReactPropGroup groupAnnotation = method.getAnnotation(ReactPropGroup.class); - if (groupAnnotation != null) { - Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length != 3) { - throw new RuntimeException( - "Wrong number of args for group prop setter: " - + cls.getName() - + "#" - + method.getName()); - } - if (!View.class.isAssignableFrom(paramTypes[0])) { - throw new RuntimeException( - "First param should be a view subclass to be updated: " - + cls.getName() - + "#" - + method.getName()); - } - if (paramTypes[1] != int.class) { - throw new RuntimeException( - "Second argument should be property index: " - + cls.getName() - + "#" - + method.getName()); - } - createPropSetters(groupAnnotation, method, paramTypes[2], props); - } - } - } - - private static void extractPropSettersFromShadowNodeClassDefinition( - Class cls, Map props) { - for (Method method : cls.getDeclaredMethods()) { - ReactProp annotation = method.getAnnotation(ReactProp.class); - if (annotation != null) { - Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length != 1) { - throw new RuntimeException( - "Wrong number of args for prop setter: " + cls.getName() + "#" + method.getName()); - } - props.put(annotation.name(), createPropSetter(annotation, method, paramTypes[0])); - } - - ReactPropGroup groupAnnotation = method.getAnnotation(ReactPropGroup.class); - if (groupAnnotation != null) { - Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length != 2) { - throw new RuntimeException( - "Wrong number of args for group prop setter: " - + cls.getName() - + "#" - + method.getName()); - } - if (paramTypes[0] != int.class) { - throw new RuntimeException( - "Second argument should be property index: " - + cls.getName() - + "#" - + method.getName()); - } - createPropSetters(groupAnnotation, method, paramTypes[1], props); - } - } - } - - private static ThreadLocal createThreadLocalArray(final int size) { - - if (size <= 0) { - return null; - } - - return new ThreadLocal() { - @Nullable - @Override - protected Object[] initialValue() { - return new Object[size]; - } - }; - } -} 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 new file mode 100644 index 000000000000..ad0798d673ca --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.kt @@ -0,0 +1,527 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.uimanager + +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.Dynamic +import com.facebook.react.bridge.DynamicFromObject +import com.facebook.react.bridge.JSApplicationIllegalArgumentException +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 java.lang.reflect.Method + +/** + * This class is responsible for holding view manager property setters and is used in a process of + * updating views with the new properties set in JS. + */ +@Suppress("DEPRECATION") // ReactShadowNode is @Deprecated but still required by this cache. +internal object ViewManagersPropertyCache { + + private val CLASS_PROPS_CACHE: MutableMap, Map> = HashMap() + private val EMPTY_PROPS_MAP: MutableMap = HashMap() + + @JvmStatic + fun clear() { + CLASS_PROPS_CACHE.clear() + EMPTY_PROPS_MAP.clear() + } + + internal abstract class PropSetter { + + val propName: String + val propType: String + protected val setter: Method + protected val index: Int? // non-null only for group setters + + protected constructor(prop: ReactProp, defaultType: String, setter: Method) { + propName = prop.name + propType = if (ReactProp.USE_DEFAULT_TYPE == prop.customType) defaultType else prop.customType + this.setter = setter + this.index = null + } + + protected constructor(prop: ReactPropGroup, defaultType: String, setter: Method, index: Int) { + propName = prop.names[index] + propType = + if (ReactPropGroup.USE_DEFAULT_TYPE == prop.customType) defaultType else prop.customType + this.setter = setter + this.index = index + } + + fun updateViewProp(viewManager: ViewManager<*, *>, viewToUpdate: View, value: Any?) { + try { + val resolved = getValueOrDefault(value, viewToUpdate.context) + if (index == null) { + setter.invoke(viewManager, viewToUpdate, resolved) + } else { + setter.invoke(viewManager, viewToUpdate, index, resolved) + } + } catch (t: Throwable) { + FLog.e(ViewManager::class.java, "Error while updating prop $propName", t) + throw JSApplicationIllegalArgumentException( + "Error while updating property '$propName' of a view managed by: ${viewManager.name}", + t, + ) + } + } + + fun updateShadowNodeProp(nodeToUpdate: ReactShadowNode<*>, value: Any?) { + try { + val resolved = getValueOrDefault(value, nodeToUpdate.themedContext) + if (index == null) { + setter.invoke(nodeToUpdate, resolved) + } else { + setter.invoke(nodeToUpdate, index, resolved) + } + } catch (t: Throwable) { + FLog.e(ViewManager::class.java, "Error while updating prop $propName", t) + throw JSApplicationIllegalArgumentException( + "Error while updating property '$propName' in shadow node of type: ${nodeToUpdate.viewClass}", + t, + ) + } + } + + protected abstract fun getValueOrDefault(value: Any?, context: Context): Any? + } + + private class DynamicPropSetter : PropSetter { + + constructor(prop: ReactProp, setter: Method) : super(prop, "mixed", setter) + + constructor( + prop: ReactPropGroup, + setter: Method, + index: Int, + ) : super(prop, "mixed", setter, index) + + override fun getValueOrDefault(value: Any?, context: Context): Any = + if (value is Dynamic) value else DynamicFromObject(value) + } + + private class IntPropSetter : PropSetter { + + private val defaultValue: Int + + constructor( + prop: ReactProp, + setter: Method, + defaultValue: Int, + ) : super(prop, "number", setter) { + this.defaultValue = defaultValue + } + + constructor( + prop: ReactPropGroup, + setter: Method, + index: Int, + defaultValue: Int, + ) : super(prop, "number", setter, index) { + this.defaultValue = defaultValue + } + + override fun getValueOrDefault(value: Any?, context: Context): Any { + // All numbers from JS are Doubles which can't be simply cast to Integer + return if (value == null) defaultValue else (value as Double).toInt() + } + } + + private class DoublePropSetter : PropSetter { + + private val defaultValue: Double + + constructor( + prop: ReactProp, + setter: Method, + defaultValue: Double, + ) : super(prop, "number", setter) { + this.defaultValue = defaultValue + } + + constructor( + prop: ReactPropGroup, + setter: Method, + index: Int, + defaultValue: Double, + ) : super(prop, "number", setter, index) { + this.defaultValue = defaultValue + } + + override fun getValueOrDefault(value: Any?, context: Context): Any = + if (value == null) defaultValue else value as Double + } + + private class ColorPropSetter : PropSetter { + + private val defaultValue: Int + + constructor(prop: ReactProp, setter: Method) : this(prop, setter, 0) + + constructor( + prop: ReactProp, + setter: Method, + defaultValue: Int, + ) : super(prop, "mixed", setter) { + this.defaultValue = defaultValue + } + + constructor( + prop: ReactPropGroup, + setter: Method, + index: Int, + defaultValue: Int, + ) : super(prop, "mixed", setter, index) { + this.defaultValue = defaultValue + } + + override fun getValueOrDefault(value: Any?, context: Context): Any? { + if (value == null) { + return defaultValue + } + return ColorPropConverter.getColor(value, context) + } + } + + private class BooleanPropSetter( + prop: ReactProp, + setter: Method, + private val defaultValue: Boolean, + ) : PropSetter(prop, "boolean", setter) { + + override fun getValueOrDefault(value: Any?, context: Context): Any { + val v = if (value == null) defaultValue else value as Boolean + return if (v) java.lang.Boolean.TRUE else java.lang.Boolean.FALSE + } + } + + private class FloatPropSetter : PropSetter { + + private val defaultValue: Float + + constructor( + prop: ReactProp, + setter: Method, + defaultValue: Float, + ) : super(prop, "number", setter) { + this.defaultValue = defaultValue + } + + constructor( + prop: ReactPropGroup, + setter: Method, + index: Int, + defaultValue: Float, + ) : super(prop, "number", setter, index) { + this.defaultValue = defaultValue + } + + override fun getValueOrDefault(value: Any?, context: Context): Any { + // All numbers from JS are Doubles which can't be simply cast to Float + return if (value == null) defaultValue else (value as Double).toFloat() + } + } + + private class ArrayPropSetter(prop: ReactProp, setter: Method) : + PropSetter(prop, "Array", setter) { + + override fun getValueOrDefault(value: Any?, context: Context): Any? = value as ReadableArray? + } + + private class MapPropSetter(prop: ReactProp, setter: Method) : PropSetter(prop, "Map", setter) { + + override fun getValueOrDefault(value: Any?, context: Context): Any? = value as ReadableMap? + } + + private class StringPropSetter(prop: ReactProp, setter: Method) : + PropSetter(prop, "String", setter) { + + override fun getValueOrDefault(value: Any?, context: Context): Any? = value as String? + } + + private class BoxedBooleanPropSetter(prop: ReactProp, setter: Method) : + PropSetter(prop, "boolean", setter) { + + override fun getValueOrDefault(value: Any?, context: Context): Any? { + if (value != null) { + return if (value as Boolean) java.lang.Boolean.TRUE else java.lang.Boolean.FALSE + } + return null + } + } + + private class BoxedIntPropSetter : PropSetter { + + constructor(prop: ReactProp, setter: Method) : super(prop, "number", setter) + + constructor( + prop: ReactPropGroup, + setter: Method, + index: Int, + ) : super(prop, "number", setter, index) + + override fun getValueOrDefault(value: Any?, context: Context): Any? { + if (value != null) { + return if (value is Double) value.toInt() else value as Int + } + return null + } + } + + private class BoxedColorPropSetter : PropSetter { + + constructor(prop: ReactProp, setter: Method) : super(prop, "mixed", setter) + + constructor( + prop: ReactPropGroup, + setter: Method, + index: Int, + ) : super(prop, "mixed", setter, index) + + override fun getValueOrDefault(value: Any?, context: Context): Any? { + if (value != null) { + return ColorPropConverter.getColor(value, context) + } + return null + } + } + + @JvmStatic + internal fun getNativePropsForView( + viewManagerTopClass: Class>, + shadowNodeTopClass: Class>, + ): Map { + val nativeProps: MutableMap = HashMap() + + val viewManagerProps = getNativePropSettersForViewManagerClass(viewManagerTopClass) + for (setter in viewManagerProps.values) { + nativeProps[setter.propName] = setter.propType + } + + val shadowNodeProps = getNativePropSettersForShadowNodeClass(shadowNodeTopClass) + for (setter in shadowNodeProps.values) { + nativeProps[setter.propName] = setter.propType + } + + return nativeProps + } + + /** + * Returns map from property name to setter instances for all the property setters annotated with + * [ReactProp] in the given [ViewManager] class plus all the setter declared by its parent + * classes. + */ + @JvmStatic + internal fun getNativePropSettersForViewManagerClass( + cls: Class> + ): Map { + if (cls == ViewManager::class.java) { + return EMPTY_PROPS_MAP + } + CLASS_PROPS_CACHE[cls]?.let { + return it + } + // This is to include all the setters from parent classes. Once calculated the result will be + // stored in CLASS_PROPS_CACHE so that we only scan for @ReactProp annotations once per class. + @Suppress("UNCHECKED_CAST") + val props: MutableMap = + HashMap( + getNativePropSettersForViewManagerClass(cls.superclass as Class>) + ) + extractPropSettersFromViewManagerClassDefinition(cls, props) + CLASS_PROPS_CACHE[cls] = props + return props + } + + /** + * Returns map from property name to setter instances for all the property setters annotated with + * [ReactProp] (or [ReactPropGroup] in the given [ReactShadowNode] subclass plus all the setters + * declared by its parent classes up to [ReactShadowNode] which is treated as a base class. + */ + @JvmStatic + internal fun getNativePropSettersForShadowNodeClass( + cls: Class> + ): Map { + for (iface in cls.interfaces) { + if (iface == ReactShadowNode::class.java) { + return EMPTY_PROPS_MAP + } + } + CLASS_PROPS_CACHE[cls]?.let { + return it + } + // Recurse up to (but not past) the ReactShadowNode interface. If we run out of class + // hierarchy (superclass is null), there are no parent setters to inherit. + @Suppress("UNCHECKED_CAST") val superclass = cls.superclass as? Class> + val parentProps = + if (superclass != null) getNativePropSettersForShadowNodeClass(superclass) + else EMPTY_PROPS_MAP + val props: MutableMap = HashMap(parentProps) + extractPropSettersFromShadowNodeClassDefinition(cls, props) + CLASS_PROPS_CACHE[cls] = props + return props + } + + private fun createPropSetter( + annotation: ReactProp, + method: Method, + propTypeClass: Class<*>, + ): PropSetter = + when (propTypeClass) { + Dynamic::class.java -> DynamicPropSetter(annotation, method) + Boolean::class.javaPrimitiveType -> + BooleanPropSetter(annotation, method, annotation.defaultBoolean) + Int::class.javaPrimitiveType -> + if ("Color" == annotation.customType) { + ColorPropSetter(annotation, method, annotation.defaultInt) + } else { + IntPropSetter(annotation, method, annotation.defaultInt) + } + Float::class.javaPrimitiveType -> + FloatPropSetter(annotation, method, annotation.defaultFloat) + Double::class.javaPrimitiveType -> + DoublePropSetter(annotation, method, annotation.defaultDouble) + String::class.java -> StringPropSetter(annotation, method) + java.lang.Boolean::class.java -> BoxedBooleanPropSetter(annotation, method) + java.lang.Integer::class.java -> + if ("Color" == annotation.customType) { + BoxedColorPropSetter(annotation, method) + } else { + BoxedIntPropSetter(annotation, method) + } + ReadableArray::class.java -> ArrayPropSetter(annotation, method) + ReadableMap::class.java -> MapPropSetter(annotation, method) + else -> + throw RuntimeException( + "Unrecognized type: $propTypeClass for method: ${method.declaringClass.name}#${method.name}" + ) + } + + private fun createPropSetters( + annotation: ReactPropGroup, + method: Method, + propTypeClass: Class<*>, + props: MutableMap, + ) { + val names = annotation.names + when (propTypeClass) { + Dynamic::class.java -> + for (i in names.indices) { + props[names[i]] = DynamicPropSetter(annotation, method, i) + } + Int::class.javaPrimitiveType -> + for (i in names.indices) { + props[names[i]] = + if ("Color" == annotation.customType) { + ColorPropSetter(annotation, method, i, annotation.defaultInt) + } else { + IntPropSetter(annotation, method, i, annotation.defaultInt) + } + } + Float::class.javaPrimitiveType -> + for (i in names.indices) { + props[names[i]] = FloatPropSetter(annotation, method, i, annotation.defaultFloat) + } + Double::class.javaPrimitiveType -> + for (i in names.indices) { + props[names[i]] = DoublePropSetter(annotation, method, i, annotation.defaultDouble) + } + java.lang.Integer::class.java -> + for (i in names.indices) { + props[names[i]] = + if ("Color" == annotation.customType) { + BoxedColorPropSetter(annotation, method, i) + } else { + BoxedIntPropSetter(annotation, method, i) + } + } + else -> + throw RuntimeException( + "Unrecognized type: $propTypeClass for method: ${method.declaringClass.name}#${method.name}" + ) + } + } + + private fun extractPropSettersFromViewManagerClassDefinition( + cls: Class>, + props: MutableMap, + ) { + for (method in cls.declaredMethods) { + val annotation = method.getAnnotation(ReactProp::class.java) + if (annotation != null) { + val paramTypes = method.parameterTypes + if (paramTypes.size != 2) { + throw RuntimeException("Wrong number of args for prop setter: ${cls.name}#${method.name}") + } + if (!View::class.java.isAssignableFrom(paramTypes[0])) { + throw RuntimeException( + "First param should be a view subclass to be updated: ${cls.name}#${method.name}" + ) + } + props[annotation.name] = createPropSetter(annotation, method, paramTypes[1]) + } + + val groupAnnotation = method.getAnnotation(ReactPropGroup::class.java) + if (groupAnnotation != null) { + val paramTypes = method.parameterTypes + if (paramTypes.size != 3) { + throw RuntimeException( + "Wrong number of args for group prop setter: ${cls.name}#${method.name}" + ) + } + if (!View::class.java.isAssignableFrom(paramTypes[0])) { + throw RuntimeException( + "First param should be a view subclass to be updated: ${cls.name}#${method.name}" + ) + } + if (paramTypes[1] != Int::class.javaPrimitiveType) { + throw RuntimeException( + "Second argument should be property index: ${cls.name}#${method.name}" + ) + } + createPropSetters(groupAnnotation, method, paramTypes[2], props) + } + } + } + + private fun extractPropSettersFromShadowNodeClassDefinition( + cls: Class>, + props: MutableMap, + ) { + for (method in cls.declaredMethods) { + val annotation = method.getAnnotation(ReactProp::class.java) + if (annotation != null) { + val paramTypes = method.parameterTypes + if (paramTypes.size != 1) { + throw RuntimeException("Wrong number of args for prop setter: ${cls.name}#${method.name}") + } + props[annotation.name] = createPropSetter(annotation, method, paramTypes[0]) + } + + val groupAnnotation = method.getAnnotation(ReactPropGroup::class.java) + if (groupAnnotation != null) { + val paramTypes = method.parameterTypes + if (paramTypes.size != 2) { + throw RuntimeException( + "Wrong number of args for group prop setter: ${cls.name}#${method.name}" + ) + } + if (paramTypes[0] != Int::class.javaPrimitiveType) { + throw RuntimeException( + "Second argument should be property index: ${cls.name}#${method.name}" + ) + } + createPropSetters(groupAnnotation, method, paramTypes[1], props) + } + } + } +}