Skip to content

Commit e6deab9

Browse files
Merge pull request #2 from callstack-internal/feat/android-fragment
[Android] - support React Native in fragment
2 parents e529415 + 215f4fc commit e6deab9

File tree

11 files changed

+256
-25
lines changed

11 files changed

+256
-25
lines changed

android/src/main/java/com/callstack/reactnativebrownfield/BridgeManager.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package com.callstack.reactnativebrownfield
22

33
import android.app.Application
4-
import com.facebook.react.ReactInstanceManager
54
import com.facebook.react.ReactNativeHost
65
import com.facebook.react.ReactPackage
7-
import com.facebook.react.bridge.ReactContext
86
import com.facebook.soloader.SoLoader
97
import java.util.concurrent.atomic.AtomicBoolean
108

android/src/main/java/com/callstack/reactnativebrownfield/BridgeManagerJava.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.callstack.reactnativebrownfield;
22

33
import android.app.Application;
4-
import com.facebook.react.ReactInstanceManager;
54
import com.facebook.react.ReactNativeHost;
65
import com.facebook.react.ReactPackage;
76
import kotlin.Unit;

android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeActivity.kt

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
package com.callstack.reactnativebrownfield
22

33
import android.annotation.TargetApi
4-
import android.app.Activity
4+
import android.content.Context
55
import android.content.Intent
66
import android.os.Build
77
import android.os.Bundle
88
import android.view.KeyEvent
9-
import androidx.appcompat.app.AppCompatActivity
109
import com.facebook.infer.annotation.Assertions
1110
import com.facebook.react.ReactActivity
12-
import com.facebook.react.ReactActivityDelegate
13-
import com.facebook.react.ReactNativeHost
1411
import com.facebook.react.ReactRootView
1512
import com.facebook.react.devsupport.DoubleTapReloadRecognizer
1613
import com.facebook.react.modules.core.PermissionListener
1714
import com.facebook.react.bridge.Callback
1815
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
1916
import com.facebook.react.modules.core.PermissionAwareActivity
2017

21-
class ReactNativeActivity : ReactActivity(), DefaultHardwareBackBtnHandler, PermissionAwareActivity {
22-
companion object {
23-
const val MODULE_NAME = "com.callstack.reactnativebrownfield.MODULE_NAME"
24-
}
18+
private const val MODULE_NAME = "com.callstack.reactnativebrownfield.ACTIVITY_MODULE_NAME"
2519

20+
class ReactNativeActivity : ReactActivity(), DefaultHardwareBackBtnHandler, PermissionAwareActivity {
2621
private var reactRootView: ReactRootView? = null
2722
private lateinit var moduleName: String
2823
private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer
@@ -55,6 +50,7 @@ class ReactNativeActivity : ReactActivity(), DefaultHardwareBackBtnHandler, Perm
5550

5651
if (BridgeManager.shared.reactNativeHost.hasInstance()) {
5752
BridgeManager.shared.reactNativeHost.reactInstanceManager?.onHostDestroy(this)
53+
BridgeManager.shared.reactNativeHost.clear()
5854
}
5955
}
6056

@@ -88,7 +84,7 @@ class ReactNativeActivity : ReactActivity(), DefaultHardwareBackBtnHandler, Perm
8884
return true
8985
}
9086
val didDoubleTapR = Assertions.assertNotNull(doubleTapReloadRecognizer)
91-
.didDoubleTapR(keyCode, this.getCurrentFocus())
87+
.didDoubleTapR(keyCode, this.currentFocus)
9288
if (didDoubleTapR) {
9389
BridgeManager.shared.reactNativeHost.reactInstanceManager.devSupportManager.handleReloadJS()
9490
return true
@@ -147,4 +143,13 @@ class ReactNativeActivity : ReactActivity(), DefaultHardwareBackBtnHandler, Perm
147143
}
148144
}
149145
}
146+
147+
companion object {
148+
@JvmStatic
149+
fun createReactActivityIntent(context: Context, moduleName: String): Intent {
150+
val intent = Intent(context, ReactNativeActivity::class.java)
151+
intent.putExtra(MODULE_NAME, moduleName)
152+
return intent
153+
}
154+
}
150155
}

