Skip to content

Commit

Permalink
Add support for Events on the Fabric Interop Layer (facebook#37059)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#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

Differential Revision: D45144246

fbshipit-source-id: 4d175d0318ab707ad7cbbab608b8603067ebbb94
  • Loading branch information
cortinico authored and facebook-github-bot committed Apr 26, 2023
1 parent aab11c2 commit 2a9cda6
Show file tree
Hide file tree
Showing 15 changed files with 566 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.interop.InteropModuleRegistry;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
import com.facebook.react.common.LifecycleState;
Expand Down Expand Up @@ -69,6 +70,8 @@ public interface RCTDeviceEventEmitter extends JavaScriptModule {
private @Nullable JSExceptionHandler mJSExceptionHandler;
private @Nullable JSExceptionHandler mExceptionHandlerWrapper;
private @Nullable WeakReference<Activity> mCurrentActivity;

private @Nullable InteropModuleRegistry mInteropModuleRegistry;
private boolean mIsInitialized = false;

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

ReactQueueConfiguration queueConfig = catalystInstance.getReactQueueConfiguration();
initializeMessageQueueThreads(queueConfig);
initializeInteropModules();
}

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

protected void initializeInteropModules() {
mInteropModuleRegistry = new InteropModuleRegistry();
}

protected void initializeInteropModules(ReactContext reactContext) {
mInteropModuleRegistry = reactContext.mInteropModuleRegistry;
}

public void resetPerfStats() {
if (mNativeModulesMessageQueueThread != null) {
mNativeModulesMessageQueueThread.resetPerfStats();
Expand Down Expand Up @@ -163,6 +175,10 @@ public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
}
throw new IllegalStateException(EARLY_JS_ACCESS_EXCEPTION_MESSAGE);
}
if (mInteropModuleRegistry != null
&& mInteropModuleRegistry.shouldReturnInteropModule(jsInterface)) {
return mInteropModuleRegistry.getInteropModule(jsInterface);
}
return mCatalystInstance.getJSModule(jsInterface);
}

Expand Down Expand Up @@ -543,4 +559,17 @@ public void registerSegment(int segmentId, String path, Callback callback) {
Assertions.assertNotNull(mCatalystInstance).registerSegment(segmentId, path);
Assertions.assertNotNull(callback).invoke();
}

