Skip to content

Commit 0067365

Browse files
authored
Merge pull request #116 from callstack/feat/react-delegate-wrapper
feat: migrate to react delegate wrapper
2 parents 90f5a65 + b94719f commit 0067365

5 files changed

Lines changed: 114 additions & 78 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.callstack.reactnativebrownfield
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import com.facebook.react.ReactDelegate
6+
import com.facebook.react.ReactHost
7+
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
8+
9+
class ReactDelegateWrapper(
10+
private val activity: ComponentActivity?,
11+
private val reactHost: ReactHost,
12+
moduleName: String,
13+
launchOptions: Bundle?
14+
): ReactDelegate(activity, reactHost, moduleName, launchOptions){
15+
private lateinit var hardwareBackHandler: () -> Unit
16+
private val backBtnHandler = DefaultHardwareBackBtnHandler {
17+
hardwareBackHandler()
18+
}
19+
20+
/**
21+
* This is invoked when there is no more RN Stack to pop.
22+
* What it means that this is now the initial RN screen.
23+
*/
24+
fun setHardwareBackHandler(backHandler: () -> Unit) {
25+
hardwareBackHandler = backHandler
26+
}
27+
28+
override fun onHostResume() {
29+
reactHost.onHostResume(activity, backBtnHandler)
30+
}
31+
}

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

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import android.app.Application
44
import android.content.Context
55
import android.os.Bundle
66
import android.widget.FrameLayout
7+
import androidx.activity.OnBackPressedCallback
78
import androidx.fragment.app.FragmentActivity
89
import androidx.lifecycle.DefaultLifecycleObserver
910
import androidx.lifecycle.LifecycleOwner
10-
import com.facebook.react.ReactDelegate
1111
import com.facebook.react.ReactInstanceEventListener
1212
import com.facebook.react.ReactInstanceManager
1313
import com.facebook.react.ReactNativeHost
@@ -99,32 +99,44 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative
9999
context: Context,
100100
activity: FragmentActivity?,
101101
moduleName: String,
102+
reactDelegate: ReactDelegateWrapper? = null,
102103
launchOptions: Bundle? = null,
103104
): FrameLayout {
104105
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
105106
val reactHost = getDefaultReactHost(
106107
context,
107108
shared.reactNativeHost
108109
)
109-
val reactDelegate = ReactDelegate(activity, reactHost, moduleName, launchOptions)
110110

111-
activity?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
112-
override fun onResume(owner: LifecycleOwner) {
113-
reactDelegate.onHostResume()
114-
}
111+
val resolvedDelegate = reactDelegate ?: ReactDelegateWrapper(activity, reactHost, moduleName, launchOptions)
115112

116-
override fun onPause(owner: LifecycleOwner) {
117-
reactDelegate.onHostPause()
113+
val mBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
114+
override fun handleOnBackPressed() {
115+
// invoked for JS stack back navigation
116+
resolvedDelegate.onBackPressed()
118117
}
118+
}
119119

120-
override fun onDestroy(owner: LifecycleOwner) {
121-
reactDelegate.onHostDestroy()
122-
owner.lifecycle.removeObserver(this) // Cleanup to avoid leaks
123-
}
124-
})
120+
// Register back press callback
121+
activity?.onBackPressedDispatcher?.addCallback(mBackPressedCallback)
122+
// invoked on the last RN screen exit
123+
resolvedDelegate.setHardwareBackHandler {
124+
mBackPressedCallback.isEnabled = false
125+
activity?.onBackPressedDispatcher?.onBackPressed()
126+
}
125127

126-
reactDelegate.loadApp()
127-
return reactDelegate.reactRootView!!
128+
/**
129+
* When createView method is called in ReactNativeFragment, a reactDelegate
130+
* instance is required. In such a case, we use the lifeCycle events of the fragment.
131+
* When createView method is called elsewhere, then reactDelegate is not required.
132+
* In such a case, we set the lifeCycle observer.
133+
*/
134+
if (reactDelegate == null) {
135+
activity?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate))
136+
}
137+
138+
resolvedDelegate.loadApp()
139+
return resolvedDelegate.reactRootView!!
128140
}
129141

