Skip to content
Closed
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Callback;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before looking to merge this - does it make sense to have a ReactActivityDelegate and then a ReactDelegate or does it make sense for ReactActivity to use the ReactDelegate directly? Was trying to bring smaller changes with this PR and focus primarily on the addition of the ReactFragment but also don't want to incur possible Tech Debt

import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionListener;

import javax.annotation.Nullable;
Expand All @@ -31,10 +29,10 @@ public class ReactActivityDelegate {
private final @Nullable Activity mActivity;
private final @Nullable String mMainComponentName;

private @Nullable ReactRootView mReactRootView;
private @Nullable DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
private @Nullable PermissionListener mPermissionListener;
private @Nullable Callback mPermissionsCallback;
private ReactDelegate mReactDelegate;


@Deprecated
public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
Expand All @@ -51,10 +49,6 @@ public ReactActivityDelegate(ReactActivity activity, @Nullable String mainCompon
return null;
}

protected ReactRootView createRootView() {
return new ReactRootView(getContext());
}

/**
* Get the {@link ReactNativeHost} used by this app. By default, assumes
* {@link Activity#getApplication()} is an instance of {@link ReactApplication} and calls
Expand All @@ -67,7 +61,7 @@ protected ReactNativeHost getReactNativeHost() {
}

public ReactInstanceManager getReactInstanceManager() {
return getReactNativeHost().getReactInstanceManager();
return mReactDelegate.getReactInstanceManager();
}

public String getMainComponentName() {
Expand All @@ -76,36 +70,24 @@ public String getMainComponentName() {

protected void onCreate(Bundle savedInstanceState) {
String mainComponentName = getMainComponentName();
if (mainComponentName != null) {
loadApp(mainComponentName);
mReactDelegate = new ReactDelegate(getPlainActivity(), mainComponentName, getLaunchOptions());
if (mMainComponentName != null) {
mReactDelegate.loadApp();
getPlainActivity().setContentView(mReactDelegate.getReactRootView());
}
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}

protected void loadApp(String appKey) {
if (mReactRootView != null) {
throw new IllegalStateException("Cannot loadApp while app is already running.");
}
mReactRootView = createRootView();
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),
appKey,
getLaunchOptions());
getPlainActivity().setContentView(mReactRootView);
mReactDelegate.loadApp(appKey);
getPlainActivity().setContentView(mReactDelegate.getReactRootView());
}

protected void onPause() {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onHostPause(getPlainActivity());
}
mReactDelegate.onHostPause();
}

protected void onResume() {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onHostResume(
getPlainActivity(),
(DefaultHardwareBackBtnHandler) getPlainActivity());
}
mReactDelegate.onHostResume();

if (mPermissionsCallback != null) {
mPermissionsCallback.invoke();
Expand All @@ -114,20 +96,11 @@ protected void onResume() {
}

protected void onDestroy() {
if (mReactRootView != null) {
mReactRootView.unmountReactApplication();
mReactRootView = null;
}
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onHostDestroy(getPlainActivity());
}
mReactDelegate.onHostDestroy();
}

public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager()
.onActivityResult(getPlainActivity(), requestCode, resultCode, data);
}
mReactDelegate.onActivityResult(requestCode, resultCode, data, true);
}

public boolean onKeyDown(int keyCode, KeyEvent event) {
Expand All @@ -141,19 +114,7 @@ && getReactNativeHost().getUseDeveloperSupport()
}

public boolean onKeyUp(int keyCode, KeyEvent event) {
if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
return true;
}
boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)
.didDoubleTapR(keyCode, getPlainActivity().getCurrentFocus());
if (didDoubleTapR) {
getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
return true;
}
}
return false;
return mReactDelegate.shouldShowDevMenuOrReload(keyCode, event);
}

public boolean onKeyLongPress(int keyCode, KeyEvent event) {
Expand All @@ -167,11 +128,7 @@ && getReactNativeHost().getUseDeveloperSupport()
}

public boolean onBackPressed() {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onBackPressed();
return true;
}
return false;
return mReactDelegate.onBackPressed();
}

public boolean onNewIntent(Intent intent) {
Expand Down
143 changes: 143 additions & 0 deletions ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Copyright (c) Facebook, Inc. and its 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;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;

import javax.annotation.Nullable;

/**
* A delegate for handling React Application support. This delegate is unaware whether it is used in
* an {@link Activity} or a {@link android.app.Fragment}.
*/
public class ReactDelegate {

private final Activity mActivity;
private ReactRootView mReactRootView;

@Nullable
private final String mMainComponentName;

@Nullable
private Bundle mLaunchOptions;

@Nullable
private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;


public ReactDelegate(Activity activity, @Nullable String appKey, @Nullable Bundle launchOptions) {
mActivity = activity;
mMainComponentName = appKey;
mLaunchOptions = launchOptions;
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}

public void onHostResume() {
if (getReactNativeHost().hasInstance()) {
if (mActivity instanceof DefaultHardwareBackBtnHandler) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This forces the host activity to implement the interface, when in fact it's already implemented by the ReactFragment (that btw is never used).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out. How would you see this working then? Extra param in the constructor to pass a DefaultHardwareBackBtnHandler object which can either be a fragment or an activity that implements the interface?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be my first choice. I wonder if the the behaviour would be the same, since ReactActivity is calling super.onBackPressed() and the fragment only calls getActivity().onBackPressed(). The chain of thought is that the host activity will have to call the fragment's onBackPressed method do decide whether or not to call the super method.

Copy link
Contributor Author

@jpshelley jpshelley Feb 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually implementing react-navigation this week in a project that will make use of the back button login, in this exact fragment class, so I can tweak it a bit to figure out what feels best. But I think I agree with what you're saying above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@serj-lotutovici So I updated the code a bit in b089982

After messing with fragment navigations stacks this week, it seemed easier to allow the Activity to handle the back navigation. If the Fragment handled the back button handler, it ended up getting stuck in a loop, or it wasn't able to safely call the super.onBackPressed() like I was hoping for. I'm open to suggestions but this is the conclusion I came to.

This will be shown via documentation for sure, but I'll work on documentation if/when we get this PR "approved" and the idea is solidified.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jpshelley I agree on the importance of Fragment support. For example some components like ViewPager return Fragments for each view. This is standard. Btw, are there tests for the Fragment solution or do you have an example app that is using these modifications?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are currently in the process of open sourcing our solution at Hudl. Since contributing back to React Native seems to be taking longer then expected, we wanted to be able to share our approach with others. I will comment back on this PR when that happens.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great! I am currently doing React Native component integration into Native app and would like to try it out when available.

getReactNativeHost().getReactInstanceManager().onHostResume(mActivity, (DefaultHardwareBackBtnHandler) mActivity);
} else {
throw new ClassCastException("Host Activity does not implement DefaultHardwareBackBtnHandler");
}
}
}

public void onHostPause() {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onHostPause(mActivity);
}
}

