Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 111 additions & 11 deletions android/src/main/java/com/reactnativenavigation/react/ReactView.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
Expand All @@ -27,12 +28,27 @@

@SuppressLint("ViewConstructor")
public class ReactView extends FrameLayout implements IReactView, Renderable {
private static final String TAG = "RNN.ReactView";
/**
* Upper bound on deferred lifecycle retries (~20s at ~60fps-style scheduling).
* After this we emit with React context even if {@link #isRendered()} is still false,
* matching legacy behavior for edge cases.
*/
private static final int MAX_DEFERRED_LIFECYCLE_RETRIES = 1200;

private final String componentId;
private final String componentName;
private boolean isAttachedToReactInstance = false;

private final ReactSurface reactSurface;

private boolean pendingWillAppear;
private boolean pendingDidAppear;
private ComponentType pendingWillType;
private ComponentType pendingDidType;
private int deferredLifecycleRetryCount;
private boolean deferredLifecycleFlushPosted;

public ReactView(final Context context, String componentId, String componentName) {
super(context);
this.componentId = componentId;
Expand Down Expand Up @@ -68,33 +84,117 @@ public ReactView asView() {

@Override
public void destroy() {
clearDeferredLifecycleEvents();
reactSurface.stop();
}

public void sendComponentWillStart(ComponentType type) {
this.post(() -> {
ReactContext currentReactContext = getReactContext();
if (currentReactContext != null)
new EventEmitter(currentReactContext).emitComponentWillAppear(componentId, componentName, type);
});
this.post(() -> handleSendComponentWillStart(type));
}

public void sendComponentStart(ComponentType type) {
this.post(() -> {
ReactContext currentReactContext = getReactContext();
if (currentReactContext != null) {
new EventEmitter(currentReactContext).emitComponentDidAppear(componentId, componentName, type);
}
});
this.post(() -> handleSendComponentStart(type));
}

public void sendComponentStop(ComponentType type) {
clearDeferredLifecycleEvents();
ReactContext currentReactContext = getReactContext();
if (currentReactContext != null) {
new EventEmitter(currentReactContext).emitComponentDidDisappear(componentId, componentName, type);
}
}

private void handleSendComponentWillStart(ComponentType type) {
if (!shouldDeferLifecycleEmit()) {
emitComponentWillAppear(type);
return;
}
pendingWillAppear = true;
pendingWillType = type;
scheduleFlushDeferredLifecycleEvents();
}

private void handleSendComponentStart(ComponentType type) {
if (!shouldDeferLifecycleEmit()) {
emitComponentDidAppear(type);
return;
}
pendingDidAppear = true;
pendingDidType = type;
scheduleFlushDeferredLifecycleEvents();
}

/**
* Wait until the Fabric surface has mounted content so JS has typically committed the screen and
* {@code bindComponent} can receive appear events (parity with iOS {@code RNNReactView} pending appear).
*/
private boolean shouldDeferLifecycleEmit() {
return getReactContext() == null || !isRendered();
}

private void scheduleFlushDeferredLifecycleEvents() {
if (deferredLifecycleFlushPosted) return;
deferredLifecycleFlushPosted = true;
post(() -> {
deferredLifecycleFlushPosted = false;
flushDeferredLifecycleEvents();
});
}

private void flushDeferredLifecycleEvents() {
if (!pendingWillAppear && !pendingDidAppear) {
deferredLifecycleRetryCount = 0;
return;
}

boolean strictReady = !shouldDeferLifecycleEmit();
if (!strictReady) {
if (++deferredLifecycleRetryCount <= MAX_DEFERRED_LIFECYCLE_RETRIES) {
scheduleFlushDeferredLifecycleEvents();
return;
}
deferredLifecycleRetryCount = 0;
if (getReactContext() == null) {
Log.w(TAG, "Deferred component lifecycle events dropped (React context not ready)");
clearDeferredLifecycleEvents();
return;
}
// Degraded: context exists but surface not reporting children yet — emit once (legacy timing).
} else {
deferredLifecycleRetryCount = 0;
}

if (pendingWillAppear) {
pendingWillAppear = false;
emitComponentWillAppear(pendingWillType);
}
if (pendingDidAppear) {
pendingDidAppear = false;
emitComponentDidAppear(pendingDidType);
}
}

private void clearDeferredLifecycleEvents() {
pendingWillAppear = false;
pendingDidAppear = false;
deferredLifecycleRetryCount = 0;
deferredLifecycleFlushPosted = false;
}

private void emitComponentWillAppear(ComponentType type) {
ReactContext currentReactContext = getReactContext();
if (currentReactContext != null) {
new EventEmitter(currentReactContext).emitComponentWillAppear(componentId, componentName, type);
}
}

private void emitComponentDidAppear(ComponentType type) {
ReactContext currentReactContext = getReactContext();
if (currentReactContext != null) {
new EventEmitter(currentReactContext).emitComponentDidAppear(componentId, componentName, type);
}
}

@Override
public void sendOnNavigationButtonPressed(String buttonId) {
ReactContext currentReactContext = getReactContext();
Expand Down