diff --git a/android/build.gradle b/android/build.gradle index 4876d1c4..d3850b23 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,3 +1,15 @@ +import groovy.json.JsonSlurper + +def reactNativeVersion = null +def packageJsonFile = rootProject.file("../node_modules/react-native/package.json") + +if (packageJsonFile.exists()) { + def json = new JsonSlurper().parse(packageJsonFile) + reactNativeVersion = json.version +} else { + reactNativeVersion = "unknown" +} + buildscript { // Buildscript is evaluated before everything else so we can't use getExtOrDefault def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["RNBrownfield_kotlinVersion"] @@ -64,6 +76,7 @@ android { targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() buildConfigField "boolean", "IS_HERMES_ENABLED", isHermesEnabled().toString() + buildConfigField "String", "RN_VERSION", "\"$reactNativeVersion\"" } buildFeatures { diff --git a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt index 160c7770..ae92d73f 100644 --- a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +++ b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt @@ -8,6 +8,7 @@ import androidx.activity.OnBackPressedCallback import androidx.fragment.app.FragmentActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner +import com.callstack.reactnativebrownfield.utils.VersionUtils import com.facebook.react.ReactInstanceEventListener import com.facebook.react.ReactInstanceManager import com.facebook.react.ReactNativeHost @@ -21,10 +22,17 @@ import java.util.concurrent.atomic.AtomicBoolean import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost -interface InitializedCallback { +fun interface OnJSBundleLoaded { operator fun invoke(initialized: Boolean) } +/** + * The threshold RN version based on which we decide whether to + * load JNI libs or not. We only load JNI libs on version less + * than this. + */ +private const val RN_THRESHOLD_VERSION = "0.80.0" + class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNativeHost) { companion object { private lateinit var instance: ReactNativeBrownfield @@ -33,16 +41,34 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative @JvmStatic val shared: ReactNativeBrownfield get() = instance + private fun loadNativeLibs (application: Application) { + val rnVersion = BuildConfig.RN_VERSION + + if (VersionUtils.isVersionLessThan(rnVersion, RN_THRESHOLD_VERSION)) { + SoLoader.init(application.applicationContext, OpenSourceMergedSoMapping) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + } + } + @JvmStatic - fun initialize(application: Application, rnHost: ReactNativeHost) { + @JvmOverloads + fun initialize(application: Application, rnHost: ReactNativeHost, onJSBundleLoaded: OnJSBundleLoaded? = null) { if (!initialized.getAndSet(true)) { + loadNativeLibs(application) instance = ReactNativeBrownfield(rnHost) - SoLoader.init(application.applicationContext, OpenSourceMergedSoMapping) + + preloadReactNative { + onJSBundleLoaded?.invoke(true) + } } } @JvmStatic - fun initialize(application: Application, options: HashMap) { + @JvmOverloads + fun initialize(application: Application, options: HashMap, onJSBundleLoaded: OnJSBundleLoaded? = null) { val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(application) { @@ -61,37 +87,27 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED } - initialize(application, reactNativeHost) + initialize(application, reactNativeHost, onJSBundleLoaded) } @JvmStatic - fun initialize(application: Application, packages: List) { + @JvmOverloads + fun initialize(application: Application, packages: List, onJSBundleLoaded: OnJSBundleLoaded? = null) { val options = hashMapOf("packages" to packages, "mainModuleName" to "index") - initialize(application, options) + initialize(application, options, onJSBundleLoaded) } - - } - - fun startReactNative(callback: InitializedCallback?) { - startReactNative { callback?.invoke(it) } - } - - @JvmName("startReactNativeKotlin") - fun startReactNative(callback: ((initialized: Boolean) -> Unit)?) { - reactNativeHost.reactInstanceManager.addReactInstanceEventListener(object : - ReactInstanceEventListener { - override fun onReactContextInitialized(reactContext: ReactContext) { - callback?.let { it(true) } - reactNativeHost.reactInstanceManager.removeReactInstanceEventListener(this) - } - }) - reactNativeHost.reactInstanceManager?.createReactContextInBackground() - - if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - // If you opted-in for the New Architecture, we load the native entry point for this app. - load() + private fun preloadReactNative(callback: ((Boolean) -> Unit)) { + val reactInstanceManager = shared.reactNativeHost.reactInstanceManager + reactInstanceManager.addReactInstanceEventListener(object : + ReactInstanceEventListener { + override fun onReactContextInitialized(reactContext: ReactContext) { + callback(true) + reactInstanceManager.removeReactInstanceEventListener(this) + } + }) + reactInstanceManager?.createReactContextInBackground() } } diff --git a/android/src/main/java/com/callstack/reactnativebrownfield/utils/VersionUtils.kt b/android/src/main/java/com/callstack/reactnativebrownfield/utils/VersionUtils.kt new file mode 100644 index 00000000..e9da5545 --- /dev/null +++ b/android/src/main/java/com/callstack/reactnativebrownfield/utils/VersionUtils.kt @@ -0,0 +1,17 @@ +package com.callstack.reactnativebrownfield.utils + +object VersionUtils { + fun isVersionLessThan(version: String, threshold: String): Boolean { + val versionParts = version.split(".").map { it.toIntOrNull() ?: 0 } + val thresholdParts = threshold.split(".").map { it.toIntOrNull() ?: 0 } + + val maxLength = maxOf(versionParts.size, thresholdParts.size) + for (i in 0 until maxLength) { + val vPart = versionParts.getOrNull(i) ?: 0 + val tPart = thresholdParts.getOrNull(i) ?: 0 + if (vPart != tPart) return vPart < tPart + } + + return false // equal versions are not less than + } +} \ No newline at end of file diff --git a/docs/GUIDELINES.md b/docs/GUIDELINES.md new file mode 100644 index 00000000..942d5016 --- /dev/null +++ b/docs/GUIDELINES.md @@ -0,0 +1,133 @@ +Here you can find the guidelines for standard brownfield approach and advanced use cases. + +### Standard Brownfield + +As a golden rule of standard brownfield with react-native, your native App should never have to interact directly +with react-native APIs. There are various reasons that doing the opposite is discouraged. + +- If you have different teams working on RN brownfield and the native App and you distribute your AAR/XCFramework to the native +team. They can use the APIs from those artifacts and interact with them. However, if the native team have to import a react native +API, say `PackageList` then the rule of brownfield is being violated. The native team have native developers and they should not need +to worry about and interact with react-native directly. All of the abstraction should be handled within your artifacts. + +- If your native App interacts with react-native directly then you could imagine how complicated the codebase would be. The native App +should follow and worry about their native APIs rather than interacting with react-native. If in future, some react-native APIs needs to +be changed or refactored, then the effort would be cumbersome. On the contrast, if your native App was interacting with your artifact only +then the native App need not to worry about what happens internally. This makes things simpler for the native App team. + +- If the native App team is using your artifact and any build, compile time or run time issue arises the stack trace would lead to your artifact +and making it simpler for the teams to focus on their area only. On the contrary, if the native App team would interact with react-native directly, +then any related issues would be time consuming for that team to figure out the root cause and then delegate to the team managing RN flows. + +Building upon the above points, below is how your brownfield implementation should look like if you're using `react-native-brownfield`: + +- In your brownfield android library or iOS xcframework, create a class following the facade pattern. The role of this class would be to encapsulate the +initialization of `react-native-brownfield` by not asking the native App to interact with `react-native` directly. Below is how it would look like: + +```kt +// Your artifact +import com.facebook.react.PackageList +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative + +class ReactNativeHostManager { + companion object { + fun initialize(application: Application) { + loadReactNative(application) // imported from autogenerated ReactNativeApplicationEntryPoint + val packages = PackageList(application).packages // imported from autogenerated PackageList + + ReactNativeBrownfield.initialize(application, packages) + } + } +} +``` + +Then the native App only needs to call `initialize` like so: + +```kt +// native App +ReactNativeHostManager.initialize(application) +``` + +If you do not follow this approach, then the usage in the native App would look like this: + +```kt +// native App +import com.facebook.react.PackageList +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative + + +loadReactNative(application) +val packages = PackageList(this).getPackages() +ReactNativeHostManager.initialize(application) +``` + +As you can see the issue here is that now we are mixing the native App with react-native APIs, which is discouraged. If we stick to +the above approach then the native App is free of interacting with react-native directly. + + +### Advanced Use Cases: + +If you built on top of the above approach, then you can rather easily scale it to incorporate advanced use cases. We will discuss here only one +case but you can extend it to your need as the gist remains the same. + +Consider you need to present a few native App's existing screens from react-native. Which means you need a communication way so that you can tell +the native App to present this screen. Let's see how we can achieve this: + +In your `ReactNativeHostManager` add the following method: + +```kt +// Your artifact +class ReactNativeHostManager { + companion object { + fun createView( + context: Context, + activity: FragmentActivity?, + moduleName: String, + launchOptions: Bundle? = null, + eventHandler: (String) -> Unit = {} + ): FrameLayout { + EventHandlerRegistry.register(moduleName, eventHandler) // Later invoke this event or callback to perform the navigation + return ReactNativeBrownfield.shared.createView(context, activity, moduleName, launchOptions) + } + } +} +``` + +What happens here is that the `createView` method now accepts an optional callback or eventHandler argument. The native App will rely on this eventHandler +to perform the navigation or to receive any events from the react-native side. The usage in the native App would look like below: + +```kt +// native App +ReactNativeHostManager.createView(context, activity, "Enterprise") { + if (it == "navigate_to_faq") { + // present faq fragment + } +} +``` + +HINT: To achieve this you will need to write some native code in your artifact to trigger an event from JS and then forward that event by invoking the eventHandler. You can wire up a native module and expose it to JS. Now, when you need to present FAQ screen from the native App, you invoke that native module method which forwards the event to eventHandler. The gist is below: + +```JS +// Your artifact + +// handlerId is the name of RN module loaded, eg: Enterprise +RNEventHandler.sendEvent("navigate_to_faq", handlerId); +``` + +```kt +// Your artifact + +// RNEventHandler +fun sendEvent(event: String, handlerId: String) { + EventHandlerRegistry.sendEvent(handlerId, event) +} +``` + +```kt +// Your artifact + +// EventHandlerRegistry +fun sendEvent(event: String, handlerId: String) { + eventHandlers[handlerId].invoke(event) +} +``` \ No newline at end of file diff --git a/docs/JAVA.md b/docs/JAVA.md index 336fe199..67367522 100644 --- a/docs/JAVA.md +++ b/docs/JAVA.md @@ -19,6 +19,29 @@ buildscript { } ``` +### React Native >= 0.80.0 (extra step) + +With react-native >= 0.80.0, an auto-generated file was added which is responsible to load your App's native libs. If you're consuming this library in a RN project, then +you will have this file `ReactNativeApplicationEntryPoint` available. If you're consuming this library in a RN android library which is backed by +`com.callstack.react:brownfield-gradle-plugin`, then this file will also be available. + +Below is the code you need to add before you call `RNBrownfield.initialize`: + +```java +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative + +loadReactNative(application); +RNBrownfield.initialize(application, packages); +``` + +
+
+ +> Note: Previously, you were required to implement `DefaultHardwareBackBtnListener` in your calling Activity. Now with > 1.1.0 you are not required to do that step. +If you're upgrading to the latest version then you can safely remove that interface implementation from your calling Activity. + +
+ ### API Reference #### `ReactNativeBrownfield` @@ -45,6 +68,7 @@ Params: | rnHost | No* | `ReactNativeHost` | An instance of [ReactNativeHost](https://bit.ly/2ZnwgnA). | | packages | No* | `List` | List of your React Native Native modules. | | options | No* | `HashMap` | Map of initial options. __Options listed below.__ | +| onJSBundleLoaded | No* | `OnJSBundleLoaded` | Callback invoked after JS bundle is fully loaded. | > * - Those fields aren't itself required, but at least one of them is. See examples below. @@ -78,12 +102,24 @@ private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { }; ReactNativeBrownfield.initialize(this, mReactNativeHost); + +OR + +ReactNativeBrownfield.initialize(this, mReactNativeHost, initialized -> { + // JS bundle loaded +}); ``` ```java List packages = new PackageList(this).getPackages(); ReactNativeBrownfield.initialize(this, packages); + +OR + +ReactNativeBrownfield.initialize(this, packages, initialized -> { + // JS bundle loaded +}); ``` ```java @@ -93,6 +129,12 @@ options.put("packages", packages); options.put("mainModuleName", "example/index"); ReactNativeBrownfield.initialize(this, options); + +OR + +ReactNativeBrownfield.initialize(this, options, initialized -> { + // JS bundle loaded +}); ``` --- @@ -119,30 +161,6 @@ ReactNativeBrownfield.getShared() **Methods:** -`startReactNative` - -Starts React Native, produces an instance of react native. You can use it to initialize React Native in your app. - -Params: - -| Param | Required | Type | Description | -| ----------------------- | -------- | ------------- | ----------------------------------------------------- | -| startReactNative | No | `Lambda` | Callback invoked after JS bundle is fully loaded. | - -Examples: - -```java -ReactNativeBrownfield.getShared().startReactNative(); -``` - -```java -ReactNativeBrownfield.getShared().startReactNative(init -> { - Log.d("loaded", "React Native loaded"); -}); -``` - ---- - `createView` Creates a React Native view with a given module name. It automatically uses an instance of React Native created in `startReactNative` method. This is useful when embedding React Native views directly in your native layouts. diff --git a/docs/KOTLIN.md b/docs/KOTLIN.md index c17f6631..d30eec9e 100644 --- a/docs/KOTLIN.md +++ b/docs/KOTLIN.md @@ -2,6 +2,29 @@ React Native Brownfield provides first-class support for Kotlin. +### React Native >= 0.80.0 (extra step) + +With react-native >= 0.80.0, an auto-generated file was added which is responsible to load your App's native libs. If you're consuming this library in a RN project, then +you will have this file `ReactNativeApplicationEntryPoint` available. If you're consuming this library in a RN android library which is backed by +`com.callstack.react:brownfield-gradle-plugin`, then this file will also be available. + +Below is the code you need to add before you call `RNBrownfield.initialize`: + +```kt +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative + +loadReactNative(application) +RNBrownfield.initialize(application, packages) +``` + +
+
+ +> Note: Previously, you were required to implement `DefaultHardwareBackBtnHandler` in your calling Activity. Now with > 1.1.0 you are not required to do that step. +If you're upgrading to the latest version then you can safely remove that interface implementation from your calling Activity. + +
+ ### API Reference #### `ReactNativeBrownfield` @@ -28,6 +51,7 @@ Params: | rnHost | No* | `ReactNativeHost` | An instance of [ReactNativeHost](https://bit.ly/2ZnwgnA). | | packages | No* | `List` | List of your React Native Native modules. | | options | No* | `HashMap` | Map of initial options. __Options listed below.__ | +| onJSBundleLoaded | No* | `OnJSBundleLoaded` | Callback invoked after JS bundle is fully loaded. | > * - Those fields aren't itself required, but at least one of them is. See examples below. @@ -56,12 +80,24 @@ val mReactNativeHost = object : ReactNativeHost(application) { } ReactNativeBrownfield.initialize(this, mReactNativeHost) + +OR + +ReactNativeBrownfield.initialize(this, mReactNativeHost) { + // onJSBundleLoaded +} ``` ```kotlin val packages = PackageList(this).getPackages() ReactNativeBrownfield.initialize(this, packages) + +OR + +ReactNativeBrownfield.initialize(this, packages) { + // onJSBundleLoaded +} ``` ```kotlin @@ -72,6 +108,12 @@ val options = hashMapOf( ) ReactNativeBrownfield.initialize(this, options) + +OR + +ReactNativeBrownfield.initialize(this, options) { + // onJSBundleLoaded +} ``` --- @@ -98,30 +140,6 @@ ReactNativeBrownfield.shared **Methods:** -`startReactNative` - -Starts React Native, produces an instance of React Native. You can use it to initialize React Native in your app. - -Params: - -| Param | Required | Type | Description | -| ----------------------- | -------- | ------------- | ----------------------------------------------------- | -| startReactNative | No | `(loaded: boolean) -> Unit` | Callback invoked after JS bundle is fully loaded. | - -Examples: - -```kotlin -ReactNativeBrownfield.shared.startReactNative() -``` - -```kotlin -ReactNativeBrownfield.shared.startReactNative { - Log.d("loaded", "React Native loaded"); -} -``` - ---- - `createView` Creates a React Native view with a given module name. It automatically uses an instance of React Native created in `startReactNative` method. This is useful when embedding React Native views directly in your native layouts or Jetpack Compose UI. diff --git a/example/kotlin/app/src/main/java/com/callstack/kotlinexample/MainApplication.kt b/example/kotlin/app/src/main/java/com/callstack/kotlinexample/MainApplication.kt index e5f38348..926068dd 100644 --- a/example/kotlin/app/src/main/java/com/callstack/kotlinexample/MainApplication.kt +++ b/example/kotlin/app/src/main/java/com/callstack/kotlinexample/MainApplication.kt @@ -10,8 +10,7 @@ class MainApplication : Application() { super.onCreate() val packages = PackageList(this).packages - ReactNativeBrownfield.initialize(this, packages) - ReactNativeBrownfield.shared.startReactNative { + ReactNativeBrownfield.initialize(this, packages) { Log.d("test", "test") } }