/**
* Register a {@link JavaScriptModule} within the Interop Layer so that can be consumed whenever
* getJSModule is invoked.
*
* <p>This method is internal to React Native and should not be used externally.
*/
public <T extends JavaScriptModule> void internal_registerInteropModule(
Class<T> interopModuleInterface, Object interopModule) {
if (mInteropModuleRegistry != null) {
mInteropModuleRegistry.registerInteropModule(interopModuleInterface, interopModule);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.bridge.interop;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.config.ReactFeatureFlags;
import java.util.HashMap;

/**
* A utility class that takes care of returning {@link JavaScriptModule} which are used for the
* Fabric Interop Layer. This allows us to override the returned classes once the user is invoking
* `ReactContext.getJsModule()`.
*
* <p>Currently we only support a `RCTEventEmitter` re-implementation, being `InteropEventEmitter`
* but this class can support other re-implementation in the future.
*/
public class InteropModuleRegistry {

@SuppressWarnings("rawtypes")
private final HashMap<Class, Object> supportedModules;

public InteropModuleRegistry() {
this.supportedModules = new HashMap<>();
}

public <T extends JavaScriptModule> boolean shouldReturnInteropModule(Class<T> requestedModule) {
return checkReactFeatureFlagsConditions() && supportedModules.containsKey(requestedModule);
}

@Nullable
public <T extends JavaScriptModule> T getInteropModule(Class<T> requestedModule) {
if (checkReactFeatureFlagsConditions()) {
//noinspection unchecked
return (T) supportedModules.get(requestedModule);
} else {
return null;
}
}

public <T extends JavaScriptModule> void registerInteropModule(
Class<T> interopModuleInterface, Object interopModule) {
if (checkReactFeatureFlagsConditions()) {
supportedModules.put(interopModuleInterface, interopModule);
}
}

private boolean checkReactFeatureFlagsConditions() {
return ReactFeatureFlags.enableFabricRenderer && ReactFeatureFlags.unstable_useFabricInterop;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ public class ReactFeatureFlags {
*/
public static volatile boolean enableFabricRenderer = false;

/**
* Should this application enable the Fabric Interop Layer for Android? If yes, the application
* will behave so that it can accept non-Fabric components and render them on Fabric. This toggle
* is controlling extra logic such as custom event dispatching that are needed for the Fabric
* Interop Layer to work correctly.
*/
public static volatile boolean unstable_useFabricInterop = false;

/**
* Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable
* the following flags: `useTurboModules` & `enableFabricRenderer`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ object DefaultNewArchitectureEntryPoint {
) {
ReactFeatureFlags.useTurboModules = turboModulesEnabled
ReactFeatureFlags.enableFabricRenderer = fabricEnabled
ReactFeatureFlags.unstable_useFabricInterop = fabricEnabled

this.privateFabricEnabled = fabricEnabled
this.privateTurboModulesEnabled = turboModulesEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import com.facebook.react.fabric.events.EventBeatManager;
import com.facebook.react.fabric.events.EventEmitterWrapper;
import com.facebook.react.fabric.events.FabricEventEmitter;
import com.facebook.react.fabric.interop.InteropEventEmitter;
import com.facebook.react.fabric.mounting.MountItemDispatcher;
import com.facebook.react.fabric.mounting.MountingManager;
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
Expand All @@ -77,6 +78,7 @@
import com.facebook.react.uimanager.events.EventCategoryDef;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.EventDispatcherImpl;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.text.TextLayoutManager;
import com.facebook.react.views.text.TextLayoutManagerMapBuffer;
import java.util.HashMap;
Expand Down Expand Up @@ -383,6 +385,11 @@ public void initialize() {

ReactMarker.addFabricListener(mDevToolsReactPerfLogger);
}
if (ReactFeatureFlags.unstable_useFabricInterop) {
InteropEventEmitter interopEventEmitter = new InteropEventEmitter(mReactApplicationContext);
mReactApplicationContext.internal_registerInteropModule(
RCTEventEmitter.class, interopEventEmitter);
}
}

// This is called on the JS thread (see CatalystInstanceImpl).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.fabric.interop;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.events.Event;

/**
* An {@link Event} class used by the {@link InteropEventEmitter}. This class is just holding the
* event name and the data which is received by the `receiveEvent` method and will be passed over
* the the {@link com.facebook.react.uimanager.events.EventDispatcher}
*/
class InteropEvent extends Event<InteropEvent> {

private final String mName;
private final WritableMap mEventData;

InteropEvent(String name, @Nullable WritableMap eventData, int surfaceId, int viewTag) {
super(surfaceId, viewTag);
mName = name;
mEventData = eventData;
}

@Override
public String getEventName() {
return mName;
}

@Override
@VisibleForTesting
public WritableMap getEventData() {
return mEventData;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.fabric.interop;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.RCTEventEmitter;

/**
* A reimplementation of {@link RCTEventEmitter} which is using a {@link EventDispatcher} under the
* hood.
*
* <p>On Fabric, you're supposed to use {@link EventDispatcher} to dispatch events. However, we
* provide an interop layer for non-Fabric migrated components.
*
* <p>This instance will be returned if the user is invoking `context.getJsModule(RCTEventEmitter)
* and is providing support for the `receiveEvent` method, so that non-Fabric ViewManagers can
* continue to deliver events also when Fabric is turned on.
*/
public class InteropEventEmitter implements RCTEventEmitter {

private final ReactContext mReactContext;

private @Nullable EventDispatcher mEventDispatcherOverride;

public InteropEventEmitter(ReactContext reactContext) {
mReactContext = reactContext;
}

@Override
public void receiveEvent(int targetReactTag, String eventName, @Nullable WritableMap eventData) {
EventDispatcher dispatcher;
if (mEventDispatcherOverride != null) {
dispatcher = mEventDispatcherOverride;
} else {
dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mReactContext, targetReactTag);
}
int surfaceId = UIManagerHelper.getSurfaceId(mReactContext);
if (dispatcher != null) {
dispatcher.dispatchEvent(new InteropEvent(eventName, eventData, surfaceId, targetReactTag));
}
}

@Override
public void receiveTouches(
String eventName, WritableArray touches, WritableArray changedIndices) {
throw new UnsupportedOperationException(
"EventEmitter#receiveTouches is not supported by the Fabric Interop Layer");
}

@VisibleForTesting
void overrideEventDispatcher(EventDispatcher eventDispatcherOverride) {
mEventDispatcherOverride = eventDispatcherOverride;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public ThemedReactContext(
if (reactApplicationContext.hasCatalystInstance()) {
initializeWithInstance(reactApplicationContext.getCatalystInstance());
}
initializeInteropModules(reactApplicationContext);
mReactApplicationContext = reactApplicationContext;
mModuleName = moduleName;
mSurfaceId = surfaceId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.systrace.SystraceMessage;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

Map viewManagerBubblingEvents = viewManager.getExportedCustomBubblingEventTypeConstants();
if (viewManagerBubblingEvents != null) {

if (ReactFeatureFlags.enableFabricRenderer && ReactFeatureFlags.unstable_useFabricInterop) {
// For Fabric, events needs to be fired with a "top" prefix.
// For the sake of Fabric Interop, here we normalize events adding "top" in their
// name if the user hasn't provided it.
normalizeEventTypes(viewManagerBubblingEvents);
}
recursiveMerge(cumulativeBubblingEventTypes, viewManagerBubblingEvents);
recursiveMerge(viewManagerBubblingEvents, defaultBubblingEvents);
viewManagerConstants.put(BUBBLING_EVENTS_KEY, viewManagerBubblingEvents);
Expand Down Expand Up @@ -145,6 +156,27 @@
return viewManagerConstants;
}

@VisibleForTesting
/* package */ static void normalizeEventTypes(Map events) {
if (events == null) {
return;
}
Set<String> keysToNormalize = new HashSet<>();
for (Object key : events.keySet()) {
if (key instanceof String) {
String keyString = (String) key;
if (!keyString.startsWith("top")) {
keysToNormalize.add(keyString);
}
}
}
for (String oldKey : keysToNormalize) {
Object value = events.get(oldKey);
String newKey = "top" + oldKey.substring(0, 1).toUpperCase() + oldKey.substring(1);
events.put(newKey, value);
}
}

/** Merges {@param source} map into {@param dest} map recursively */
private static void recursiveMerge(@Nullable Map dest, @Nullable Map source) {
if (dest == null || source == null || source.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.bridge.interop;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;

public class FakeRCTEventEmitter implements RCTEventEmitter {

@Override
public void receiveEvent(int targetReactTag, String eventName, @Nullable WritableMap event) {}

@Override
public void receiveTouches(
String eventName, WritableArray touches, WritableArray changedIndices) {}
}
Loading

0 comments on commit 2a9cda6

Please sign in to comment.