Skip to content

Commit ccc50dd

Browse files
cortinicofacebook-github-bot
authored andcommitted
Fabric Interop - Properly dispatch integer commands (#38527)
Summary: Pull Request resolved: #38527 This fixes a bug that got reported for the Fabric Interop for Android with Command dispatching. (See terrylinla/react-native-sketch-canvas#236) The problem is that libraries that were receiving commands as ints with: ``` public void receiveCommand( int surfaceId, int reactTag, int commandId, Nullable ReadableArray commandArgs) { ``` would not receive command with the Fabric Interop for Android. The problem is that with Fabric, events are dispatched as string always. cipolleschi took care of this for iOS, but we realized that the Android part was missing. I'm adding it here. The logic is, if the event is dispatched as a string that represents a number (say `"42"`) and the user has Fabric Interop enabled, then we dispatch the event as `int` (so libraries will keep on working). Changelog: [Android] [Fixed] - Fabric Interop - Properly dispatch integer commands Reviewed By: cipolleschi Differential Revision: D47600094 fbshipit-source-id: c35f0509e6c6c0cddc7090a069882f92dd95532e
1 parent 8f4542d commit ccc50dd

File tree

4 files changed

+127
-14
lines changed

4 files changed

+127
-14
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
import com.facebook.react.common.build.ReactBuildConfig;
5656
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
5757
import com.facebook.react.config.ReactFeatureFlags;
58-
import com.facebook.react.fabric.events.EventBeatManager;
5958
import com.facebook.react.fabric.events.EventEmitterWrapper;
6059
import com.facebook.react.fabric.events.FabricEventEmitter;
6160
import com.facebook.react.fabric.interfaces.SurfaceHandler;
@@ -65,6 +64,7 @@
6564
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
6665
import com.facebook.react.fabric.mounting.SurfaceMountingManager.ViewEvent;
6766
import com.facebook.react.fabric.mounting.mountitems.BatchMountItem;
67+
import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem;
6868
import com.facebook.react.fabric.mounting.mountitems.MountItem;
6969
import com.facebook.react.fabric.mounting.mountitems.MountItemFactory;
7070
import com.facebook.react.modules.core.ReactChoreographer;
@@ -79,6 +79,7 @@
7979
import com.facebook.react.uimanager.UIManagerHelper;
8080
import com.facebook.react.uimanager.ViewManagerPropertyUpdater;
8181
import com.facebook.react.uimanager.ViewManagerRegistry;
82+
import com.facebook.react.uimanager.events.BatchEventDispatchedListener;
8283
import com.facebook.react.uimanager.events.EventCategoryDef;
8384
import com.facebook.react.uimanager.events.EventDispatcher;
8485
import com.facebook.react.uimanager.events.EventDispatcherImpl;
@@ -168,7 +169,7 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
168169
@NonNull private final MountItemDispatcher mMountItemDispatcher;
169170
@NonNull private final ViewManagerRegistry mViewManagerRegistry;
170171

171-
@NonNull private final EventBeatManager mEventBeatManager;
172+
@NonNull private final BatchEventDispatchedListener mBatchEventDispatchedListener;
172173

173174
@NonNull
174175
private final CopyOnWriteArrayList<UIManagerListener> mListeners = new CopyOnWriteArrayList<>();
@@ -213,14 +214,14 @@ public void executeItems(Queue<MountItem> items) {
213214
public FabricUIManager(
214215
@NonNull ReactApplicationContext reactContext,
215216
@NonNull ViewManagerRegistry viewManagerRegistry,
216-
@NonNull EventBeatManager eventBeatManager) {
217+
@NonNull BatchEventDispatchedListener batchEventDispatchedListener) {
217218
mDispatchUIFrameCallback = new DispatchUIFrameCallback(reactContext);
218219
mReactApplicationContext = reactContext;
219220
mMountingManager = new MountingManager(viewManagerRegistry, mMountItemExecutor);
220221
mMountItemDispatcher =
221222
new MountItemDispatcher(mMountingManager, new MountItemDispatchListener());
222223
mEventDispatcher = new EventDispatcherImpl(reactContext);
223-
mEventBeatManager = eventBeatManager;
224+
mBatchEventDispatchedListener = batchEventDispatchedListener;
224225
mReactApplicationContext.addLifecycleEventListener(this);
225226

226227
mViewManagerRegistry = viewManagerRegistry;
@@ -388,7 +389,7 @@ public void stopSurface(final int surfaceID) {
388389
@Override
389390
public void initialize() {
390391
mEventDispatcher.registerEventEmitter(FABRIC, new FabricEventEmitter(this));
391-
mEventDispatcher.addBatchEventDispatchedListener(mEventBeatManager);
392+
mEventDispatcher.addBatchEventDispatchedListener(mBatchEventDispatchedListener);
392393
if (ENABLE_FABRIC_PERF_LOGS) {
393394
mDevToolsReactPerfLogger = new DevToolsReactPerfLogger();
394395
mDevToolsReactPerfLogger.addDevToolsReactPerfLoggerListener(FABRIC_PERF_LOGGER);
@@ -427,7 +428,7 @@ public void onCatalystInstanceDestroy() {
427428
// memory immediately.
428429
mDispatchUIFrameCallback.stop();
429430

430-
mEventDispatcher.removeBatchEventDispatchedListener(mEventBeatManager);
431+
mEventDispatcher.removeBatchEventDispatchedListener(mBatchEventDispatchedListener);
431432
mEventDispatcher.unregisterEventEmitter(FABRIC);
432433

433434
mReactApplicationContext.unregisterComponentCallbacks(mViewManagerRegistry);
@@ -1047,9 +1048,17 @@ public void dispatchCommand(
10471048
final int reactTag,
10481049
final String commandId,
10491050
@Nullable final ReadableArray commandArgs) {
1050-
mMountItemDispatcher.dispatchCommandMountItem(
1051-
MountItemFactory.createDispatchCommandMountItem(
1052-
surfaceId, reactTag, commandId, commandArgs));
1051+
if (ReactFeatureFlags.unstable_useFabricInterop) {
1052+
// For Fabric Interop, we check if the commandId is an integer. If it is, we use the integer
1053+
// overload of dispatchCommand. Otherwise, we use the string overload.
1054+
// and the events won't be correctly dispatched.
1055+
mMountItemDispatcher.dispatchCommandMountItem(
1056+
createDispatchCommandMountItemForInterop(surfaceId, reactTag, commandId, commandArgs));
1057+
} else {
1058+
mMountItemDispatcher.dispatchCommandMountItem(
1059+
MountItemFactory.createDispatchCommandMountItem(
1060+
surfaceId, reactTag, commandId, commandArgs));
1061+
}
10531062
}
10541063

10551064
@Override
@@ -1246,6 +1255,26 @@ public void didDispatchMountItems() {
12461255
}
12471256
}
12481257

1258+
/**
1259+
* Util function that takes care of handling commands for Fabric Interop. If the command is a
1260+
* string that represents a number (say "42"), it will be parsed as an integer and the
1261+
* corresponding dispatch command mount item will be created.
1262+
*/
1263+
/* package */ DispatchCommandMountItem createDispatchCommandMountItemForInterop(
1264+
final int surfaceId,
1265+
final int reactTag,
1266+
final String commandId,
1267+
@Nullable final ReadableArray commandArgs) {
1268+
try {
1269+
int commandIdInteger = Integer.parseInt(commandId);
1270+
return MountItemFactory.createDispatchCommandMountItem(
1271+
surfaceId, reactTag, commandIdInteger, commandArgs);
1272+
} catch (NumberFormatException e) {
1273+
return MountItemFactory.createDispatchCommandMountItem(
1274+
surfaceId, reactTag, commandId, commandArgs);
1275+
}
1276+
}
1277+
12491278
private class DispatchUIFrameCallback extends GuardedFrameCallback {
12501279

12511280
private volatile boolean mIsMountingEnabled = true;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.fabric
9+
10+
import com.facebook.react.bridge.ReactApplicationContext
11+
import com.facebook.react.uimanager.ViewManagerRegistry
12+
import com.facebook.react.uimanager.events.BatchEventDispatchedListener
13+
import com.facebook.testutils.fakes.FakeBatchEventDispatchedListener
14+
import com.facebook.testutils.shadows.ShadowSoLoader
15+
import org.junit.Assert.assertEquals
16+
import org.junit.Before
17+
import org.junit.Test
18+
import org.junit.runner.RunWith
19+
import org.robolectric.RobolectricTestRunner
20+
import org.robolectric.RuntimeEnvironment
21+
import org.robolectric.annotation.Config
22+
23+
@RunWith(RobolectricTestRunner::class)
24+
@Config(shadows = [ShadowSoLoader::class])
25+
class FabricUIManagerTest {
26+
27+
private lateinit var reactContext: ReactApplicationContext
28+
private lateinit var viewManagerRegistry: ViewManagerRegistry
29+
private lateinit var batchEventDispatchedListener: BatchEventDispatchedListener
30+
private lateinit var underTest: FabricUIManager
31+
32+
@Before
33+
fun setup() {
34+
reactContext = ReactApplicationContext(RuntimeEnvironment.getApplication())
35+
viewManagerRegistry = ViewManagerRegistry(emptyList())
36+
batchEventDispatchedListener = FakeBatchEventDispatchedListener()
37+
underTest = FabricUIManager(reactContext, viewManagerRegistry, batchEventDispatchedListener)
38+
}
39+
40+
@Test
41+
fun createDispatchCommandMountItemForInterop_withValidString_returnsStringEvent() {
42+
val command = underTest.createDispatchCommandMountItemForInterop(11, 1, "anEvent", null)
43+
44+
// DispatchStringCommandMountItem is package private so we can `as` check it.
45+
val className = command::class.java.name.substringAfterLast(".")
46+
assertEquals("DispatchStringCommandMountItem", className)
47+
}
48+
49+
@Test
50+
fun createDispatchCommandMountItemForInterop_withValidInt_returnsIntEvent() {
51+
val command = underTest.createDispatchCommandMountItemForInterop(11, 1, "42", null)
52+
53+
// DispatchIntCommandMountItem is package private so we can `as` check it.
54+
val className = command::class.java.name.substringAfterLast(".")
55+
assertEquals("DispatchIntCommandMountItem", className)
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.testutils.fakes
9+
10+
import com.facebook.react.uimanager.events.BatchEventDispatchedListener
11+
12+
/** A fake [BatchEventDispatchedListener] for testing that does nothing. */
13+
class FakeBatchEventDispatchedListener : BatchEventDispatchedListener {
14+
override fun onBatchEventDispatched() {
15+
// do nothing
16+
}
17+
}

packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/MyLegacyViewManager.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
public class MyLegacyViewManager extends SimpleViewManager<MyNativeView> {
2828

2929
public static final String REACT_CLASS = "RNTMyLegacyNativeView";
30-
private static final Integer COMMAND_CHANGE_BACKGROUND_COLOR = 42;
30+
private static final int COMMAND_CHANGE_BACKGROUND_COLOR = 42;
3131
private final ReactApplicationContext mCallerContext;
3232

3333
public MyLegacyViewManager(ReactApplicationContext reactContext) {
@@ -71,8 +71,7 @@ public final Map<String, Object> getExportedViewConstants() {
7171

7272
@Override
7373
public final Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
74-
Map<String, Object> eventTypeConstants = new HashMap<String, Object>();
75-
eventTypeConstants.putAll(
74+
return new HashMap<>(
7675
MapBuilder.<String, Object>builder()
7776
.put(
7877
"onColorChanged",
@@ -81,18 +80,29 @@ public final Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
8180
MapBuilder.of(
8281
"bubbled", "onColorChanged", "captured", "onColorChangedCapture")))
8382
.build());
84-
return eventTypeConstants;
8583
}
8684

8785
@Override
8886
public void receiveCommand(
8987
@NonNull MyNativeView view, String commandId, @Nullable ReadableArray args) {
90-
if (commandId.contentEquals(COMMAND_CHANGE_BACKGROUND_COLOR.toString())) {
88+
if (commandId.contentEquals("changeBackgroundColor")) {
9189
int sentColor = Color.parseColor(args.getString(0));
9290
view.setBackgroundColor(sentColor);
9391
}
9492
}
9593

94+
@SuppressWarnings("deprecation") // We intentionally want to test against the legacy API here.
95+
@Override
96+
public void receiveCommand(
97+
@NonNull MyNativeView view, int commandId, @Nullable ReadableArray args) {
98+
switch (commandId) {
99+
case COMMAND_CHANGE_BACKGROUND_COLOR:
100+
int sentColor = Color.parseColor(args.getString(0));
101+
view.setBackgroundColor(sentColor);
102+
break;
103+
}
104+
}
105+
96106
@Nullable
97107
@Override
98108
public Map<String, Integer> getCommandsMap() {

0 commit comments

Comments
 (0)