130142
val instanceManager: ReactInstanceManager? = shared.reactNativeHost?.reactInstanceManager
@@ -137,5 +149,22 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative
137149

138150
return reactView
139151
}
152+
153+
private fun getLifeCycleObserver(reactDelegate: ReactDelegateWrapper): DefaultLifecycleObserver {
154+
return object : DefaultLifecycleObserver {
155+
override fun onResume(owner: LifecycleOwner) {
156+
reactDelegate.onHostResume()
157+
}
158+
159+
override fun onPause(owner: LifecycleOwner) {
160+
reactDelegate.onHostPause()
161+
}
162+
163+
override fun onDestroy(owner: LifecycleOwner) {
164+
reactDelegate.onHostDestroy()
165+
owner.lifecycle.removeObserver(this) // Cleanup to avoid leaks
166+
}
167+
}
168+
}
140169
}
141170

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

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,60 @@ package com.callstack.reactnativebrownfield;
33
import android.annotation.TargetApi
44
import android.os.Build
55
import android.os.Bundle
6+
import android.util.Log
67
import android.view.KeyEvent
8+
import android.view.LayoutInflater
9+
import android.view.View
10+
import android.view.ViewGroup
711
import com.facebook.infer.annotation.Assertions
812
import com.facebook.react.ReactFragment
913
import com.facebook.react.ReactHost
1014
import com.facebook.react.ReactNativeHost
1115
import com.facebook.react.bridge.Callback
1216
import com.facebook.react.bridge.WritableMap
13-
import com.facebook.react.common.LifecycleState
1417
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
1518
import com.facebook.react.devsupport.DoubleTapReloadRecognizer
16-
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
1719
import com.facebook.react.modules.core.PermissionAwareActivity
1820
import com.facebook.react.modules.core.PermissionListener
1921

2022
class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
2123
private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer
2224
private lateinit var permissionsCallback: Callback
2325
private var permissionListener: PermissionListener? = null
26+
private lateinit var moduleName: String
2427

2528
override fun onCreate(savedInstanceState: Bundle?) {
26-
super.onCreate(savedInstanceState)
29+
/**
30+
* ReactFragment.onCreate will throw an exception if we do not provide arg_component_name as arguments.
31+
* We silently catch this exception. The reason is we want to invoke the super<Fragment>.onCreate in
32+
* ReactFragment. Then initialise the mReactDelegate with ReactDelegateWrapper instead of ReactDelegate.
33+
*
34+
* So we purposely force ReactFragment.onCreate to throw an exception, so that we can provide our own
35+
* implementation for mReactDelegate: ReactDelegateWrapper
36+
*/
37+
try{
38+
super.onCreate(savedInstanceState)
39+
} catch (e: IllegalStateException){
40+
Log.w("ReactNativeFragment", "ReactFragment threw due to missing arg_component_name: ${e.message} - This is an expected behaviour.")
41+
}
42+
43+
moduleName = arguments?.getString(ARG_MODULE_NAME)!!
44+
this.mReactDelegate = this.reactHost?.let {
45+
ReactDelegateWrapper(activity,
46+
it, moduleName, arguments?.getBundle("arg_launch_options"))
47+
}
48+
2749
doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
2850
}
2951