public void onHostDestroy() {
if (mReactRootView != null) {
mReactRootView.unmountReactApplication();
mReactRootView = null;
}
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onHostDestroy(mActivity);
}
}

public boolean onBackPressed() {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager().onBackPressed();
return true;
}
return false;
}

public void onActivityResult(int requestCode, int resultCode, Intent data, boolean shouldForwardToReactInstance) {
if (getReactNativeHost().hasInstance() && shouldForwardToReactInstance) {
getReactNativeHost().getReactInstanceManager().onActivityResult(mActivity, requestCode, resultCode, data);
}
}

public void loadApp() {
loadApp(mMainComponentName);
}

public void loadApp(String appKey) {
if (mReactRootView != null) {
throw new IllegalStateException("Cannot loadApp while app is already running.");
}
mReactRootView = new ReactRootView(mActivity);
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),
appKey,
mLaunchOptions);

}

public ReactRootView getReactRootView() {
return mReactRootView;
}

/**
* Handles delegating the {@link Activity#onKeyUp(int, KeyEvent)} method to determine whether
* the application should show the developer menu or should reload the React Application.
*
* @return true if we consume the event and either shoed the develop menu or reloaded the application.
*/
public boolean shouldShowDevMenuOrReload(int keyCode, KeyEvent event) {
if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
return true;
}
boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer).didDoubleTapR(keyCode, mActivity.getCurrentFocus());
if (didDoubleTapR) {
getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
return true;
}
}
return false;
}

/**
* Get the {@link ReactNativeHost} used by this app. By default, assumes
* {@link Activity#getApplication()} is an instance of {@link ReactApplication} and calls
* {@link ReactApplication#getReactNativeHost()}. Override this method if your application class
* does not implement {@code ReactApplication} or you simply have a different mechanism for
* storing a {@code ReactNativeHost}, e.g. as a static field somewhere.
*/
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) mActivity.getApplication()).getReactNativeHost();
}

public ReactInstanceManager getReactInstanceManager() {
return getReactNativeHost().getReactInstanceManager();
}

}
Loading