diff --git a/android/src/main/java/com/callstack/reactnativebrownfield/ReactDelegateWrapper.kt b/android/src/main/java/com/callstack/reactnativebrownfield/ReactDelegateWrapper.kt new file mode 100644 index 00000000..1c1f6f86 --- /dev/null +++ b/android/src/main/java/com/callstack/reactnativebrownfield/ReactDelegateWrapper.kt @@ -0,0 +1,31 @@ +package com.callstack.reactnativebrownfield + +import android.os.Bundle +import androidx.activity.ComponentActivity +import com.facebook.react.ReactDelegate +import com.facebook.react.ReactHost +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler + +class ReactDelegateWrapper( + private val activity: ComponentActivity?, + private val reactHost: ReactHost, + moduleName: String, + launchOptions: Bundle? +): ReactDelegate(activity, reactHost, moduleName, launchOptions){ + private lateinit var hardwareBackHandler: () -> Unit + private val backBtnHandler = DefaultHardwareBackBtnHandler { + hardwareBackHandler() + } + + /** + * This is invoked when there is no more RN Stack to pop. + * What it means that this is now the initial RN screen. + */ + fun setHardwareBackHandler(backHandler: () -> Unit) { + hardwareBackHandler = backHandler + } + + override fun onHostResume() { + reactHost.onHostResume(activity, backBtnHandler) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt index dd38a63b..160c7770 100644 --- a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +++ b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt @@ -4,10 +4,10 @@ import android.app.Application import android.content.Context import android.os.Bundle import android.widget.FrameLayout +import androidx.activity.OnBackPressedCallback import androidx.fragment.app.FragmentActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import com.facebook.react.ReactDelegate import com.facebook.react.ReactInstanceEventListener import com.facebook.react.ReactInstanceManager import com.facebook.react.ReactNativeHost @@ -99,6 +99,7 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative context: Context, activity: FragmentActivity?, moduleName: String, + reactDelegate: ReactDelegateWrapper? = null, launchOptions: Bundle? = null, ): FrameLayout { if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { @@ -106,25 +107,36 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative context, shared.reactNativeHost ) - val reactDelegate = ReactDelegate(activity, reactHost, moduleName, launchOptions) - activity?.lifecycle?.addObserver(object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - reactDelegate.onHostResume() - } + val resolvedDelegate = reactDelegate ?: ReactDelegateWrapper(activity, reactHost, moduleName, launchOptions) - override fun onPause(owner: LifecycleOwner) { - reactDelegate.onHostPause() + val mBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + // invoked for JS stack back navigation + resolvedDelegate.onBackPressed() } + } - override fun onDestroy(owner: LifecycleOwner) { - reactDelegate.onHostDestroy() - owner.lifecycle.removeObserver(this) // Cleanup to avoid leaks - } - }) + // Register back press callback + activity?.onBackPressedDispatcher?.addCallback(mBackPressedCallback) + // invoked on the last RN screen exit + resolvedDelegate.setHardwareBackHandler { + mBackPressedCallback.isEnabled = false + activity?.onBackPressedDispatcher?.onBackPressed() + } - reactDelegate.loadApp() - return reactDelegate.reactRootView!! + /** + * When createView method is called in ReactNativeFragment, a reactDelegate + * instance is required. In such a case, we use the lifeCycle events of the fragment. + * When createView method is called elsewhere, then reactDelegate is not required. + * In such a case, we set the lifeCycle observer. + */ + if (reactDelegate == null) { + activity?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate)) + } + + resolvedDelegate.loadApp() + return resolvedDelegate.reactRootView!! } val instanceManager: ReactInstanceManager? = shared.reactNativeHost?.reactInstanceManager @@ -137,5 +149,22 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative return reactView } + + private fun getLifeCycleObserver(reactDelegate: ReactDelegateWrapper): DefaultLifecycleObserver { + return object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + reactDelegate.onHostResume() + } + + override fun onPause(owner: LifecycleOwner) { + reactDelegate.onHostPause() + } + + override fun onDestroy(owner: LifecycleOwner) { + reactDelegate.onHostDestroy() + owner.lifecycle.removeObserver(this) // Cleanup to avoid leaks + } + } + } } diff --git a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt index 2457f173..cef42544 100644 --- a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +++ b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt @@ -3,17 +3,19 @@ package com.callstack.reactnativebrownfield; import android.annotation.TargetApi import android.os.Build import android.os.Bundle +import android.util.Log import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import com.facebook.infer.annotation.Assertions import com.facebook.react.ReactFragment import com.facebook.react.ReactHost import com.facebook.react.ReactNativeHost import com.facebook.react.bridge.Callback import com.facebook.react.bridge.WritableMap -import com.facebook.react.common.LifecycleState import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.devsupport.DoubleTapReloadRecognizer -import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler import com.facebook.react.modules.core.PermissionAwareActivity import com.facebook.react.modules.core.PermissionListener @@ -21,12 +23,40 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity { private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer private lateinit var permissionsCallback: Callback private var permissionListener: PermissionListener? = null + private lateinit var moduleName: String override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + /** + * ReactFragment.onCreate will throw an exception if we do not provide arg_component_name as arguments. + * We silently catch this exception. The reason is we want to invoke the super.onCreate in + * ReactFragment. Then initialise the mReactDelegate with ReactDelegateWrapper instead of ReactDelegate. + * + * So we purposely force ReactFragment.onCreate to throw an exception, so that we can provide our own + * implementation for mReactDelegate: ReactDelegateWrapper + */ + try{ + super.onCreate(savedInstanceState) + } catch (e: IllegalStateException){ + Log.w("ReactNativeFragment", "ReactFragment threw due to missing arg_component_name: ${e.message} - This is an expected behaviour.") + } + + moduleName = arguments?.getString(ARG_MODULE_NAME)!! + this.mReactDelegate = this.reactHost?.let { + ReactDelegateWrapper(activity, + it, moduleName, arguments?.getBundle("arg_launch_options")) + } + doubleTapReloadRecognizer = DoubleTapReloadRecognizer() } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ReactNativeBrownfield.shared.createView(this.requireContext(), activity, moduleName, this.mReactDelegate as ReactDelegateWrapper) + } + override fun getReactHost(): ReactHost? { return activity?.let { getDefaultReactHost( @@ -40,36 +70,6 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity { return ReactNativeBrownfield.shared.reactNativeHost } - override fun onResume() { - super.onResume() - if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) { - ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager?.onHostResume( - activity, - activity as DefaultHardwareBackBtnHandler - ) - } - } - - override fun onPause() { - super.onPause() - if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) { - ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager?.onHostPause( - activity - ) - } - } - - override fun onDestroy() { - super.onDestroy() - if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) { - val reactInstanceMgr = ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager - - if (reactInstanceMgr.lifecycleState != LifecycleState.RESUMED) { - reactInstanceMgr.onHostDestroy(activity) - } - } - } - override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, @@ -118,22 +118,16 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity { .didDoubleTapR(keyCode, it) } if (didDoubleTapR == true) { - ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager.devSupportManager.handleReloadJS() + reactDelegate.reload() handled = true } } return handled } - fun onBackPressed(backBtnHandler: DefaultHardwareBackBtnHandler) { - if (ReactNativeBrownfieldModule.shouldPopToNative) { - backBtnHandler.invokeDefaultOnBackPressed() - } else if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) { - ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager.onBackPressed() - } - } - companion object { + private const val ARG_MODULE_NAME = "arg_module_name" + @JvmStatic @JvmOverloads fun createReactNativeFragment( @@ -142,7 +136,7 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity { ): ReactNativeFragment { val fragment = ReactNativeFragment() val args = Bundle() - args.putString(ARG_COMPONENT_NAME, moduleName) + args.putString(ARG_MODULE_NAME, moduleName) if (initialProps != null) { args.putBundle(ARG_LAUNCH_OPTIONS, initialProps) } diff --git a/example/kotlin/app/src/main/java/com/callstack/kotlinexample/MainActivity.kt b/example/kotlin/app/src/main/java/com/callstack/kotlinexample/MainActivity.kt index 0ffe9ccf..21a6cafe 100644 --- a/example/kotlin/app/src/main/java/com/callstack/kotlinexample/MainActivity.kt +++ b/example/kotlin/app/src/main/java/com/callstack/kotlinexample/MainActivity.kt @@ -22,9 +22,8 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.callstack.reactnativebrownfield.ReactNativeBrownfield -import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler -class MainActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler { +class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -60,10 +59,6 @@ class MainActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler { } } - override fun invokeDefaultOnBackPressed() { - super.onBackPressed() - } - fun startReactNativeFragment() { val intent = Intent(this, ReactNativeFragmentActivity::class.java) startActivity(intent) diff --git a/example/kotlin/app/src/main/java/com/callstack/kotlinexample/ReactNativeFragmentActivity.kt b/example/kotlin/app/src/main/java/com/callstack/kotlinexample/ReactNativeFragmentActivity.kt index e97c1600..e1dc23bf 100644 --- a/example/kotlin/app/src/main/java/com/callstack/kotlinexample/ReactNativeFragmentActivity.kt +++ b/example/kotlin/app/src/main/java/com/callstack/kotlinexample/ReactNativeFragmentActivity.kt @@ -4,12 +4,8 @@ import android.os.Bundle import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity import com.callstack.reactnativebrownfield.ReactNativeFragment -import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler -class ReactNativeFragmentActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler { - override fun invokeDefaultOnBackPressed() { - super.onBackPressed() - } +class ReactNativeFragmentActivity : AppCompatActivity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -31,13 +27,4 @@ class ReactNativeFragmentActivity : AppCompatActivity(), DefaultHardwareBackBtnH } return handled || super.onKeyUp(keyCode, event) } - - override fun onBackPressed() { - val activeFragment = supportFragmentManager.findFragmentById(R.id.container_main) - if (activeFragment is ReactNativeFragment) { - activeFragment.onBackPressed(this) - } else { - super.onBackPressed() - } - } }