Skip to content

Commit 6f2f1d6

Browse files
cortinicofacebook-github-bot
authored andcommitted
Add support for Events on the Fabric Interop Layer (#37059)
Summary: Pull Request resolved: #37059 This diff introduces InteropEventEmitter, a re-implementation of RCTEventEmitter that works with Fabric and allows to support events on the Fabric Interop for Android. Thanks to this, users can keep on calling `getJSModule(RCTEventEmitter.class).receiveEvent(...)` in their legacy ViewManagers and they will be using the EventDispatcher under the hood to dispatch events. The logic is enabled only if the `unstable_useFabricInterop` flag is turned on. I've turned this on for the template setup and for RN Tester. On top of this, this diff takes care also of event name "normalization". On Fabric, all the events needs to be registered with a "top" prefix. With this diff, we'll be adding the "top" prefix at registration time, if the user hasn't added them. This allows to use legacy ViewManagers on Fabric without having to ask users to change their event name. Changelog: [Android] [Added] - Add support for Events on the Fabric Interop Layer Reviewed By: mdvacca Differential Revision: D45144246 fbshipit-source-id: 63d4060153907c05977c976379b90574d1f69866
1 parent 6c385c6 commit 6f2f1d6

File tree

15 files changed

+566
-1
lines changed

15 files changed

+566
-1
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.facebook.infer.annotation.Assertions;
2222
import com.facebook.infer.annotation.ThreadConfined;
2323
import com.facebook.proguard.annotations.DoNotStrip;
24+
import com.facebook.react.bridge.interop.InteropModuleRegistry;
2425
import com.facebook.react.bridge.queue.MessageQueueThread;
2526
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
2627
import com.facebook.react.common.LifecycleState;
@@ -69,6 +70,8 @@ public interface RCTDeviceEventEmitter extends JavaScriptModule {
6970
private @Nullable JSExceptionHandler mJSExceptionHandler;
7071
private @Nullable JSExceptionHandler mExceptionHandlerWrapper;
7172
private @Nullable WeakReference<Activity> mCurrentActivity;
73+
74+
private @Nullable InteropModuleRegistry mInteropModuleRegistry;
7275
private boolean mIsInitialized = false;
7376

7477
public ReactContext(Context base) {
@@ -93,6 +96,7 @@ public void initializeWithInstance(CatalystInstance catalystInstance) {
9396

9497
ReactQueueConfiguration queueConfig = catalystInstance.getReactQueueConfiguration();
9598
initializeMessageQueueThreads(queueConfig);
99+
initializeInteropModules();
96100
}
97101

98102
/** Initialize message queue threads using a ReactQueueConfiguration. */
@@ -120,6 +124,14 @@ public synchronized void initializeMessageQueueThreads(ReactQueueConfiguration q
120124
mIsInitialized = true;
121125
}
122126

127+
protected void initializeInteropModules() {
128+
mInteropModuleRegistry = new InteropModuleRegistry();
129+
}
130+
131+
protected void initializeInteropModules(ReactContext reactContext) {
132+
mInteropModuleRegistry = reactContext.mInteropModuleRegistry;
133+
}
134+
123135
public void resetPerfStats() {
124136
if (mNativeModulesMessageQueueThread != null) {
125137
mNativeModulesMessageQueueThread.resetPerfStats();
@@ -163,6 +175,10 @@ public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
163175
}
164176
throw new IllegalStateException(EARLY_JS_ACCESS_EXCEPTION_MESSAGE);
165177
}
178+
if (mInteropModuleRegistry != null
179+
&& mInteropModuleRegistry.shouldReturnInteropModule(jsInterface)) {
180+
return mInteropModuleRegistry.getInteropModule(jsInterface);
181+
}
166182
return mCatalystInstance.getJSModule(jsInterface);
167183
}
168184

@@ -543,4 +559,17 @@ public void registerSegment(int segmentId, String path, Callback callback) {
543559
Assertions.assertNotNull(mCatalystInstance).registerSegment(segmentId, path);
544560
Assertions.assertNotNull(callback).invoke();
545561
}
562+
563+
/**
564+
* Register a {@link JavaScriptModule} within the Interop Layer so that can be consumed whenever
565+
* getJSModule is invoked.
566+
*
567+
* <p>This method is internal to React Native and should not be used externally.
568+
*/
569+
public <T extends JavaScriptModule> void internal_registerInteropModule(
570+
Class<T> interopModuleInterface, Object interopModule) {
571+
if (mInteropModuleRegistry != null) {
572+
mInteropModuleRegistry.registerInteropModule(interopModuleInterface, interopModule);
573+
}
574+
}
546575
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.bridge.interop;
9+
10+
import androidx.annotation.Nullable;
11+
import com.facebook.react.bridge.JavaScriptModule;
12+
import com.facebook.react.config.ReactFeatureFlags;
13+
import java.util.HashMap;
14+
15+
/**
16+
* A utility class that takes care of returning {@link JavaScriptModule} which are used for the
17+
* Fabric Interop Layer. This allows us to override the returned classes once the user is invoking
18+
* `ReactContext.getJsModule()`.
19+
*
20+
* <p>Currently we only support a `RCTEventEmitter` re-implementation, being `InteropEventEmitter`
21+
* but this class can support other re-implementation in the future.
22+
*/
23+
public class InteropModuleRegistry {
24+
25+
@SuppressWarnings("rawtypes")
26+
private final HashMap<Class, Object> supportedModules;
27+
28+
public InteropModuleRegistry() {
29+
this.supportedModules = new HashMap<>();
30+
}
31+
32+
public <T extends JavaScriptModule> boolean shouldReturnInteropModule(Class<T> requestedModule) {
33+
return checkReactFeatureFlagsConditions() && supportedModules.containsKey(requestedModule);
34+
}
35+
36+
@Nullable
37+
public <T extends JavaScriptModule> T getInteropModule(Class<T> requestedModule) {
38+
if (checkReactFeatureFlagsConditions()) {
39+
//noinspection unchecked
40+
return (T) supportedModules.get(requestedModule);
41+
} else {
42+
return null;
43+
}
44+
}
45+
46+
public <T extends JavaScriptModule> void registerInteropModule(
47+
Class<T> interopModuleInterface, Object interopModule) {
48+
if (checkReactFeatureFlagsConditions()) {
49+
supportedModules.put(interopModuleInterface, interopModule);
50+
}
51+
}
52+
53+
private boolean checkReactFeatureFlagsConditions() {
54+
return ReactFeatureFlags.enableFabricRenderer && ReactFeatureFlags.unstable_useFabricInterop;
55+
}
56+
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ public class ReactFeatureFlags {
4242
*/
4343
public static volatile boolean enableFabricRenderer = false;
4444

45+
/**
46+
* Should this application enable the Fabric Interop Layer for Android? If yes, the application
47+
* will behave so that it can accept non-Fabric components and render them on Fabric. This toggle
48+
* is controlling extra logic such as custom event dispatching that are needed for the Fabric
49+
* Interop Layer to work correctly.
50+
*/
51+
public static volatile boolean unstable_useFabricInterop = false;
52+
4553
/**
4654
* Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable
4755
* the following flags: `useTurboModules` & `enableFabricRenderer`.

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ object DefaultNewArchitectureEntryPoint {
3131
) {
3232
ReactFeatureFlags.useTurboModules = turboModulesEnabled
3333
ReactFeatureFlags.enableFabricRenderer = fabricEnabled
34+
ReactFeatureFlags.unstable_useFabricInterop = fabricEnabled
3435

3536
this.privateFabricEnabled = fabricEnabled
3637
this.privateTurboModulesEnabled = turboModulesEnabled

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import com.facebook.react.fabric.events.EventBeatManager;
5656
import com.facebook.react.fabric.events.EventEmitterWrapper;
5757
import com.facebook.react.fabric.events.FabricEventEmitter;
58+
import com.facebook.react.fabric.interop.InteropEventEmitter;
5859
import com.facebook.react.fabric.mounting.MountItemDispatcher;
5960
import com.facebook.react.fabric.mounting.MountingManager;
6061
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
@@ -77,6 +78,7 @@
7778
import com.facebook.react.uimanager.events.EventCategoryDef;
7879
import com.facebook.react.uimanager.events.EventDispatcher;
7980
import com.facebook.react.uimanager.events.EventDispatcherImpl;
81+
import com.facebook.react.uimanager.events.RCTEventEmitter;
8082
import com.facebook.react.views.text.TextLayoutManager;
8183
import com.facebook.react.views.text.TextLayoutManagerMapBuffer;
8284
import java.util.HashMap;
@@ -383,6 +385,11 @@ public void initialize() {
383385

384386
ReactMarker.addFabricListener(mDevToolsReactPerfLogger);
385387
}
388+
if (ReactFeatureFlags.unstable_useFabricInterop) {
389+
InteropEventEmitter interopEventEmitter = new InteropEventEmitter(mReactApplicationContext);
390+
mReactApplicationContext.internal_registerInteropModule(
391+
RCTEventEmitter.class, interopEventEmitter);
392+
}
386393
}
387394

388395
// This is called on the JS thread (see CatalystInstanceImpl).
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.interop;
9+
10+
import androidx.annotation.Nullable;
11+
import com.facebook.react.bridge.WritableMap;
12+
import com.facebook.react.common.annotations.VisibleForTesting;
13+
import com.facebook.react.uimanager.events.Event;
14+
15+
/**
16+
* An {@link Event} class used by the {@link InteropEventEmitter}. This class is just holding the
17+
* event name and the data which is received by the `receiveEvent` method and will be passed over
18+
* the the {@link com.facebook.react.uimanager.events.EventDispatcher}
19+
*/
20+
class InteropEvent extends Event<InteropEvent> {
21+
22+
private final String mName;
23+
private final WritableMap mEventData;
24+
25+
InteropEvent(String name, @Nullable WritableMap eventData, int surfaceId, int viewTag) {
26+
super(surfaceId, viewTag);
27+
mName = name;
28+
mEventData = eventData;
29+
}
30+
31+
@Override
32+
public String getEventName() {
33+
return mName;
34+
}
35+
36+
@Override
37+
@VisibleForTesting
38+
public WritableMap getEventData() {
39+
return mEventData;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.interop;
9+
10+
import androidx.annotation.Nullable;
11+
import com.facebook.react.bridge.ReactContext;
12+
import com.facebook.react.bridge.WritableArray;
13+
import com.facebook.react.bridge.WritableMap;
14+
import com.facebook.react.common.annotations.VisibleForTesting;
15+
import com.facebook.react.uimanager.UIManagerHelper;
16+
import com.facebook.react.uimanager.events.EventDispatcher;
17+
import com.facebook.react.uimanager.events.RCTEventEmitter;
18+
19+
/**
20+
* A reimplementation of {@link RCTEventEmitter} which is using a {@link EventDispatcher} under the
21+
* hood.
22+
*
23+
* <p>On Fabric, you're supposed to use {@link EventDispatcher} to dispatch events. However, we
24+
* provide an interop layer for non-Fabric migrated components.
25+
*
26+
* <p>This instance will be returned if the user is invoking `context.getJsModule(RCTEventEmitter)
27+
* and is providing support for the `receiveEvent` method, so that non-Fabric ViewManagers can
28+
* continue to deliver events also when Fabric is turned on.
29+
*/
30+
public class InteropEventEmitter implements RCTEventEmitter {
31+
32+
private final ReactContext mReactContext;
33+
34+
private @Nullable EventDispatcher mEventDispatcherOverride;
35+
36+
public InteropEventEmitter(ReactContext reactContext) {
37+
mReactContext = reactContext;
38+
}
39+
40+
@Override
41+
public void receiveEvent(int targetReactTag, String eventName, @Nullable WritableMap eventData) {
42+
EventDispatcher dispatcher;
43+
if (mEventDispatcherOverride != null) {
44+
dispatcher = mEventDispatcherOverride;
45+
} else {
46+
dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mReactContext, targetReactTag);
47+
}
48+
int surfaceId = UIManagerHelper.getSurfaceId(mReactContext);
49+
if (dispatcher != null) {
50+
dispatcher.dispatchEvent(new InteropEvent(eventName, eventData, surfaceId, targetReactTag));
51+
}
52+
}
53+
54+
@Override
55+
public void receiveTouches(
56+
String eventName, WritableArray touches, WritableArray changedIndices) {
57+
throw new UnsupportedOperationException(
58+
"EventEmitter#receiveTouches is not supported by the Fabric Interop Layer");
59+
}
60+
61+
@VisibleForTesting
62+
void overrideEventDispatcher(EventDispatcher eventDispatcherOverride) {
63+
mEventDispatcherOverride = eventDispatcherOverride;
64+
}
65+
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public ThemedReactContext(
5050
if (reactApplicationContext.hasCatalystInstance()) {
5151
initializeWithInstance(reactApplicationContext.getCatalystInstance());
5252
}
53+
initializeInteropModules(reactApplicationContext);
5354
mReactApplicationContext = reactApplicationContext;
5455
mModuleName = moduleName;
5556
mSurfaceId = surfaceId;

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE;
1111

1212
import androidx.annotation.Nullable;
13+
import androidx.annotation.VisibleForTesting;
1314
import com.facebook.react.common.MapBuilder;
15+
import com.facebook.react.config.ReactFeatureFlags;
1416
import com.facebook.systrace.SystraceMessage;
1517
import java.util.ArrayList;
18+
import java.util.HashSet;
1619
import java.util.List;
1720
import java.util.Map;
21+
import java.util.Set;
1822

1923
/**
2024
* Helps generate constants map for {@link UIManagerModule} by collecting and merging constants from
@@ -113,6 +117,13 @@
113117

114118
Map viewManagerBubblingEvents = viewManager.getExportedCustomBubblingEventTypeConstants();
115119
if (viewManagerBubblingEvents != null) {
120+
121+
if (ReactFeatureFlags.enableFabricRenderer && ReactFeatureFlags.unstable_useFabricInterop) {
122+
// For Fabric, events needs to be fired with a "top" prefix.
123+
// For the sake of Fabric Interop, here we normalize events adding "top" in their
124+
// name if the user hasn't provided it.
125+
normalizeEventTypes(viewManagerBubblingEvents);
126+
}
116127
recursiveMerge(cumulativeBubblingEventTypes, viewManagerBubblingEvents);
117128
recursiveMerge(viewManagerBubblingEvents, defaultBubblingEvents);
118129
viewManagerConstants.put(BUBBLING_EVENTS_KEY, viewManagerBubblingEvents);
@@ -145,6 +156,27 @@
145156
return viewManagerConstants;
146157
}
147158

159+
@VisibleForTesting
160+
/* package */ static void normalizeEventTypes(Map events) {
161+
if (events == null) {
162+
return;
163+
}
164+
Set<String> keysToNormalize = new HashSet<>();
165+
for (Object key : events.keySet()) {
166+
if (key instanceof String) {
167+
String keyString = (String) key;
168+
if (!keyString.startsWith("top")) {
169+
keysToNormalize.add(keyString);
170+
}
171+
}
172+
}
173+
for (String oldKey : keysToNormalize) {
174+
Object value = events.get(oldKey);
175+
String newKey = "top" + oldKey.substring(0, 1).toUpperCase() + oldKey.substring(1);
176+
events.put(newKey, value);
177+
}
178+
}
179+
148180
/** Merges {@param source} map into {@param dest} map recursively */
149181
private static void recursiveMerge(@Nullable Map dest, @Nullable Map source) {
150182
if (dest == null || source == null || source.isEmpty()) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.bridge.interop;
9+
10+
import androidx.annotation.Nullable;
11+
import com.facebook.react.bridge.WritableArray;
12+
import com.facebook.react.bridge.WritableMap;
13+
import com.facebook.react.uimanager.events.RCTEventEmitter;
14+
15+
public class FakeRCTEventEmitter implements RCTEventEmitter {
16+
17+
@Override
18+
public void receiveEvent(int targetReactTag, String eventName, @Nullable WritableMap event) {}
19+
20+
@Override
21+
public void receiveTouches(
22+
String eventName, WritableArray touches, WritableArray changedIndices) {}
23+
}

0 commit comments

Comments
 (0)