52+
override fun onCreateView(
53+
inflater: LayoutInflater,
54+
container: ViewGroup?,
55+
savedInstanceState: Bundle?
56+
): View {
57+
return ReactNativeBrownfield.shared.createView(this.requireContext(), activity, moduleName, this.mReactDelegate as ReactDelegateWrapper)
58+
}
59+
3060
override fun getReactHost(): ReactHost? {
3161
return activity?.let {
3262
getDefaultReactHost(
@@ -40,36 +70,6 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
4070
return ReactNativeBrownfield.shared.reactNativeHost
4171
}
4272

43-
override fun onResume() {
44-
super.onResume()
45-
if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
46-
ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager?.onHostResume(
47-
activity,
48-
activity as DefaultHardwareBackBtnHandler
49-
)
50-
}
51-
}
52-
53-
override fun onPause() {
54-
super.onPause()
55-
if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
56-
ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager?.onHostPause(
57-
activity
58-
)
59-
}
60-
}
61-
62-
override fun onDestroy() {
63-
super.onDestroy()
64-
if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
65-
val reactInstanceMgr = ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager
66-
67-
if (reactInstanceMgr.lifecycleState != LifecycleState.RESUMED) {
68-
reactInstanceMgr.onHostDestroy(activity)
69-
}
70-
}
71-
}
72-
7373
override fun onRequestPermissionsResult(
7474
requestCode: Int,
7575
permissions: Array<String>,
@@ -118,22 +118,16 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
118118
.didDoubleTapR(keyCode, it)
119119
}
120120
if (didDoubleTapR == true) {
121-
ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager.devSupportManager.handleReloadJS()
121+
reactDelegate.reload()
122122
handled = true
123123
}
124124
}
125125
return handled
126126
}
127127

128-
fun onBackPressed(backBtnHandler: DefaultHardwareBackBtnHandler) {
129-
if (ReactNativeBrownfieldModule.shouldPopToNative) {
130-
backBtnHandler.invokeDefaultOnBackPressed()
131-
} else if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
132-
ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager.onBackPressed()
133-
}
134-
}
135-
136128
companion object {
129+
private const val ARG_MODULE_NAME = "arg_module_name"
130+
137131
@JvmStatic
138132
@JvmOverloads
139133
fun createReactNativeFragment(
@@ -142,7 +136,7 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
142136
): ReactNativeFragment {
143137
val fragment = ReactNativeFragment()
144138
val args = Bundle()
145-
args.putString(ARG_COMPONENT_NAME, moduleName)
139+
args.putString(ARG_MODULE_NAME, moduleName)
146140
if (initialProps != null) {
147141
args.putBundle(ARG_LAUNCH_OPTIONS, initialProps)
148142
}

example/kotlin/app/src/main/java/com/callstack/kotlinexample/MainActivity.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ import androidx.navigation.compose.NavHost
2222
import androidx.navigation.compose.composable
2323
import androidx.navigation.compose.rememberNavController
2424
import com.callstack.reactnativebrownfield.ReactNativeBrownfield
25-
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
2625

27-
class MainActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler {
26+
class MainActivity : AppCompatActivity() {
2827
override fun onCreate(savedInstanceState: Bundle?) {
2928
super.onCreate(savedInstanceState)
3029

@@ -60,10 +59,6 @@ class MainActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler {
6059
}
6160
}
6261

63-
override fun invokeDefaultOnBackPressed() {
64-
super.onBackPressed()
65-
}
66-
6762
fun startReactNativeFragment() {
6863
val intent = Intent(this, ReactNativeFragmentActivity::class.java)
6964
startActivity(intent)

example/kotlin/app/src/main/java/com/callstack/kotlinexample/ReactNativeFragmentActivity.kt

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,8 @@ import android.os.Bundle
44
import android.view.KeyEvent
55
import androidx.appcompat.app.AppCompatActivity
66
import com.callstack.reactnativebrownfield.ReactNativeFragment
7-
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
87

9-
class ReactNativeFragmentActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler {
10-
override fun invokeDefaultOnBackPressed() {
11-
super.onBackPressed()
12-
}
8+
class ReactNativeFragmentActivity : AppCompatActivity() {
139

1410
public override fun onCreate(savedInstanceState: Bundle?) {
1511
super.onCreate(savedInstanceState)
@@ -31,13 +27,4 @@ class ReactNativeFragmentActivity : AppCompatActivity(), DefaultHardwareBackBtnH
3127
}
3228
return handled || super.onKeyUp(keyCode, event)
3329
}
34-
35-
override fun onBackPressed() {
36-
val activeFragment = supportFragmentManager.findFragmentById(R.id.container_main)
37-
if (activeFragment is ReactNativeFragment) {
38-
activeFragment.onBackPressed(this)
39-
} else {
40-
super.onBackPressed()
41-
}
42-
}
4330
}

0 commit comments

Comments
 (0)