Skip to content

Commit 691f2f9

Browse files
jpshelleyfacebook-github-bot
authored andcommitted
Android - Add a ReactFragment (#12199)
Summary: React Native on Android has currently been focused and targeted at using an [Activity](https://developer.android.com/reference/android/app/Activity.html) for its main form of instantiation. While this has probably worked for most companies and developers, you lose some of the modularity of a more cohesive application when working in a "brown-field" project that is currently native. This hurts more companies that are looking to adopt React Native and slowly implement it in a fully native application. A lot of developers follow Android's guidelines of using Fragments in their projects, even if it is a debated subject in the Android community, and this addition will allow others to embrace React Native more freely. (I even assume it could help with managing navigation state in applications that contain a decent amount of Native code and would be appreciated in those projects. Such as sharing the Toolbar, TabBar, ViewPager, etc in Native Android) Even with this addition, a developer will still need to host the fragment in an activity, but now that activity can contain native logic like a Drawer, Tabs, ViewPager, etc. **Test plan (required)** * We have been using this class at Hudl for over a couple of months and have found it valuable. * If the community agrees on the addition, I can add documentation to the Android sections to include notes about the potential of this Fragment. * If the community agrees on the addition, I can update one or more of the examples in the `/Examples` folder and make use of the Fragment, or even create a new example that uses a native layout manager like Drawer, Tabs, Viewpager, etc) Make sure tests pass on both Travis and Circle CI. _To Note:_ * There is also talk of using React Native inside Android Fragment's without any legit documentation, this could help remedy some of that with more documentation included in this PR https://facebook.github.io/react-native/releases/0.26/docs/embedded-app-android.html#sharing-a-reactinstance-across-multiple-activities-fragments-in-your-app * Others have also requested something similar and have a half-baked solution as well http://stackoverflow.com/questions/35221447/react-native-inside-a-fragment [ANDROID][FEATURE][ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java] - Adds support for Android's Fragment system. This allows for a more hybrid application. <!-- Help reviewers and the release process by writing your own release notes **INTERNAL and MINOR tagged notes will not be included in the next version's final release notes.** CATEGORY [----------] TYPE [ CLI ] [-------------] LOCATION [ DOCS ] [ BREAKING ] [-------------] [ GENERAL ] [ BUGFIX ] [-{Component}-] [ INTERNAL ] [ ENHANCEMENT ] [ {File} ] [ IOS ] [ FEATURE ] [ {Directory} ] |-----------| [ ANDROID ] [ MINOR ] [ {Framework} ] - | {Message} | [----------] [-------------] [-------------] |-----------| [CATEGORY] [TYPE] [LOCATION] - MESSAGE EXAMPLES: [IOS] [BREAKING] [FlatList] - Change a thing that breaks other things [ANDROID] [BUGFIX] [TextInput] - Did a thing to TextInput [CLI] [FEATURE] [local-cli/info/info.js] - CLI easier to do things with [DOCS] [BUGFIX] [GettingStarted.md] - Accidentally a thing/word [GENERAL] [ENHANCEMENT] [Yoga] - Added new yoga thing/position [INTERNAL] [FEATURE] [./scripts] - Added thing to script that nobody will see --> Pull Request resolved: #12199 Differential Revision: D14590665 Pulled By: mdvacca fbshipit-source-id: b50b708cde458f9634e0c14b3952fa32f9d82048
1 parent e62cfc0 commit 691f2f9

File tree

3 files changed

+373
-55
lines changed

3 files changed

+373
-55
lines changed

Diff for: ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java

+17-55
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515

1616
import com.facebook.infer.annotation.Assertions;
1717
import com.facebook.react.bridge.Callback;
18-
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
19-
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
2018
import com.facebook.react.modules.core.PermissionListener;
2119

20+
import com.facebook.react.uimanager.RootView;
2221
import javax.annotation.Nullable;
2322

2423
/**
@@ -31,10 +30,10 @@ public class ReactActivityDelegate {
3130
private final @Nullable Activity mActivity;
3231
private final @Nullable String mMainComponentName;
3332

34-
private @Nullable ReactRootView mReactRootView;
35-
private @Nullable DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
3633
private @Nullable PermissionListener mPermissionListener;
3734
private @Nullable Callback mPermissionsCallback;
35+
private ReactDelegate mReactDelegate;
36+
3837

3938
@Deprecated
4039
public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
@@ -52,7 +51,7 @@ public ReactActivityDelegate(ReactActivity activity, @Nullable String mainCompon
5251
}
5352

5453
protected ReactRootView createRootView() {
55-
return new ReactRootView(getContext());
54+
return mReactDelegate.createRootView();
5655
}
5756

5857
/**
@@ -67,7 +66,7 @@ protected ReactNativeHost getReactNativeHost() {
6766
}
6867

6968
public ReactInstanceManager getReactInstanceManager() {
70-
return getReactNativeHost().getReactInstanceManager();
69+
return mReactDelegate.getReactInstanceManager();
7170
}
7271

7372
public String getMainComponentName() {
@@ -76,36 +75,24 @@ public String getMainComponentName() {
7675

7776
protected void onCreate(Bundle savedInstanceState) {
7877
String mainComponentName = getMainComponentName();
79-
if (mainComponentName != null) {
80-
loadApp(mainComponentName);
78+
mReactDelegate = new ReactDelegate(getPlainActivity(), getReactNativeHost(), mainComponentName, getLaunchOptions());
79+
if (mMainComponentName != null) {
80+
mReactDelegate.loadApp();
81+
getPlainActivity().setContentView(mReactDelegate.getReactRootView());
8182
}
82-
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
8383
}
8484

8585
protected void loadApp(String appKey) {
86-
if (mReactRootView != null) {
87-
throw new IllegalStateException("Cannot loadApp while app is already running.");
88-
}
89-
mReactRootView = createRootView();
90-
mReactRootView.startReactApplication(
91-
getReactNativeHost().getReactInstanceManager(),
92-
appKey,
93-
getLaunchOptions());
94-
getPlainActivity().setContentView(mReactRootView);
86+
mReactDelegate.loadApp(appKey);
87+
getPlainActivity().setContentView(mReactDelegate.getReactRootView());
9588
}
9689

9790
protected void onPause() {
98-
if (getReactNativeHost().hasInstance()) {
99-
getReactNativeHost().getReactInstanceManager().onHostPause(getPlainActivity());
100-
}
91+
mReactDelegate.onHostPause();
10192
}
10293

10394
protected void onResume() {
104-
if (getReactNativeHost().hasInstance()) {
105-
getReactNativeHost().getReactInstanceManager().onHostResume(
106-
getPlainActivity(),
107-
(DefaultHardwareBackBtnHandler) getPlainActivity());
108-
}
95+
mReactDelegate.onHostResume();
10996

11097
if (mPermissionsCallback != null) {
11198
mPermissionsCallback.invoke();
@@ -114,20 +101,11 @@ protected void onResume() {
114101
}
115102

116103
protected void onDestroy() {
117-
if (mReactRootView != null) {
118-
mReactRootView.unmountReactApplication();
119-
mReactRootView = null;
120-
}
121-
if (getReactNativeHost().hasInstance()) {
122-
getReactNativeHost().getReactInstanceManager().onHostDestroy(getPlainActivity());
123-
}
104+
mReactDelegate.onHostDestroy();
124105
}
125106

126107
public void onActivityResult(int requestCode, int resultCode, Intent data) {
127-
if (getReactNativeHost().hasInstance()) {
128-
getReactNativeHost().getReactInstanceManager()
129-
.onActivityResult(getPlainActivity(), requestCode, resultCode, data);
130-
}
108+
mReactDelegate.onActivityResult(requestCode, resultCode, data, true);
131109
}
132110

133111
public boolean onKeyDown(int keyCode, KeyEvent event) {
@@ -141,19 +119,7 @@ && getReactNativeHost().getUseDeveloperSupport()
141119
}
142120

143121
public boolean onKeyUp(int keyCode, KeyEvent event) {
144-
if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
145-
if (keyCode == KeyEvent.KEYCODE_MENU) {
146-
getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
147-
return true;
148-
}
149-
boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)
150-
.didDoubleTapR(keyCode, getPlainActivity().getCurrentFocus());
151-
if (didDoubleTapR) {
152-
getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
153-
return true;
154-
}
155-
}
156-
return false;
122+
return mReactDelegate.shouldShowDevMenuOrReload(keyCode, event);
157123
}
158124

159125
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
@@ -167,11 +133,7 @@ && getReactNativeHost().getUseDeveloperSupport()
167133
}
168134

169135
public boolean onBackPressed() {
170-
if (getReactNativeHost().hasInstance()) {
171-
getReactNativeHost().getReactInstanceManager().onBackPressed();
172-
return true;
173-
}
174-
return false;
136+
return mReactDelegate.onBackPressed();
175137
}
176138

177139
public boolean onNewIntent(Intent intent) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react;
9+
10+
import android.app.Activity;
11+
import android.content.Intent;
12+
import android.os.Bundle;
13+
import android.view.KeyEvent;
14+
15+
import com.facebook.infer.annotation.Assertions;
16+
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
17+
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
18+
19+
import javax.annotation.Nullable;
20+
21+
/**
22+
* A delegate for handling React Application support. This delegate is unaware whether it is used in
23+
* an {@link Activity} or a {@link android.app.Fragment}.
24+
*/
25+
public class ReactDelegate {
26+
27+
private final Activity mActivity;
28+
private ReactRootView mReactRootView;
29+
30+
@Nullable
31+
private final String mMainComponentName;
32+
33+
@Nullable
34+
private Bundle mLaunchOptions;
35+
36+
@Nullable
37+
private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
38+
39+
private ReactNativeHost mReactNativeHost;
40+
41+
42+
public ReactDelegate(Activity activity, ReactNativeHost reactNativeHost, @Nullable String appKey, @Nullable Bundle launchOptions) {
43+
mActivity = activity;
44+
mMainComponentName = appKey;
45+
mLaunchOptions = launchOptions;
46+
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
47+
mReactNativeHost = reactNativeHost;
48+
}
49+
50+
public void onHostResume() {
51+
if (getReactNativeHost().hasInstance()) {
52+
if (mActivity instanceof DefaultHardwareBackBtnHandler) {
53+
getReactNativeHost().getReactInstanceManager().onHostResume(mActivity, (DefaultHardwareBackBtnHandler) mActivity);
54+
} else {
55+
throw new ClassCastException("Host Activity does not implement DefaultHardwareBackBtnHandler");
56+
}
57+
}
58+
}
59+
60+
public void onHostPause() {
61+
if (getReactNativeHost().hasInstance()) {
62+
getReactNativeHost().getReactInstanceManager().onHostPause(mActivity);
63+
}
64+
}
65+
66+
public void onHostDestroy() {
67+
if (mReactRootView != null) {
68+
mReactRootView.unmountReactApplication();
69+
mReactRootView = null;
70+
}
71+
if (getReactNativeHost().hasInstance()) {
72+
getReactNativeHost().getReactInstanceManager().onHostDestroy(mActivity);
73+
}
74+
}
75+
76+
public boolean onBackPressed() {
77+
if (getReactNativeHost().hasInstance()) {
78+
getReactNativeHost().getReactInstanceManager().onBackPressed();
79+
return true;
80+
}
81+
return false;
82+
}
83+
84+
public void onActivityResult(int requestCode, int resultCode, Intent data, boolean shouldForwardToReactInstance) {
85+
if (getReactNativeHost().hasInstance() && shouldForwardToReactInstance) {
86+
getReactNativeHost().getReactInstanceManager().onActivityResult(mActivity, requestCode, resultCode, data);
87+
}
88+
}
89+
90+
public void loadApp() {
91+
loadApp(mMainComponentName);
92+
}
93+
94+
public void loadApp(String appKey) {
95+
if (mReactRootView != null) {
96+
throw new IllegalStateException("Cannot loadApp while app is already running.");
97+
}
98+
mReactRootView = createRootView();
99+
mReactRootView.startReactApplication(
100+
getReactNativeHost().getReactInstanceManager(),
101+
appKey,
102+
mLaunchOptions);
103+
104+
}
105+
106+
public ReactRootView getReactRootView() {
107+
return mReactRootView;
108+
}
109+
110+
111+
protected ReactRootView createRootView() {
112+
return new ReactRootView(mActivity);
113+
}
114+
115+
/**
116+
* Handles delegating the {@link Activity#onKeyUp(int, KeyEvent)} method to determine whether
117+
* the application should show the developer menu or should reload the React Application.
118+
*
119+
* @return true if we consume the event and either shoed the develop menu or reloaded the application.
120+
*/
121+
public boolean shouldShowDevMenuOrReload(int keyCode, KeyEvent event) {
122+
if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
123+
if (keyCode == KeyEvent.KEYCODE_MENU) {
124+
getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
125+
return true;
126+
}
127+
boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer).didDoubleTapR(keyCode, mActivity.getCurrentFocus());
128+
if (didDoubleTapR) {
129+
getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
130+
return true;
131+
}
132+
}
133+
return false;
134+
}
135+
136+
/**
137+
* Get the {@link ReactNativeHost} used by this app.
138+
*/
139+
private ReactNativeHost getReactNativeHost() {
140+
return mReactNativeHost;
141+
}
142+
143+
public ReactInstanceManager getReactInstanceManager() {
144+
return getReactNativeHost().getReactInstanceManager();
145+
}
146+
147+
}

0 commit comments

Comments
 (0)