Skip to content

Commit

Permalink
Queue the event for preallocated but not mounted view to dispatch later
Browse files Browse the repository at this point in the history
Summary:
This diff fixed an edge case that event dispatching is failed after pre-allocation of a view and before the view is mounted.

When a cached image is loaded, we will dispatch the event to JS immediately. This is could happen after the view is created during pre-allocation phase, when the event emitter is not instantiated yet. In that case, we will see [an error](https://github.com/facebook/react-native/blob/main/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java#L927) and the event will effectively be ignored.

To fix that we introduced a queue in this diff for those events. They will be dispatched in order when the view is mounted and the event emitter is non-null.

Changelog:
[Android][Fixed] - Fixed an edge case that event dispatching is failed after pre-allocation of a view and before the view is mounted.

Reviewed By: mullender

Differential Revision: D36331914

fbshipit-source-id: cd065b0b36978cb5f0aac793d8d16f07a48f0881
  • Loading branch information
ryancat authored and facebook-github-bot committed May 14, 2022
1 parent 644fe43 commit a093fe5
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ public static boolean isMapBufferSerializationEnabled() {

public static boolean dispatchPointerEvents = false;

/** Feature Flag to enable the pending event queue in fabric before mounting views */
public static boolean enableFabricPendingEventQueue = false;

/** Feature Flag to control RN Android scrollEventThrottle prop. */
public static boolean enableScrollEventThrottle = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import com.facebook.react.fabric.mounting.MountItemDispatcher;
import com.facebook.react.fabric.mounting.MountingManager;
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
import com.facebook.react.fabric.mounting.SurfaceMountingManager.ViewEvent;
import com.facebook.react.fabric.mounting.mountitems.DispatchIntCommandMountItem;
import com.facebook.react.fabric.mounting.mountitems.DispatchStringCommandMountItem;
import com.facebook.react.fabric.mounting.mountitems.IntBufferBatchMountItem;
Expand Down Expand Up @@ -923,8 +924,18 @@ public void receiveEvent(
EventEmitterWrapper eventEmitter = mMountingManager.getEventEmitter(surfaceId, reactTag);

if (eventEmitter == null) {
// This can happen if the view has disappeared from the screen (because of async events)
FLog.d(TAG, "Unable to invoke event: " + eventName + " for reactTag: " + reactTag);
if (ReactFeatureFlags.enableFabricPendingEventQueue
&& mMountingManager.getViewExists(reactTag)) {
// The view is preallocated and created. However, it hasn't been mounted yet. We will have
// access to the event emitter later when the view is mounted. For now just save the event
// in the view state and trigger it later.
mMountingManager.enqueuePendingEvent(
reactTag,
new ViewEvent(eventName, params, eventCategory, canCoalesceEvent, customCoalesceKey));
} else {
// This can happen if the view has disappeared from the screen (because of async events)
FLog.d(TAG, "Unable to invoke event: " + eventName + " for reactTag: " + reactTag);
}
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.facebook.react.common.mapbuffer.MapBuffer;
import com.facebook.react.fabric.FabricUIManager;
import com.facebook.react.fabric.events.EventEmitterWrapper;
import com.facebook.react.fabric.mounting.SurfaceMountingManager.ViewEvent;
import com.facebook.react.fabric.mounting.mountitems.MountItem;
import com.facebook.react.touch.JSResponderHandler;
import com.facebook.react.uimanager.RootViewManager;
Expand Down Expand Up @@ -422,4 +423,14 @@ public long measureMapBuffer(
public void initializeViewManager(String componentName) {
mViewManagerRegistry.get(componentName);
}

public void enqueuePendingEvent(int reactTag, ViewEvent viewEvent) {
@Nullable SurfaceMountingManager smm = getSurfaceManagerForView(reactTag);
if (smm == null) {
// Cannot queue event without valid surface mountng manager. Do nothing here.
return;
}

smm.enqueuePendingEvent(reactTag, viewEvent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.facebook.react.bridge.RetryableMountingLayerException;
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
import com.facebook.react.config.ReactFeatureFlags;
Expand All @@ -43,9 +44,12 @@
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.ViewManagerRegistry;
import com.facebook.react.uimanager.events.EventCategoryDef;
import com.facebook.react.views.view.ReactMapBufferViewManager;
import com.facebook.react.views.view.ReactViewManagerWrapper;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
Expand Down Expand Up @@ -849,6 +853,7 @@ public void updateState(final int reactTag, @Nullable StateWrapper stateWrapper)
}
}

/** We update the event emitter from the main thread when the view is mounted. */
@UiThread
public void updateEventEmitter(int reactTag, @NonNull EventEmitterWrapper eventEmitter) {
UiThreadUtil.assertOnUiThread();
Expand All @@ -870,6 +875,20 @@ public void updateEventEmitter(int reactTag, @NonNull EventEmitterWrapper eventE
if (previousEventEmitterWrapper != eventEmitter && previousEventEmitterWrapper != null) {
previousEventEmitterWrapper.destroy();
}

if (viewState.mPendingEventQueue != null) {
// Invoke pending event queued to the view state
for (ViewEvent viewEvent : viewState.mPendingEventQueue) {
if (viewEvent.canCoalesceEvent()) {
eventEmitter.invokeUnique(
viewEvent.getEventName(), viewEvent.getParams(), viewEvent.getCustomCoalesceKey());
} else {
eventEmitter.invoke(
viewEvent.getEventName(), viewEvent.getParams(), viewEvent.getEventCategory());
}
}
viewState.mPendingEventQueue = null;
}
}

@UiThread
Expand Down Expand Up @@ -1065,6 +1084,21 @@ public void printSurfaceState() {
}
}

@UiThread
public void enqueuePendingEvent(int reactTag, ViewEvent viewEvent) {
UiThreadUtil.assertOnUiThread();

ViewState viewState = mTagToViewState.get(reactTag);
if (viewState == null) {
// Cannot queue event without view state. Do nothing here.
return;
}
if (viewState.mPendingEventQueue == null) {
viewState.mPendingEventQueue = new LinkedList<>();
}
viewState.mPendingEventQueue.add(viewEvent);
}

/**
* This class holds view state for react tags. Objects of this class are stored into the {@link
* #mTagToViewState}, and they should be updated in the same thread.
Expand All @@ -1078,6 +1112,7 @@ private static class ViewState {
@Nullable public ReadableMap mCurrentLocalData = null;
@Nullable public StateWrapper mStateWrapper = null;
@Nullable public EventEmitterWrapper mEventEmitter = null;
@Nullable public Queue<ViewEvent> mPendingEventQueue = null;

private ViewState(
int reactTag, @Nullable View view, @Nullable ReactViewManagerWrapper viewManager) {
Expand Down Expand Up @@ -1112,4 +1147,45 @@ public String toString() {
+ isLayoutOnly;
}
}

public static class ViewEvent {
private final String mEventName;
private final boolean mCanCoalesceEvent;
private final int mCustomCoalesceKey;
private final @EventCategoryDef int mEventCategory;
private @Nullable WritableMap mParams;

public ViewEvent(
String eventName,
@Nullable WritableMap params,
@EventCategoryDef int eventCategory,
boolean canCoalesceEvent,
int customCoalesceKey) {
mEventName = eventName;
mParams = params;
mEventCategory = eventCategory;
mCanCoalesceEvent = canCoalesceEvent;
mCustomCoalesceKey = customCoalesceKey;
}

public String getEventName() {
return mEventName;
}

public boolean canCoalesceEvent() {
return mCanCoalesceEvent;
}

public int getCustomCoalesceKey() {
return mCustomCoalesceKey;
}

public @EventCategoryDef int getEventCategory() {
return mEventCategory;
}

public @Nullable WritableMap getParams() {
return mParams;
}
}
}

0 comments on commit a093fe5

Please sign in to comment.