Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<String, Any>) {
@JvmOverloads
fun initialize(application: Application, options: HashMap<String, Any>, onJSBundleLoaded: OnJSBundleLoaded? = null) {
val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(application) {

Expand All @@ -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<ReactPackage>) {
@JvmOverloads
fun initialize(application: Application, packages: List<ReactPackage>, 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()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
133 changes: 133 additions & 0 deletions docs/GUIDELINES.md
Original file line number Diff line number Diff line change
@@ -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)
}
```
Loading
Loading