I. Problem description

React Native project: If the superior page A is an RN page and there is A Modal bomb layer in this page, the inferior page B is redirected through routing, and A Native RN container is used to carry A new RN page. At this point if you go to the lower page B and go back to the higher page A you’ll notice that clicking on it no longer shows you the Modal popup.

Second, cause analysis

We can first look at the Modal shell layer’s native implementation reactModalHostView.java, key code analysis

  /** * showOrUpdate will display the Dialog. It is called by the manager once all properties are set * because we need to  know all of them before creating the Dialog. It is also smart during updates * if the changed properties can be applied  directly to the Dialog or require the recreation of a * new Dialog. */
  protected void showOrUpdate(a) {
    // If the existing Dialog is currently up, we may need to redraw it or we may be able to update
    // the property without having to recreate the dialog
    if(mDialog ! =null) {
      if (mPropertyRequiresNewDialog) {
        dismiss();
      } else {
        updateProperties();
        return; }}// Reset the flag since we are going to create a new dialog
    mPropertyRequiresNewDialog = false;
    int theme = R.style.Theme_FullScreenDialog;
    if (mAnimationType.equals("fade")) {
      theme = R.style.Theme_FullScreenDialogAnimatedFade;
    } else if (mAnimationType.equals("slide")) {
      theme = R.style.Theme_FullScreenDialogAnimatedSlide;
    }
    Activity currentActivity = getCurrentActivity();
    Context context = currentActivity == null ? getContext() : currentActivity;
    mDialog = new Dialog(context, theme);
    mDialog
        .getWindow()
        .setFlags(
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

    mDialog.setContentView(getContentView());
    updateProperties();

    mDialog.setOnShowListener(mOnShowListener);
    mDialog.setOnKeyListener(
        new DialogInterface.OnKeyListener() {
          @Override
          public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
            if (event.getAction() == KeyEvent.ACTION_UP) {
              // We need to stop the BACK button from closing the dialog by default so we capture
              // that
              // event and instead inform JS so that it can make the decision as to whether or not
              // to
              // allow the back button to close the dialog. If it chooses to, it can just set
              // visible
              // to false on the Modal and the Modal will go away
              if (keyCode == KeyEvent.KEYCODE_BACK) {
                Assertions.assertNotNull(
                    mOnRequestCloseListener,
                    "setOnRequestCloseListener must be called by the manager");
                mOnRequestCloseListener.onRequestClose(dialog);
                return true;
              } else {
                // We redirect the rest of the key events to the current activity, since the
                // activity
                // expects to receive those events and react to them, ie. in the case of the dev
                // menu
                Activity currentActivity = ((ReactContext) getContext()).getCurrentActivity();
                if(currentActivity ! =null) {
                  returncurrentActivity.onKeyUp(keyCode, event); }}}return false; }}); mDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);if (mHardwareAccelerated) {
      mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
    }
    if(currentActivity ! =null && !currentActivity.isFinishing()) {
      mDialog.show();
      if (context instanceofActivity) { mDialog .getWindow() .getDecorView() .setSystemUiVisibility( ((Activity) context).getWindow().getDecorView().getSystemUiVisibility()); } mDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); }}Copy the code

Notice the following logic to get the Activity to which the current ReactContext belongs

  private @Nullable Activity getCurrentActivity(a) {
    return ((ReactContext) getContext()).getCurrentActivity();
  }
Copy the code
  /**
   * Get the activity to which this context is currently attached, or {@code null} if not attached.
   * DO NOT HOLD LONG-LIVED REFERENCES TO THE OBJECT RETURNED BY THIS METHOD, AS THIS WILL CAUSE
   * MEMORY LEAKS.
   */
  public @Nullable Activity getCurrentActivity(a) {
    if (mCurrentActivity == null) {
      return null;
    }
    return mCurrentActivity.get();
  }
Copy the code

So we can guess that there is a high probability that the reason why the Activity is not displayed is related to the Activity, and the Activity is not currently displayed! Walk the breakpoint! As expected, I got the new page B container Activity, so why is not the current display of the Activity! Trace the code to see where mCurrentActivity was assigned

  /** Should be called by the hosting Fragment in {@link Fragment#onResume} */
  public void onHostResume(@Nullable Activity activity) {
    mLifecycleState = LifecycleState.RESUMED;
    mCurrentActivity = new WeakReference(activity);
    ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_RESUME_START);
    for (LifecycleEventListener listener : mLifecycleEventListeners) {
      try {
        listener.onHostResume();
      } catch (RuntimeException e) {
        handleException(e);
      }
    }
    ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_RESUME_END);
  }

  public void onNewIntent(@Nullable Activity activity, Intent intent) {
    UiThreadUtil.assertOnUiThread();
    mCurrentActivity = new WeakReference(activity);
    for (ActivityEventListener listener : mActivityEventListeners) {
      try {
        listener.onNewIntent(intent);
      } catch(RuntimeException e) { handleException(e); }}}Copy the code

