From 8639725e154abdf9376b0d64d3302fe5e2bea85d Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 28 Oct 2026 02:44:12 -0700 Subject: [PATCH] Skip native prop metadata for delegate-backed managers (#59296) Summary: Add the default-off `skipNativePropsForViewManagerWithDelegateAndroid` feature flag. When enabled, `ViewManager.getNativeProps()` returns an empty map for managers with a non-null, non-generic delegate, avoiding unused manager and shadow-node prop metadata generation. Return a fresh mutable map so subclasses can safely extend `super.getNativeProps()`. The constants helper already omits `NativeProps` for an empty map; commands, events and custom constants are preserved. Null and generic delegates keep the existing metadata path, including its legacy-architecture shadow-node handling. The flag must only be enabled when delegate-backed components use static view configs rather than runtime native prop metadata. NativeProps queries reuse `getOrCreateViewManagerDelegate()`, sharing the same delegate with prop updates and commands. Make its lazy initialisation thread-safe with a volatile cache and double-checked synchronisation, leaving warm reads lock-free. Document that initialisation may occur on the JavaScript thread when this flag is enabled. Regenerate all feature-flag bindings and cover enabled/disabled paths, generic and null delegates, marker-only managers, preserved constants, subclass prop extensions, independent mutable maps, existing-cache reuse and concurrent metadata/command initialisation. Changelog: [Android][Changed] - Add an opt-in optimisation to skip native prop metadata for delegate-backed view managers using static view configs. Reviewed By: andrewdacenko Differential Revision: D126630163 --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../facebook/react/uimanager/ViewManager.java | 31 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../UIManagerModuleConstantsHelperTest.kt | 305 ++++++++++++++++++ .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 54 ++-- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 11 + .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 22 files changed, 485 insertions(+), 42 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 7fcdaaeec16d..7492f00b757a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<251ba53185bdf10220816b4c239ef6d5>> */ /** @@ -510,6 +510,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun skipActivityIdentityAssertionOnHostPause(): Boolean = accessor.skipActivityIdentityAssertionOnHostPause() + /** + * Skip native prop metadata generation on Android for view managers with custom delegates. Requires their components to use static view configs. + */ + @JvmStatic + public fun skipNativePropsForViewManagerWithDelegateAndroid(): Boolean = accessor.skipNativePropsForViewManagerWithDelegateAndroid() + /** * When a node changes, the differ leaves out its children that are unchanged in both trees instead of describing and comparing them. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 6e0da4d9d0e4..63f42e41e157 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<1bb0fee5a17f70b91f33850d97479cc1>> */ /** @@ -100,6 +100,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null + private var skipNativePropsForViewManagerWithDelegateAndroidCache: Boolean? = null private var skipUnchangedChildrenInDifferentiatorCache: Boolean? = null private var syncAndroidClipBoundsWithOverflowCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null @@ -838,6 +839,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun skipNativePropsForViewManagerWithDelegateAndroid(): Boolean { + var cached = skipNativePropsForViewManagerWithDelegateAndroidCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.skipNativePropsForViewManagerWithDelegateAndroid() + skipNativePropsForViewManagerWithDelegateAndroidCache = cached + } + return cached + } + override fun skipUnchangedChildrenInDifferentiator(): Boolean { var cached = skipUnchangedChildrenInDifferentiatorCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 2268d03f3674..7044e217e01f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6d86d4adbc261c9271b5bed72bd12dcf>> + * @generated SignedSource<<8686d1f71576023f0cd22aca37b2e8ba>> */ /** @@ -188,6 +188,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun skipActivityIdentityAssertionOnHostPause(): Boolean + @DoNotStrip @JvmStatic public external fun skipNativePropsForViewManagerWithDelegateAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun skipUnchangedChildrenInDifferentiator(): Boolean @DoNotStrip @JvmStatic public external fun syncAndroidClipBoundsWithOverflow(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index defafe46f6b4..42f5b96ff44e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<09945e5bc899957424ce20441022830c>> */ /** @@ -183,6 +183,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun skipActivityIdentityAssertionOnHostPause(): Boolean = false + override fun skipNativePropsForViewManagerWithDelegateAndroid(): Boolean = false + override fun skipUnchangedChildrenInDifferentiator(): Boolean = false override fun syncAndroidClipBoundsWithOverflow(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 53af4b4edab0..da71e2850328 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<12dd3ead4fa496af154f770cabc252f1>> */ /** @@ -104,6 +104,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null + private var skipNativePropsForViewManagerWithDelegateAndroidCache: Boolean? = null private var skipUnchangedChildrenInDifferentiatorCache: Boolean? = null private var syncAndroidClipBoundsWithOverflowCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null @@ -922,6 +923,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun skipNativePropsForViewManagerWithDelegateAndroid(): Boolean { + var cached = skipNativePropsForViewManagerWithDelegateAndroidCache + if (cached == null) { + cached = currentProvider.skipNativePropsForViewManagerWithDelegateAndroid() + accessedFeatureFlags.add("skipNativePropsForViewManagerWithDelegateAndroid") + skipNativePropsForViewManagerWithDelegateAndroidCache = cached + } + return cached + } + override fun skipUnchangedChildrenInDifferentiator(): Boolean { var cached = skipUnchangedChildrenInDifferentiatorCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 8fa2f4269464..729221df1b8d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<8227961bd327cc2851437304b21dee6a>> */ /** @@ -183,6 +183,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun skipActivityIdentityAssertionOnHostPause(): Boolean + @DoNotStrip public fun skipNativePropsForViewManagerWithDelegateAndroid(): Boolean + @DoNotStrip public fun skipUnchangedChildrenInDifferentiator(): Boolean @DoNotStrip public fun syncAndroidClipBoundsWithOverflow(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java index 613e0c785fa3..9db0f27e14e4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java @@ -46,7 +46,7 @@ public abstract class ViewManager> private static final String TAG = "ViewManager"; - private @Nullable ViewManagerDelegate mDelegate = null; + private volatile @Nullable ViewManagerDelegate mDelegate = null; /** * For View recycling: we store a Stack of unused, dead Views. This is null by default, and when @@ -113,6 +113,11 @@ public void updateProperties(@NonNull T viewToUpdate, ReactStylesDiffMap props) *

By default, this methods returns a generic {@link ViewManagerDelegate} which uses {@link * ViewManagerSetter} to apply property updates. * + *

When {@code skipNativePropsForViewManagerWithDelegateAndroid} is enabled, lazy delegate + * initialisation may also occur during native prop metadata generation on the JavaScript thread. + * The returned delegate is cached and shared with subsequent prop updates and commands. Overrides + * must be safe to initialise on either thread. + * * @return an instance of {@link ViewManagerDelegate} if the props of the view managed by this * view manager should be set via this delegate */ @@ -131,8 +136,13 @@ protected ViewManagerDelegate getDelegate() { private ViewManagerDelegate getOrCreateViewManagerDelegate() { ViewManagerDelegate delegate = mDelegate; if (delegate == null) { - delegate = getDelegate(); - mDelegate = delegate; + synchronized (this) { + delegate = mDelegate; + if (delegate == null) { + delegate = getDelegate(); + mDelegate = delegate; + } + } } return delegate; } @@ -402,10 +412,21 @@ public void receiveCommand(@NonNull T view, String commandId, ReadableArray args } /** - * Returns a {@link Map} representing the native props of the view manager. The - * Map contains the names (key) and types (value) of the ViewManager's props. + * Returns a mutable {@link Map} representing the native props of the view + * manager. The Map contains the names (key) and types (value) of the ViewManager's props. + * + *

When {@code skipNativePropsForViewManagerWithDelegateAndroid} is enabled, managers with a + * non-generic delegate return an empty map. Enable this only when those components use static + * view configs rather than deriving their valid attributes from native prop metadata. */ public Map getNativeProps() { + if (ReactNativeFeatureFlags.skipNativePropsForViewManagerWithDelegateAndroid()) { + ViewManagerDelegate delegate = getOrCreateViewManagerDelegate(); + if (delegate != null + && !(delegate instanceof ViewManagerPropertyUpdater.GenericViewManagerDelegate)) { + return new HashMap<>(); + } + } if (ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE && ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) { // TODO: review if we need to check fabricInterop here diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 23fd0978a766..5be7408a9ecc 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<944781e7a547e601f0637869a3a3ec5e>> + * @generated SignedSource<<223e38a173e3762fa0034ed47f96a84d>> */ /** @@ -519,6 +519,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool skipNativePropsForViewManagerWithDelegateAndroid() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("skipNativePropsForViewManagerWithDelegateAndroid"); + return method(javaProvider_); + } + bool skipUnchangedChildrenInDifferentiator() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("skipUnchangedChildrenInDifferentiator"); @@ -1025,6 +1031,11 @@ bool JReactNativeFeatureFlagsCxxInterop::skipActivityIdentityAssertionOnHostPaus return ReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause(); } +bool JReactNativeFeatureFlagsCxxInterop::skipNativePropsForViewManagerWithDelegateAndroid( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::skipNativePropsForViewManagerWithDelegateAndroid(); +} + bool JReactNativeFeatureFlagsCxxInterop::skipUnchangedChildrenInDifferentiator( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator(); @@ -1381,6 +1392,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "skipActivityIdentityAssertionOnHostPause", JReactNativeFeatureFlagsCxxInterop::skipActivityIdentityAssertionOnHostPause), + makeNativeMethod( + "skipNativePropsForViewManagerWithDelegateAndroid", + JReactNativeFeatureFlagsCxxInterop::skipNativePropsForViewManagerWithDelegateAndroid), makeNativeMethod( "skipUnchangedChildrenInDifferentiator", JReactNativeFeatureFlagsCxxInterop::skipUnchangedChildrenInDifferentiator), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index ae774f80c82b..a1f6044f7bcc 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -270,6 +270,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool skipActivityIdentityAssertionOnHostPause( facebook::jni::alias_ref); + static bool skipNativePropsForViewManagerWithDelegateAndroid( + facebook::jni::alias_ref); + static bool skipUnchangedChildrenInDifferentiator( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.kt index 8c313263cf55..8642ff2931aa 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.kt @@ -7,11 +7,30 @@ package com.facebook.react.uimanager +import android.annotation.SuppressLint +import android.view.View +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.views.view.ReactViewManager +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import java.util.concurrent.atomic.AtomicInteger import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.After import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class UIManagerModuleConstantsHelperTest { @Before @@ -19,6 +38,292 @@ class UIManagerModuleConstantsHelperTest { ReactNativeFeatureFlagsForTests.setUp() } + @After + fun tearDown() { + ViewManagerPropertyUpdater.clear() + ReactNativeFeatureFlags.dangerouslyReset() + } + + @Test + fun nativeProps_withCoreGenericDelegateAndFlagDisabled_areGenerated() { + assertThat(ReactViewManager().nativeProps).containsEntry("opacity", "number") + } + + @Test + fun nativeProps_withCoreGenericDelegateAndFlagEnabled_areGenerated() { + enableSkippingNativeProps() + + assertThat(ReactViewManager().nativeProps).containsEntry("opacity", "number") + } + + @Test + fun nativeProps_withDelegateAndFlagDisabled_areGenerated() { + val viewManager = DelegatedTestViewManager() + + assertThat(viewManager.nativeProps).containsEntry("testProp", "boolean") + assertThat(viewManager.delegateRequests).isZero() + } + + @Test + fun nativeProps_withDelegateAndFlagEnabled_areSkipped() { + enableSkippingNativeProps() + val viewManager = DelegatedTestViewManager() + + assertThat(viewManager.nativeProps).isEmpty() + assertThat(viewManager.shadowNodeClassRequests).isZero() + } + + @Test + fun nativeProps_withDelegateAndFlagEnabled_allowsSubclassToAddProps() { + enableSkippingNativeProps() + val viewManager = + object : DelegatedTestViewManager() { + override fun getNativeProps(): MutableMap = + super.getNativeProps().apply { put("extraProp", "String") } + } + + val constants = + UIManagerModuleConstantsHelper.createConstantsForViewManager( + viewManager, + null, + null, + null, + null, + ) + + assertThat(constants).containsEntry("NativeProps", mapOf("extraProp" to "String")) + assertThat(viewManager.shadowNodeClassRequests).isZero() + } + + @Test + fun nativeProps_withDelegateAndFlagEnabled_returnsIndependentMutableMaps() { + enableSkippingNativeProps() + val viewManager = DelegatedTestViewManager() + + val first = viewManager.nativeProps + first["extraProp"] = "String" + val second = viewManager.nativeProps + + assertThat(first).containsEntry("extraProp", "String") + assertThat(second).isEmpty() + assertThat(second === first).isFalse() + } + + @Test + fun nativeProps_withGenericDelegateAndFlagEnabled_areGenerated() { + enableSkippingNativeProps() + val viewManager = TestViewManager() + + assertThat(viewManager.nativeProps).containsEntry("testProp", "boolean") + } + + @Test + fun nativeProps_withGeneratedInterfaceButNoCustomDelegate_areGenerated() { + enableSkippingNativeProps() + val viewManager = MarkerOnlyTestViewManager() + + assertThat(viewManager.nativeProps).containsEntry("testProp", "boolean") + } + + @Test + fun nativeProps_withNullDelegateAndFlagEnabled_areGenerated() { + enableSkippingNativeProps() + val viewManager = + object : TestViewManager() { + override fun getDelegate(): ViewManagerDelegate? = null + } + + assertThat(viewManager.nativeProps).containsEntry("testProp", "boolean") + } + + @Test + fun nativeProps_withDelegate_reusesThePropUpdateDelegateCache() { + enableSkippingNativeProps() + val viewManager = DelegatedTestViewManager() + + assertThat(viewManager.nativeProps).isEmpty() + assertThat(viewManager.nativeProps).isEmpty() + assertThat(viewManager.delegateRequests).isEqualTo(1) + + val view: View = mock() + val args: ReadableArray = mock() + viewManager.receiveCommand(view, "testCommand", args) + viewManager.receiveCommand(view, "testCommand", args) + + assertThat(viewManager.delegateRequests).isEqualTo(1) + assertThat(viewManager.testDelegate.commands) + .containsExactly(Triple(view, "testCommand", args), Triple(view, "testCommand", args)) + } + + @Test + fun nativeProps_withPreviouslyCachedDelegate_doesNotRequestAnotherDelegate() { + enableSkippingNativeProps() + val viewManager = DelegatedTestViewManager() + val view: View = mock() + val args: ReadableArray = mock() + + viewManager.receiveCommand(view, "testCommand", args) + assertThat(viewManager.nativeProps).isEmpty() + + assertThat(viewManager.delegateRequests).isEqualTo(1) + assertThat(viewManager.testDelegate.commands).containsExactly(Triple(view, "testCommand", args)) + } + + // This executor belongs only to this test and is never shared. + @SuppressLint("BadMethodUse-java.util.concurrent.ExecutorService.shutdownNow") + @Test + fun nativeProps_andCommands_initializeTheDelegateOnceAcrossThreads() { + enableSkippingNativeProps() + val delegate = RecordingDelegate() + val delegateRequests = AtomicInteger() + val initializing = CountDownLatch(1) + val releaseInitialization = CountDownLatch(1) + val viewManager = + object : TestViewManager() { + override fun getDelegate(): ViewManagerDelegate { + delegateRequests.incrementAndGet() + initializing.countDown() + check(releaseInitialization.await(5, TimeUnit.SECONDS)) + return delegate + } + } + val view: View = mock() + val args: ReadableArray = mock() + val ready = CountDownLatch(8) + val start = CountDownLatch(1) + val executor = Executors.newFixedThreadPool(8) + + try { + val futures = + (0 until 8).map { index -> + executor.submit { + ready.countDown() + check(start.await(5, TimeUnit.SECONDS)) + if (index % 2 == 0) { + assertThat(viewManager.nativeProps).isEmpty() + } else { + viewManager.receiveCommand(view, "testCommand", args) + } + } + } + assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue() + start.countDown() + assertThat(initializing.await(5, TimeUnit.SECONDS)).isTrue() + assertThatThrownBy { futures.first().get(100, TimeUnit.MILLISECONDS) } + .isInstanceOf(TimeoutException::class.java) + assertThat(delegateRequests.get()).isEqualTo(1) + releaseInitialization.countDown() + futures.forEach { it.get(5, TimeUnit.SECONDS) } + + assertThat(delegateRequests.get()).isEqualTo(1) + assertThat(delegate.commands) + .containsExactly( + Triple(view, "testCommand", args), + Triple(view, "testCommand", args), + Triple(view, "testCommand", args), + Triple(view, "testCommand", args), + ) + } finally { + start.countDown() + releaseInitialization.countDown() + executor.shutdownNow() + } + } + + @Test + fun constants_withDelegateAndFlagEnabled_preserveOtherMetadata() { + enableSkippingNativeProps() + val viewManager = DelegatedTestViewManager() + + val constants = + UIManagerModuleConstantsHelper.createConstantsForViewManager( + viewManager, + null, + null, + null, + null, + ) + + assertThat(constants) + .doesNotContainKey("NativeProps") + .containsEntry("Constants", mapOf("testConstant" to true)) + .containsEntry("Commands", mapOf("testCommand" to 1)) + .containsEntry( + "directEventTypes", + mapOf("topTest" to mapOf("registrationName" to "onTest")), + ) + assertThat(viewManager.shadowNodeClassRequests).isZero() + } + + private fun enableSkippingNativeProps() { + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun skipNativePropsForViewManagerWithDelegateAndroid(): Boolean = true + }, + ) + } + + @Suppress("DEPRECATION") + private open class TestViewManager : ViewManager() { + var shadowNodeClassRequests: Int = 0 + private set + + override fun getName(): String = "TestView" + + override fun getShadowNodeClass(): Class { + shadowNodeClassRequests++ + return LayoutShadowNode::class.java + } + + override fun createViewInstance(context: ThemedReactContext): View = + error("This test does not create views") + + override fun updateExtraData(root: View, extraData: Any?) = Unit + + override fun prepareToRecycleView(reactContext: ThemedReactContext, view: View): View? = null + + override fun getExportedViewConstants(): MutableMap = + mutableMapOf("testConstant" to true) + + override fun getCommandsMap(): MutableMap = mutableMapOf("testCommand" to 1) + + override fun getExportedCustomDirectEventTypeConstants(): MutableMap = + mutableMapOf("topTest" to mapOf("registrationName" to "onTest")) + + @ReactProp(name = "testProp") + @Suppress("UNUSED_PARAMETER") + fun setTestProp(view: View, value: Boolean) = Unit + } + + private class MarkerOnlyTestViewManager : TestViewManager(), ViewManagerWithGeneratedInterface + + private open class DelegatedTestViewManager : TestViewManager() { + val testDelegate = RecordingDelegate() + var delegateRequests: Int = 0 + private set + + override fun getDelegate(): ViewManagerDelegate { + delegateRequests++ + return testDelegate + } + } + + private class RecordingDelegate : ViewManagerDelegate { + val commands = ConcurrentLinkedQueue>() + + @Suppress("ACCIDENTAL_OVERRIDE", "INAPPLICABLE_JVM_NAME") + @JvmName("kotlinCompat\$setProperty") + @JvmSynthetic + override fun setProperty(view: T, propName: String, value: Any?) = Unit + + @Suppress("ACCIDENTAL_OVERRIDE", "INAPPLICABLE_JVM_NAME") + @JvmName("kotlinCompat\$receiveCommand") + @JvmSynthetic + override fun receiveCommand(view: T, commandName: String, args: ReadableArray) { + commands.add(Triple(view, commandName, args)) + } + } + @Test fun normalizeEventTypes_withEmptyMap_doesNothing() { val emptyMap = mutableMapOf() diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index ac8ddb96a112..c258b24c45fb 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<56e127471f63e47798ec619e6fa56738>> + * @generated SignedSource<> */ /** @@ -346,6 +346,10 @@ bool ReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause() { return getAccessor().skipActivityIdentityAssertionOnHostPause(); } +bool ReactNativeFeatureFlags::skipNativePropsForViewManagerWithDelegateAndroid() { + return getAccessor().skipNativePropsForViewManagerWithDelegateAndroid(); +} + bool ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator() { return getAccessor().skipUnchangedChildrenInDifferentiator(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 0e5b3f2013fb..d1515d8a1862 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6887b671526bc8a3b0678e3e45b4e3b4>> + * @generated SignedSource<> */ /** @@ -441,6 +441,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool skipActivityIdentityAssertionOnHostPause(); + /** + * Skip native prop metadata generation on Android for view managers with custom delegates. Requires their components to use static view configs. + */ + RN_EXPORT static bool skipNativePropsForViewManagerWithDelegateAndroid(); + /** * When a node changes, the differ leaves out its children that are unchanged in both trees instead of describing and comparing them. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7640cea6418a..616bd7eff1ec 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5d9712bd7e62ba3a29b084ba4c60bec5>> + * @generated SignedSource<<0c181c95d3be402ddcad90e1e32939e7>> */ /** @@ -1469,6 +1469,24 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::skipNativePropsForViewManagerWithDelegateAndroid() { + auto flagValue = skipNativePropsForViewManagerWithDelegateAndroid_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(80, "skipNativePropsForViewManagerWithDelegateAndroid"); + + flagValue = currentProvider_->skipNativePropsForViewManagerWithDelegateAndroid(); + skipNativePropsForViewManagerWithDelegateAndroid_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::skipUnchangedChildrenInDifferentiator() { auto flagValue = skipUnchangedChildrenInDifferentiator_.load(); @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::skipUnchangedChildrenInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "skipUnchangedChildrenInDifferentiator"); + markFlagAsAccessed(81, "skipUnchangedChildrenInDifferentiator"); flagValue = currentProvider_->skipUnchangedChildrenInDifferentiator(); skipUnchangedChildrenInDifferentiator_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(82, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(83, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(85, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(86, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useFabricInterop"); + markFlagAsAccessed(87, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(88, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "useNestedScrollViewAndroid"); + markFlagAsAccessed(89, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "useSharedAnimatedBackend"); + markFlagAsAccessed(90, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1658,7 +1676,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(91, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "useTurboModuleInterop"); + markFlagAsAccessed(92, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1694,7 +1712,7 @@ bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "useWindowDensityForDisplayMetricsAndroid"); + markFlagAsAccessed(93, "useWindowDensityForDisplayMetricsAndroid"); flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); useWindowDensityForDisplayMetricsAndroid_ = flagValue; @@ -1712,7 +1730,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(93, "viewCullingOutsetRatio"); + markFlagAsAccessed(94, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1730,7 +1748,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(94, "viewTransitionEnabled"); + markFlagAsAccessed(95, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1748,7 +1766,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(95, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(96, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1766,7 +1784,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(96, "virtualViewPrerenderRatio"); + markFlagAsAccessed(97, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 7a9bc59bc57e..a9fc9c55dd01 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<17f41335dce7e5b45e7af2878c47d69b>> + * @generated SignedSource<> */ /** @@ -114,6 +114,7 @@ class ReactNativeFeatureFlagsAccessor { bool shouldPressibilityUseW3CPointerEventsForHover(); bool shouldTriggerResponderTransferOnScrollAndroid(); bool skipActivityIdentityAssertionOnHostPause(); + bool skipNativePropsForViewManagerWithDelegateAndroid(); bool skipUnchangedChildrenInDifferentiator(); bool syncAndroidClipBoundsWithOverflow(); bool traceTurboModulePromiseRejectionsOnAndroid(); @@ -142,7 +143,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 97> accessedFeatureFlags_; + std::array, 98> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -224,6 +225,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> shouldPressibilityUseW3CPointerEventsForHover_; std::atomic> shouldTriggerResponderTransferOnScrollAndroid_; std::atomic> skipActivityIdentityAssertionOnHostPause_; + std::atomic> skipNativePropsForViewManagerWithDelegateAndroid_; std::atomic> skipUnchangedChildrenInDifferentiator_; std::atomic> syncAndroidClipBoundsWithOverflow_; std::atomic> traceTurboModulePromiseRejectionsOnAndroid_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 600aab1882cc..7a6bf09bd772 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<35e8175277baa31d135d802ef5a5e6f1>> + * @generated SignedSource<> */ /** @@ -349,6 +349,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool skipNativePropsForViewManagerWithDelegateAndroid() override { + return false; + } + bool skipUnchangedChildrenInDifferentiator() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 88878807cfec..86ff46974e90 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<04346fee018c72aa1fa6449391255036>> */ /** @@ -767,6 +767,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::skipActivityIdentityAssertionOnHostPause(); } + bool skipNativePropsForViewManagerWithDelegateAndroid() override { + auto value = values_["skipNativePropsForViewManagerWithDelegateAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::skipNativePropsForViewManagerWithDelegateAndroid(); + } + bool skipUnchangedChildrenInDifferentiator() override { auto value = values_["skipUnchangedChildrenInDifferentiator"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 664922287009..e9d6a3de9af9 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7413e958441d0502dc1266ff87e9da85>> + * @generated SignedSource<> */ /** @@ -107,6 +107,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool shouldPressibilityUseW3CPointerEventsForHover() = 0; virtual bool shouldTriggerResponderTransferOnScrollAndroid() = 0; virtual bool skipActivityIdentityAssertionOnHostPause() = 0; + virtual bool skipNativePropsForViewManagerWithDelegateAndroid() = 0; virtual bool skipUnchangedChildrenInDifferentiator() = 0; virtual bool syncAndroidClipBoundsWithOverflow() = 0; virtual bool traceTurboModulePromiseRejectionsOnAndroid() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index ca8a80f1adbf..9b37569fb917 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<02977c7bec909966aad7c18292558380>> */ /** @@ -444,6 +444,11 @@ bool NativeReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause( return ReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause(); } +bool NativeReactNativeFeatureFlags::skipNativePropsForViewManagerWithDelegateAndroid( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::skipNativePropsForViewManagerWithDelegateAndroid(); +} + bool NativeReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7413766e285a..4d90a2ad1986 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<90f271ea47f91e6a84979209b7b3d816>> + * @generated SignedSource<<992231c2daae834180564ff58766c9cd>> */ /** @@ -198,6 +198,8 @@ class NativeReactNativeFeatureFlags bool skipActivityIdentityAssertionOnHostPause(jsi::Runtime& runtime); + bool skipNativePropsForViewManagerWithDelegateAndroid(jsi::Runtime& runtime); + bool skipUnchangedChildrenInDifferentiator(jsi::Runtime& runtime); bool syncAndroidClipBoundsWithOverflow(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 612c196ff22d..6bbbeae7d4df 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -911,6 +911,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + skipNativePropsForViewManagerWithDelegateAndroid: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-21', + description: + 'Skip native prop metadata generation on Android for view managers with custom delegates. Requires their components to use static view configs.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, skipUnchangedChildrenInDifferentiator: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index e5a8120cff62..826fec11d958 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<674f975884ae746e067e614523f2bfc3>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -129,6 +129,7 @@ export type ReactNativeFeatureFlags = Readonly<{ shouldPressibilityUseW3CPointerEventsForHover: Getter, shouldTriggerResponderTransferOnScrollAndroid: Getter, skipActivityIdentityAssertionOnHostPause: Getter, + skipNativePropsForViewManagerWithDelegateAndroid: Getter, skipUnchangedChildrenInDifferentiator: Getter, syncAndroidClipBoundsWithOverflow: Getter, traceTurboModulePromiseRejectionsOnAndroid: Getter, @@ -542,6 +543,10 @@ export const shouldTriggerResponderTransferOnScrollAndroid: Getter = cr * Skip activity identity assertion in ReactHostImpl::onHostPause() */ export const skipActivityIdentityAssertionOnHostPause: Getter = createNativeFlagGetter('skipActivityIdentityAssertionOnHostPause', false); +/** + * Skip native prop metadata generation on Android for view managers with custom delegates. Requires their components to use static view configs. + */ +export const skipNativePropsForViewManagerWithDelegateAndroid: Getter = createNativeFlagGetter('skipNativePropsForViewManagerWithDelegateAndroid', false); /** * When a node changes, the differ leaves out its children that are unchanged in both trees instead of describing and comparing them. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 6fc752bba2a9..30b43da6a096 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2eb3f4a3dca013c51ad2913944855010>> + * @generated SignedSource<<9a8a20ffc77a1f9c99a007c5d931d68f>> * @flow strict * @noformat */ @@ -105,6 +105,7 @@ export interface Spec extends TurboModule { readonly shouldPressibilityUseW3CPointerEventsForHover?: () => boolean; readonly shouldTriggerResponderTransferOnScrollAndroid?: () => boolean; readonly skipActivityIdentityAssertionOnHostPause?: () => boolean; + readonly skipNativePropsForViewManagerWithDelegateAndroid?: () => boolean; readonly skipUnchangedChildrenInDifferentiator?: () => boolean; readonly syncAndroidClipBoundsWithOverflow?: () => boolean; readonly traceTurboModulePromiseRejectionsOnAndroid?: () => boolean;