android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfieldModule.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import com.facebook.react.bridge.ReactApplicationContext
44
import com.facebook.react.bridge.ReactContextBaseJavaModule
55
import com.facebook.react.bridge.ReactMethod
66

7-
87
class ReactNativeBrownfieldModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
98
companion object {
109
var shouldPopToNative: Boolean = false
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package com.callstack.reactnativebrownfield;
2+
3+
import android.annotation.TargetApi
4+
import android.os.Build
5+
import android.os.Bundle
6+
import androidx.fragment.app.Fragment
7+
import android.view.KeyEvent
8+
import android.view.LayoutInflater
9+
import android.view.View
10+
import android.view.ViewGroup
11+
import com.facebook.infer.annotation.Assertions
12+
import com.facebook.react.ReactRootView
13+
import com.facebook.react.bridge.Callback
14+
import com.facebook.react.common.LifecycleState
15+
import com.facebook.react.devsupport.DoubleTapReloadRecognizer
16+
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
17+
import com.facebook.react.modules.core.PermissionAwareActivity
18+
import com.facebook.react.modules.core.PermissionListener
19+
20+
private const val MODULE_NAME = "com.callstack.reactnativebrownfield.FRAGMENT_MODULE_NAME"
21+
22+
class ReactNativeFragment : Fragment(), PermissionAwareActivity {
23+
24+
private var reactRootView: ReactRootView? = null
25+
private lateinit var moduleName: String
26+
private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer
27+
private lateinit var permissionsCallback: Callback
28+
private var permissionListener: PermissionListener? = null
29+
30+
override fun onCreate(savedInstanceState: Bundle?) {
31+
super.onCreate(savedInstanceState)
32+
moduleName = arguments!!.getString(MODULE_NAME)!!
33+
34+
doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
35+
}
36+
37+
38+
override fun onCreateView(
39+
inflater: LayoutInflater,
40+
container: ViewGroup?,
41+
savedInstanceState: Bundle?
42+
): View {
43+
44+
reactRootView = ReactRootView(context)
45+
reactRootView?.startReactApplication(
46+
BridgeManager.shared.reactNativeHost.reactInstanceManager,
47+
moduleName,
48+
null
49+
)
50+
51+
return reactRootView!!
52+
}
53+
54+
override fun onResume() {
55+
super.onResume()
56+
if (BridgeManager.shared.reactNativeHost.hasInstance()) {
57+
BridgeManager.shared.reactNativeHost.reactInstanceManager?.onHostResume(
58+
activity,
59+
activity as DefaultHardwareBackBtnHandler
60+
)
61+
}
62+
}
63+
64+
override fun onPause() {
65+
super.onPause()
66+
if (BridgeManager.shared.reactNativeHost.hasInstance()) {
67+
BridgeManager.shared.reactNativeHost.reactInstanceManager?.onHostPause(
68+
activity
69+
)
70+
}
71+
}
72+
73+
override fun onDestroy() {
74+
super.onDestroy()
75+
reactRootView?.unmountReactApplication()
76+
reactRootView = null
77+
if (BridgeManager.shared.reactNativeHost.hasInstance()) {
78+
val reactInstanceMgr = BridgeManager.shared.reactNativeHost.reactInstanceManager
79+
80+
if (reactInstanceMgr.lifecycleState != LifecycleState.RESUMED) {
81+
reactInstanceMgr.onHostDestroy(activity)
82+
BridgeManager.shared.reactNativeHost.clear()
83+
}
84+
}
85+
}
86+
87+
override fun onRequestPermissionsResult(
88+
requestCode: Int,
89+
permissions: Array<String>,
90+
grantResults: IntArray
91+
) {
92+
permissionsCallback = Callback {
93+
if (permissionListener != null) {
94+
permissionListener?.onRequestPermissionsResult(
95+
requestCode,
96+
permissions,
97+
grantResults
98+
)
99+
100+
permissionListener = null
101+
}
102+
}
103+
}
104+
105+
override fun checkPermission(permission: String, pid: Int, uid: Int): Int {
106+
return activity!!.checkPermission(permission, pid, uid)
107+
}
108+
109+
@TargetApi(Build.VERSION_CODES.M)
110+
override fun checkSelfPermission(permission: String): Int {
111+
return activity!!.checkSelfPermission(permission)
112+
}
113+
114+
@TargetApi(Build.VERSION_CODES.M)
115+
override fun requestPermissions(
116+
permissions: Array<String>,
117+
requestCode: Int,
118+
listener: PermissionListener
119+
) {
120+
permissionListener = listener
121+
this.requestPermissions(permissions, requestCode)
122+
}
123+
124+
fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
125+
var handled = false
126+
if (BridgeManager.shared.reactNativeHost.useDeveloperSupport && BridgeManager.shared.reactNativeHost.hasInstance()) {
127+
if (keyCode == KeyEvent.KEYCODE_MENU) {
128+
BridgeManager.shared.reactNativeHost.reactInstanceManager.showDevOptionsDialog()
129+
handled = true
130+
}
131+
val didDoubleTapR = Assertions.assertNotNull(doubleTapReloadRecognizer)
132+
.didDoubleTapR(keyCode, activity?.currentFocus)
133+
if (didDoubleTapR) {
134+
BridgeManager.shared.reactNativeHost.reactInstanceManager.devSupportManager.handleReloadJS()
135+
handled = true
136+
}
137+
}
138+
return handled
139+
}
140+
141+
fun onBackPressed(backBtnHandler: DefaultHardwareBackBtnHandler) {
142+
if(ReactNativeBrownfieldModule.shouldPopToNative) {
143+
backBtnHandler.invokeDefaultOnBackPressed()
144+
} else if (BridgeManager.shared.reactNativeHost.hasInstance()) {
145+
BridgeManager.shared.reactNativeHost.reactInstanceManager.onBackPressed()
146+
}
147+
}
148+
149+
companion object {
150+
@JvmStatic
151+
fun createReactNativeFragment(moduleName: String): ReactNativeFragment {
152+
val fragment = ReactNativeFragment()
153+
val args = Bundle()
154+
args.putString(MODULE_NAME, moduleName)
155+
fragment.arguments = args
156+
return fragment
157+
}
158+
}
159+
160+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
3+
<application android:usesCleartextTraffic="true" tools:targetApi="28" />
4+
</manifest>
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
package="com.callstack.nativeexample">
44

55
<uses-permission android:name="android.permission.INTERNET"/>
6-
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
6+
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
77

88
<application
99
android:name=".MainApplication"
1010
android:allowBackup="true"
1111
android:icon="@mipmap/ic_launcher"
1212
android:label="@string/app_name"
1313
android:roundIcon="@mipmap/ic_launcher_round"
14-
android:usesCleartextTraffic="true" tools:targetApi="28"
1514
android:supportsRtl="true"
1615
android:theme="@style/AppTheme">
16+
<activity android:name=".ReactNativeFragmentActivity"/>
1717
<activity android:name=".MainActivity">
1818
<intent-filter>
1919
<action android:name="android.intent.action.MAIN"/>
2020

2121
<category android:name="android.intent.category.LAUNCHER"/>
2222
</intent-filter>
2323
</activity>
24-
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
25-
<activity android:name="com.callstack.reactnativebrownfield.ReactNativeActivity" />
24+
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
25+
<activity android:name="com.callstack.reactnativebrownfield.ReactNativeActivity"/>
2626
</application>
2727

2828
</manifest>
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
package com.callstack.nativeexample;
22

33
import android.content.Intent;
4-
import android.net.Uri;
5-
import android.os.Build;
64
import android.os.Bundle;
7-
import android.provider.Settings;
85
import android.view.View;
9-
import androidx.annotation.Nullable;
106
import androidx.appcompat.app.AppCompatActivity;
11-
import com.callstack.reactnativebrownfield.BridgeManagerJava;
127
import com.callstack.reactnativebrownfield.ReactNativeActivity;
138

149
public class MainActivity extends AppCompatActivity {
@@ -20,8 +15,15 @@ protected void onCreate(Bundle savedInstanceState) {
2015
}
2116

2217
public void startReactNative(View view) {
23-
Intent intent = new Intent(this, ReactNativeActivity.class);
24-
intent.putExtra(ReactNativeActivity.MODULE_NAME, "ReactNative");
18+
Intent intent = ReactNativeActivity.createReactActivityIntent(
19+
this,
20+
"ReactNative"
21+
);
22+
startActivity(intent);
23+
}
24+
25+
public void startReactNativeFragment(View view) {
26+
Intent intent = new Intent(this, ReactNativeFragmentActivity.class);
2527
startActivity(intent);
2628
}
2729
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.callstack.nativeexample;
2+
3+
import android.os.Bundle;
4+
import android.view.KeyEvent;
5+
import androidx.appcompat.app.AppCompatActivity;
6+
import androidx.fragment.app.Fragment;
7+
import com.callstack.reactnativebrownfield.ReactNativeFragment;
8+
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
9+
10+
import javax.annotation.Nullable;
11+
12+
public class ReactNativeFragmentActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {
13+
@Override
14+
public void invokeDefaultOnBackPressed() {
15+
super.onBackPressed();
16+
}
17+
18+
@Override
19+
public void onCreate(@Nullable Bundle savedInstanceState) {
20+
super.onCreate(savedInstanceState);
21+
getSupportActionBar().hide();
22+
setContentView(R.layout.activity_react_native_fragment);
23+
24+
if (savedInstanceState == null) {
25+
ReactNativeFragment reactNativeFragment = ReactNativeFragment.createReactNativeFragment("ReactNative");
26+
getSupportFragmentManager().beginTransaction()
27+
.add(R.id.container_main, reactNativeFragment)
28+
.commit();
29+
}
30+
}
31+
32+
@Override
33+
public boolean onKeyUp(int keyCode, KeyEvent event) {
34+
boolean handled = false;
35+
Fragment activeFragment = getSupportFragmentManager().findFragmentById(R.id.container_main);
36+
if (activeFragment instanceof ReactNativeFragment) {
37+
handled = ((ReactNativeFragment)activeFragment).onKeyUp(keyCode, event);
38+
}
39+
return handled || super.onKeyUp(keyCode, event);
40+
}
41+
42+
@Override
43+
public void onBackPressed() {
44+
Fragment activeFragment = getSupportFragmentManager().findFragmentById(R.id.container_main);
45+
if (activeFragment instanceof ReactNativeFragment) {
46+
((ReactNativeFragment)activeFragment).onBackPressed(this);
47+
} else {
48+
super.onBackPressed();
49+
}
50+
}
51+
}

example/native/android/app/src/main/res/layout/activity_main.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,12 @@
2525
app:layout_constraintTop_toBottomOf="@+id/textView" android:onClick="startReactNative"
2626
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="125dp"
2727
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="125dp"/>
28+
<Button
29+
android:text="OPEN REACT NATIVE IN FRAGMENT"
30+
android:layout_width="wrap_content"
31+
android:layout_height="wrap_content"
32+
android:id="@+id/button2" app:layout_constraintStart_toStartOf="parent"
33+
app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="30dp"
34+
app:layout_constraintTop_toBottomOf="@+id/button" android:onClick="startReactNativeFragment"/>
2835

2936
</androidx.constraintlayout.widget.ConstraintLayout>

0 commit comments

Comments
 (0)