We don’t have to worry about the assignment on onNewIntent because we’re new and we’re not going to get there, so we’re going to focus on where onHostResume gets called, Will finally be able to see in ReactInstanceManager. Java moveToResumedLifecycleState method is invoked

  private synchronized void moveToResumedLifecycleState(boolean force) {
    ReactContext currentContext = getCurrentReactContext();
    if(currentContext ! =null) {
      // we currently don't have an onCreate callback so we call onResume for both transitions
      if (force
          || mLifecycleState == LifecycleState.BEFORE_RESUME
          || mLifecycleState == LifecycleState.BEFORE_CREATE) {
        currentContext.onHostResume(mCurrentActivity);
      }
    }
    mLifecycleState = LifecycleState.RESUMED;
  }
Copy the code

And that method will eventually be called by RN’s onResume method, and you can probably guess why, because WHEN I call A’s onResume on page B back to page A, I don’t assign it, because the condition hasn’t passed, Now the mLifecycleState is still RESUMED

if (force
    || mLifecycleState == LifecycleState.BEFORE_RESUME
    || mLifecycleState == LifecycleState.BEFORE_CREATE) {
  currentContext.onHostResume(mCurrentActivity);
}
Copy the code

We continue to back ReactInstanceManager. In Java onHostDestroy method, see page B destroyed when do what things

  /**
   * Call this from {@link Activity#onDestroy()}. This notifies any listening modules so they can do
   * any necessary cleanup.
   *
   * @deprecated use {@link #onHostDestroy(Activity)} instead
   */
  @ThreadConfined(UI)
  public void onHostDestroy(a) {
    UiThreadUtil.assertOnUiThread();

    if (mUseDeveloperSupport) {
      mDevSupportManager.setDevSupportEnabled(false);
    }

    moveToBeforeCreateLifecycleState();
    mCurrentActivity = null;
  }

  private synchronized void moveToBeforeCreateLifecycleState(a) {
    ReactContext currentContext = getCurrentReactContext();
    if(currentContext ! =null) {
      if (mLifecycleState == LifecycleState.RESUMED) {
        currentContext.onHostPause();
        mLifecycleState = LifecycleState.BEFORE_RESUME;
      }
      if (mLifecycleState == LifecycleState.BEFORE_RESUME) {
        currentContext.onHostDestroy();
      }
    }
    mLifecycleState = LifecycleState.BEFORE_CREATE;
  }
Copy the code

Can be seen in the code, at the time of destruction to reset the life cycle state for BEFORE_CREATE mLifecycleState values, then combined with the previous analysis code, should be when performing moveToResumedLifecycleState method, If mLifecycleState is set to BEFORE_CREATE, re-assign mCurrentActivity to the Activity of page A. RN’s onResume and onDestroy methods are used as breakpoints in RN’s container. B back to the page in the page, the first page can be carried to A onResume method, then, to perform page B onDestroy method, so lead to when performing the moveToResumedLifecycleState method, At this point the lifecycle state mLifecycleState has not been reset to BEFORE_CREATE, so there is no logic to re-assign mCurrentActivity. When you click the event on page A to display Modal, Reactmodalhostview.java gets not the current Activity, but the Activity from the last assigned page B, so you don’t display Modal

Third, solve the problem

Analysis of the above code, we can know that Modal display is not in the order of RN container life cycle execution, so the only thing we need to do is to ensure that page B onDestroy method before page A onResume method to solve the problem! I’m going to execute onDestroy of the RN proxy method before finish on page B or after clicking on the root view of the RN container. This ensures that onDestroy on page B precedes onResume on page A!

OnDestroy is executed before Finish () because onResume is executed before onDestroy
getReactDelegate().onDestroy();
this.finish();
Copy the code
public boolean onBackPressed(a) {
    Activity rootActivity = ActivityRegisterUtil.getCurrentActivity();
    if (getReactNativeHost().hasInstance()) {
        if(rootActivity ! =null && !rootActivity.isFinishing() && rootActivity instanceof XReactActivity) {
            String rootPageName = ((XReactActivity) rootActivity).getPageName();
            // Check whether it is the root view of the RN container. If so, execute the native return, if not, execute the RN return event
            ArrayList pageNameList = ((XReactActivity) rootActivity).getPageNameList();
            if (pageNameList.size() == 1 && rootPageName.equals(pageNameList.get(0))) {
                OnDestroy is executed before Finish () because onResume is executed before onDestroy
                onDestroy();
                return false;
            } else {
                getReactNativeHost().getReactInstanceManager().onBackPressed();
                return true;
            }
        }
        getReactNativeHost().getReactInstanceManager().onBackPressed();
        return true;
    }
    return false;
}
Copy the code