diff --git a/.changeset/sharp-berries-relax.md b/.changeset/sharp-berries-relax.md new file mode 100644 index 000000000..5952d7f13 --- /dev/null +++ b/.changeset/sharp-berries-relax.md @@ -0,0 +1,6 @@ +--- +'@rnef/plugin-brownfield-android': minor +'rnef-docs': patch +--- + +correctly load and present RNViews for the respective RN arch diff --git a/packages/plugin-brownfield-android/template/android/build.gradle b/packages/plugin-brownfield-android/template/android/build.gradle index 8ac7f82eb..0c8957dc8 100644 --- a/packages/plugin-brownfield-android/template/android/build.gradle +++ b/packages/plugin-brownfield-android/template/android/build.gradle @@ -9,21 +9,13 @@ buildscript { } repositories { google() - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/callstack/big-fat-aar") - credentials { - username = System.getenv("GITHUB_USERNAME") - password = System.getenv("GITHUB_TOKEN") - } - } mavenCentral() } dependencies { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") - classpath("com.callstack.react:brownfield-gradle-plugin:0.2.0") + classpath("com.callstack.react:brownfield-gradle-plugin:0.3.0") } } diff --git a/packages/plugin-brownfield-android/template/android/helloworldreact/build.gradle.kts b/packages/plugin-brownfield-android/template/android/helloworldreact/build.gradle.kts index 4dd81f99f..2e3e83190 100644 --- a/packages/plugin-brownfield-android/template/android/helloworldreact/build.gradle.kts +++ b/packages/plugin-brownfield-android/template/android/helloworldreact/build.gradle.kts @@ -1,25 +1,31 @@ +import groovy.json.JsonOutput +import groovy.json.JsonSlurper + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("com.callstack.react.brownfield") `maven-publish` + id("com.facebook.react") +} + +react { + autolinkLibrariesWithApp() } repositories { mavenCentral() } -val appProject = project(":app") -val appBuildDir: Directory = appProject.layout.buildDirectory.get() -val moduleBuildDir: Directory = layout.buildDirectory.get() -val autolinkingJavaSources = "generated/autolinking/src/main/java" - android { namespace = "com.helloworldreact" compileSdk = 34 defaultConfig { minSdk = 24 + + buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", properties["newArchEnabled"].toString()) + buildConfigField("boolean", "IS_HERMES_ENABLED", properties["hermesEnabled"].toString()) } compileOptions { @@ -31,18 +37,9 @@ android { jvmTarget = "17" } - buildTypes { - release { - buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", properties["newArchEnabled"].toString()) - buildConfigField("boolean", "IS_HERMES_ENABLED", properties["hermesEnabled"].toString()) - } - } - - sourceSets { - getByName("main") { - assets.srcDirs("$appBuildDir/generated/assets/createBundleReleaseJsAndAssets") - res.srcDirs("$appBuildDir/generated/res/createBundleReleaseJsAndAssets") - java.srcDirs("$moduleBuildDir/$autolinkingJavaSources") + publishing { + multipleVariants { + allVariants() } } } @@ -53,20 +50,22 @@ publishing { groupId = "com.helloworldreact" artifactId = "helloworld" version = "0.0.1-local" - artifact("$moduleBuildDir/outputs/aar/helloworldreact-release.aar") + afterEvaluate { + from(components.getByName("default")) + } pom { withXml { - asNode().appendNode("dependencies").apply { - configurations.getByName("api").allDependencies.forEach { dependency -> - appendNode("dependency").apply { - appendNode("groupId", dependency.group) - appendNode("artifactId", dependency.name) - appendNode("version", dependency.version) - appendNode("scope", "compile") - } - } - } + /** + * As a result of `from(components.getByName("default")` all of the project + * dependencies are added to `pom.xml` file. We do not need the react-native + * third party dependencies to be a part of it as we embed those dependencies. + */ + val dependenciesNode = (asNode().get("dependencies") as groovy.util.NodeList).first() as groovy.util.Node + dependenciesNode.children() + .filterIsInstance() + .filter { (it.get("groupId") as groovy.util.NodeList).text() == rootProject.name } + .forEach { dependenciesNode.remove(it) } } } } @@ -78,23 +77,29 @@ publishing { } dependencies { - api("com.facebook.react:react-android:0.77.0-rc.2") - api("com.facebook.react:hermes-android:0.77.0-rc.2") + api("com.facebook.react:react-android:0.78.0") + api("com.facebook.react:hermes-android:0.78.0") } -tasks.register("copyAutolinkingSources") { - dependsOn(":app:generateAutolinkingPackageList") - from("$appBuildDir/$autolinkingJavaSources") - into("$moduleBuildDir/$autolinkingJavaSources") -} +val moduleBuildDir: Directory = layout.buildDirectory.get() -tasks.named("preBuild").configure{ - dependsOn("copyAutolinkingSources") - val buildType = when { - gradle.startParameter.taskNames.any { it.contains("Release", ignoreCase = true) } -> "Release" - else -> "Debug" - } - if (buildType == "Release") { - dependsOn(":app:createBundleReleaseJsAndAssets") +/** + * As a result of `from(components.getByName("default")` all of the project + * dependencies are added to `module.json` file. We do not need the react-native + * third party dependencies to be a part of it as we embed those dependencies. + */ +tasks.register("removeDependenciesFromModuleFile") { + doLast { + file("$moduleBuildDir/publications/mavenAar/module.json").run { + val json = inputStream().use { JsonSlurper().parse(it) as Map } + (json["variants"] as? List>)?.forEach { variant -> + (variant["dependencies"] as? MutableList>)?.removeAll { it["group"] == rootProject.name } + } + writer().use { it.write(JsonOutput.prettyPrint(JsonOutput.toJson(json))) } + } } } + +tasks.named("generateMetadataFileForMavenAarPublication") { + finalizedBy("removeDependenciesFromModuleFile") +} \ No newline at end of file diff --git a/packages/plugin-brownfield-android/template/android/helloworldreact/src/main/java/com/helloworldreact/RNViewFactory.kt b/packages/plugin-brownfield-android/template/android/helloworldreact/src/main/java/com/helloworldreact/RNViewFactory.kt index 5b077c744..2d27c36b2 100644 --- a/packages/plugin-brownfield-android/template/android/helloworldreact/src/main/java/com/helloworldreact/RNViewFactory.kt +++ b/packages/plugin-brownfield-android/template/android/helloworldreact/src/main/java/com/helloworldreact/RNViewFactory.kt @@ -3,22 +3,55 @@ package com.helloworldreact import android.content.Context import android.os.Bundle import android.widget.FrameLayout +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.facebook.react.ReactDelegate import com.facebook.react.ReactInstanceManager import com.facebook.react.ReactRootView object RNViewFactory { fun createFrameLayout( context: Context, - params: Bundle? = null, + activity: FragmentActivity, + initialParams: Bundle? = null, ): FrameLayout { + val componentName = "HelloWorld" + val reactHost = ReactNativeHostManager.shared.getReactHost() + + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + val reactDelegate = ReactDelegate(activity, reactHost!!, componentName, initialParams) + val lifecycleObserver = getLifeCycleObserver(reactDelegate) + + activity.lifecycle.addObserver(lifecycleObserver) + reactDelegate.loadApp() + return reactDelegate.reactRootView as FrameLayout + } + + val instanceManager: ReactInstanceManager? = ReactNativeHostManager.shared.getReactNativeHost()?.reactInstanceManager val reactView = ReactRootView(context) - val reactNativeHost = ReactNativeHostManager.shared.getReactNativeHost() - val instanceManager: ReactInstanceManager? = reactNativeHost?.reactInstanceManager reactView.startReactApplication( instanceManager, - "HelloWorld", - params, + componentName, + initialParams, ) return reactView } + + private fun getLifeCycleObserver(reactDelegate: ReactDelegate): 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) + } + } + } } \ No newline at end of file diff --git a/packages/plugin-brownfield-android/template/android/helloworldreact/src/main/java/com/helloworldreact/ReactNativeHostManager.kt b/packages/plugin-brownfield-android/template/android/helloworldreact/src/main/java/com/helloworldreact/ReactNativeHostManager.kt index 12af30361..875db9dcf 100644 --- a/packages/plugin-brownfield-android/template/android/helloworldreact/src/main/java/com/helloworldreact/ReactNativeHostManager.kt +++ b/packages/plugin-brownfield-android/template/android/helloworldreact/src/main/java/com/helloworldreact/ReactNativeHostManager.kt @@ -4,6 +4,10 @@ import android.app.Application import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader @@ -12,39 +16,49 @@ class ReactNativeHostManager { companion object { val shared: ReactNativeHostManager by lazy { ReactNativeHostManager() } private var reactNativeHost: ReactNativeHost? = null + private var reactHost: ReactHost? = null } fun getReactNativeHost(): ReactNativeHost? { return reactNativeHost } - fun initialize( - application: Application, - ) { - if (reactNativeHost == null) { - SoLoader.init(application, OpenSourceMergedSoMapping) - reactNativeHost = - object : DefaultReactNativeHost(application) { - override fun getUseDeveloperSupport(): Boolean { - return BuildConfig.DEBUG - } + fun getReactHost(): ReactHost? { + return reactHost + } + + fun initialize(application: Application) { + if (reactNativeHost != null && reactHost != null) { + return + } + + SoLoader.init(application, 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() + } + val reactApp = object : ReactApplication { + override val reactNativeHost: ReactNativeHost = + object : DefaultReactNativeHost(application) { override fun getPackages(): MutableList { - val packages: MutableList = PackageList(application).packages - return packages + return PackageList(application).packages } - override fun getJSMainModuleName(): String { - return "index" - } + override fun getJSMainModuleName(): String = "index" + override fun getBundleAssetName(): String = "index.android.bundle" - override fun getBundleAssetName(): String { - return "index.android.bundle" - } + override fun getUseDeveloperSupport() = BuildConfig.DEBUG override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED } + + override val reactHost: ReactHost + get() = getDefaultReactHost(application, reactNativeHost) } + + reactNativeHost = reactApp.reactNativeHost + reactHost = reactApp.reactHost } } diff --git a/website/docs/docs/brownfield/android.md b/website/docs/docs/brownfield/android.md index a8c0a5601..d6c042f62 100644 --- a/website/docs/docs/brownfield/android.md +++ b/website/docs/docs/brownfield/android.md @@ -10,13 +10,13 @@ First, we'll create a new Android library module in your React Native project. T 1. Open your React Native project's `android` folder in Android Studio 1. Go to File → New Module → Android Library - ![Create new module in Android Studio](/create_module.png) + ![Create new module in Android Studio](./assets/create_module.png) 1. After the sync completes, run your React Native app to make sure everything works 1. Test the build by running `./gradlew assembleRelease` in the android directory -## 2. Set Up the Fat AAR Gradle Plugin +## 2. Set Up the AAR Gradle Plugin -We need a special Gradle plugin to create a "fat" AAR that includes all dependencies. We'll use the `brownfield-gradle-plugin` plugin from Callstack. +We need a special Gradle plugin to create an AAR that includes all dependencies. We'll use the `brownfield-gradle-plugin` plugin from Callstack. 1. Add the gradle plugin dependency to your `android/build.gradle`: @@ -27,7 +27,7 @@ We need a special Gradle plugin to create a "fat" AAR that includes all dependen mavenCentral() } dependencies { - classpath("com.callstack.react:brownfield-gradle-plugin:0.2.0") + classpath("com.callstack.react:brownfield-gradle-plugin:0.3.0") } } ``` @@ -43,7 +43,8 @@ We need a special Gradle plugin to create a "fat" AAR that includes all dependen ``` 1. Run `./gradlew assembleRelease` to verify the setup - ![Verify AAR plugin setup](/verify_aar_plugin_setup.png) + + ![Verify AAR plugin setup](./assets/verify_aar_plugin_setup.png) ## 3. Add React Native Dependencies @@ -67,6 +68,10 @@ import android.app.Application import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader @@ -75,36 +80,50 @@ class ReactNativeHostManager { companion object { val shared: ReactNativeHostManager by lazy { ReactNativeHostManager() } private var reactNativeHost: ReactNativeHost? = null + private var reactHost: ReactHost? = null } fun getReactNativeHost(): ReactNativeHost? { return reactNativeHost } + fun getReactHost(): ReactHost? { + return reactHost + } + fun initialize(application: Application) { - if (reactNativeHost == null) { - SoLoader.init(application, OpenSourceMergedSoMapping) - reactNativeHost = object : DefaultReactNativeHost(application) { - override fun getUseDeveloperSupport(): Boolean { - return BuildConfig.DEBUG - } + if (reactNativeHost != null && reactHost != null) { + return + } - override fun getPackages(): MutableList { - return PackageList(application).packages - } + SoLoader.init(application, 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() + } - override fun getJSMainModuleName(): String { - return "index" - } + val reactApp = object : ReactApplication { + override val reactNativeHost: ReactNativeHost = + object : DefaultReactNativeHost(application) { + override fun getPackages(): MutableList { + return PackageList(application).packages + } + + override fun getJSMainModuleName(): String = "index" + override fun getBundleAssetName(): String = "index.android.bundle" - override fun getBundleAssetName(): String { - return "index.android.bundle" + override fun getUseDeveloperSupport() = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED } - override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED - override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED - } + override val reactHost: ReactHost + get() = getDefaultReactHost(application, reactNativeHost) } + + reactNativeHost = reactApp.reactNativeHost + reactHost = reactApp.reactHost } } ``` @@ -127,69 +146,16 @@ Add build configuration fields: ```gradle title="rnbrownfield/build.gradle.kts" {4-5} android { - buildTypes { - release { - buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", properties["newArchEnabled"].toString()) - buildConfigField("boolean", "IS_HERMES_ENABLED", properties["hermesEnabled"].toString()) - } - } -} -``` - -Set up autolinking: - -```gradle title="rnbrownfield/build.gradle.kts" {1-4,7-11,14-22} -val appProject = project(":app") -val appBuildDir: Directory = appProject.layout.buildDirectory.get() -val moduleBuildDir: Directory = layout.buildDirectory.get() -val autolinkingJavaSources = "generated/autolinking/src/main/java" - -android { - sourceSets { - getByName("main") { - java.srcDirs("$moduleBuildDir/$autolinkingJavaSources") - } - } -} - -tasks.register("copyAutolinkingSources") { - dependsOn(":app:generateAutolinkingPackageList") - from("$appBuildDir/$autolinkingJavaSources") - into("$moduleBuildDir/$autolinkingJavaSources") -} - -tasks.named("preBuild").configure { - dependsOn("copyAutolinkingSources") -} -``` - -## 5. Include the JavaScript Bundle - -Update your `rnbrownfield/build.gradle.kts` to include the JS bundle in the AAR: - -```gradle title="rnbrownfield/build.gradle.kts" {4,12-18} -android { - sourceSets { - getByName("main") { - assets.srcDirs("$appBuildDir/generated/assets/createBundleReleaseJsAndAssets") - java.srcDirs("$moduleBuildDir/$autolinkingJavaSources") - } - } -} + defaultConfig { + minSdk = 24 -tasks.named("preBuild").configure { - dependsOn("copyAutolinkingSources") - val buildType = when { - gradle.startParameter.taskNames.any { it.contains("Release", ignoreCase = true) } -> "Release" - else -> "Debug" - } - if (buildType == "Release") { - dependsOn(":app:createBundleReleaseJsAndAssets") + buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", properties["newArchEnabled"].toString()) + buildConfigField("boolean", "IS_HERMES_ENABLED", properties["hermesEnabled"].toString()) } } ``` -## 6. Create the React Native Entry Point +## 5. Create the React Native Entry Point Create a new file called `RNViewFactory.kt` to wrap your React Native UI in a `FrameLayout`: @@ -197,28 +163,61 @@ Create a new file called `RNViewFactory.kt` to wrap your React Native UI in a `F import android.content.Context import android.os.Bundle import android.widget.FrameLayout +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.facebook.react.ReactDelegate import com.facebook.react.ReactInstanceManager import com.facebook.react.ReactRootView object RNViewFactory { fun createFrameLayout( context: Context, - params: Bundle? = null, + activity: FragmentActivity, + initialParams: Bundle? = null, ): FrameLayout { + val componentName = "BrownFieldTest" + val reactHost = ReactNativeHostManager.shared.getReactHost() + + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + val reactDelegate = ReactDelegate(activity, reactHost!!, componentName, initialParams) + val lifecycleObserver = getLifeCycleObserver(reactDelegate) + + activity.lifecycle.addObserver(lifecycleObserver) + reactDelegate.loadApp() + return reactDelegate.reactRootView as FrameLayout + } + + val instanceManager: ReactInstanceManager? = ReactNativeHostManager.shared.getReactNativeHost()?.reactInstanceManager val reactView = ReactRootView(context) - val reactNativeHost = ReactNativeHostManager.shared.getReactNativeHost() - val instanceManager: ReactInstanceManager? = reactNativeHost?.reactInstanceManager reactView.startReactApplication( instanceManager, - "BrownfieldTest", - params, + componentName, + initialParams, ) return reactView } + + private fun getLifeCycleObserver(reactDelegate: ReactDelegate): 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) + } + } + } } ``` -## 7. Configure Maven Publishing +## 6. Configure Maven Publishing Add the Maven publish plugin to your `rnbrownfield/build.gradle.kts`: @@ -240,20 +239,22 @@ publishing { groupId = "com.callstack" artifactId = "rnbrownfield" version = "0.0.1-local" - artifact("$moduleBuildDir/outputs/aar/rnbrownfield-release.aar") + afterEvaluate { + from(components.getByName("default")) + } pom { withXml { - asNode().appendNode("dependencies").apply { - configurations.getByName("api").allDependencies.forEach { dependency -> - appendNode("dependency").apply { - appendNode("groupId", dependency.group) - appendNode("artifactId", dependency.name) - appendNode("version", dependency.version) - appendNode("scope", "compile") - } - } - } + /** + * As a result of `from(components.getByName("default")` all of the project + * dependencies are added to `pom.xml` file. We do not need the react-native + * third party dependencies to be a part of it as we embed those dependencies. + */ + val dependenciesNode = (asNode().get("dependencies") as groovy.util.NodeList).first() as groovy.util.Node + dependenciesNode.children() + .filterIsInstance() + .filter { (it.get("groupId") as groovy.util.NodeList).text() == rootProject.name } + .forEach { dependenciesNode.remove(it) } } } } @@ -263,9 +264,32 @@ publishing { mavenLocal() // Publishes to the local Maven repository (~/.m2/repository by default) } } + +val moduleBuildDir: Directory = layout.buildDirectory.get() + +/** + * As a result of `from(components.getByName("default")` all of the project + * dependencies are added to `module.json` file. We do not need the react-native + * third party dependencies to be a part of it as we embed those dependencies. + */ +tasks.register("removeDependenciesFromModuleFile") { + doLast { + file("$moduleBuildDir/publications/mavenAar/module.json").run { + val json = inputStream().use { JsonSlurper().parse(it) as Map } + (json["variants"] as? List>)?.forEach { variant -> + (variant["dependencies"] as? MutableList>)?.removeAll { it["group"] == rootProject.name } + } + writer().use { it.write(JsonOutput.prettyPrint(JsonOutput.toJson(json))) } + } + } +} + +tasks.named("generateMetadataFileForMavenAarPublication") { + finalizedBy("removeDependenciesFromModuleFile") +} ``` -## 8. Set up RNEF for AAR generation +## 7. Set up RNEF for AAR generation 1. Add `@rnef/plugin-brownfield-android` to your dependencies 1. Update your `rnef.config.mjs`: @@ -290,7 +314,7 @@ publishing { rnef publish-local:aar --module-name rnbrownfield ``` -## 9. Add the AAR to Your Android App +## 8. Add the AAR to Your Android App > Note: You'll need an existing Android app or create a new one in Android Studio. @@ -314,7 +338,7 @@ publishing { } ``` -## 10. Show the React Native UI +## 9. Show the React Native UI Create a new `RNAppFragment.kt`: @@ -383,3 +407,5 @@ class MainActivity : AppCompatActivity() { ``` Now you can run your app and test the React Native integration! + +>Note: `brownfield-gradle-plugin` copies `.so` files to the `lib` folder. Make sure to add `**/*.so` to your .gitignore file, as to not commit these .so files. The reason is they are auto-generated each time. \ No newline at end of file diff --git a/docs/assets/android-brownfield/create_module.png b/website/docs/docs/brownfield/assets/create_module.png similarity index 100% rename from docs/assets/android-brownfield/create_module.png rename to website/docs/docs/brownfield/assets/create_module.png diff --git a/docs/assets/android-brownfield/verify_aar_plugin_setup.png b/website/docs/docs/brownfield/assets/verify_aar_plugin_setup.png similarity index 100% rename from docs/assets/android-brownfield/verify_aar_plugin_setup.png rename to website/docs/docs/brownfield/assets/verify_aar_plugin_setup.png