From c4e7d9c41e144a042f7bfcfd88231240391c9355 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 16 Oct 2026 05:02:05 -0700 Subject: [PATCH 1/2] fix: loadReactNative from RNB CLI --- .changeset/olive-pumas-shave.md | 15 +++++++++++++++ .../react-native-brownfield/java.mdx | 9 +++++---- .../react-native-brownfield/kotlin.mdx | 9 +++++---- docs/docs/docs/getting-started/android.mdx | 3 ++- docs/docs/docs/guides/guidelines.mdx | 12 +++++------- .../__tests__/withAndroidModuleFiles.test.ts | 7 +++++-- .../android/ReactNativeHostManager.vanilla.kt | 16 +++++++++------- 7 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 .changeset/olive-pumas-shave.md diff --git a/.changeset/olive-pumas-shave.md b/.changeset/olive-pumas-shave.md new file mode 100644 index 000000000..e376c560e --- /dev/null +++ b/.changeset/olive-pumas-shave.md @@ -0,0 +1,15 @@ +--- +'@callstack/react-native-brownfield': patch +--- + +fix(android): stop the scaffolded `ReactNativeHostManager` from loading React Native twice + +The `ReactNativeHostManager.kt` generated by `npm create @callstack/react-native-brownfield` for React Native CLI projects called `loadReactNative(application)` and then `ReactNativeBrownfield.initialize(...)`. Both run `DefaultNewArchitectureEntryPoint.load()`, which overrides the React Native feature flags, so the app crashed on startup with `Feature flags cannot be overridden more than once`. + +The generated host manager now: + +- no longer calls `loadReactNative` (`ReactNativeBrownfield.initialize` already loads the native libraries and feature flags exactly once); +- passes the `ReactHost` as a factory to the non-deprecated `initialize` overload, so it is built after `SoLoader.init` instead of crashing with `SoLoader.init() not yet called`; +- keeps the one behavior `loadReactNative` provided on the side by flagging edge-to-edge when `IS_EDGE_TO_EDGE_ENABLED` is set. + +Fixes #492. diff --git a/docs/docs/docs/api-reference/react-native-brownfield/java.mdx b/docs/docs/docs/api-reference/react-native-brownfield/java.mdx index 23649a682..cb9b54164 100644 --- a/docs/docs/docs/api-reference/react-native-brownfield/java.mdx +++ b/docs/docs/docs/api-reference/react-native-brownfield/java.mdx @@ -23,15 +23,16 @@ buildscript { 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 `ReactNativeBrownfield.initialize`: +You don't need to call `loadReactNative` yourself. `ReactNativeBrownfield.initialize` loads the native libraries and the React Native feature flags on its own: ```java -import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative - -loadReactNative(application); ReactNativeBrownfield.initialize(application, packages); ``` +:::warning Important +Do not call `loadReactNative(application)` before `ReactNativeBrownfield.initialize`. Both load the React Native feature flags, and feature flags cannot be overridden more than once, so doing so crashes the app with `Feature flags cannot be overridden more than once`. If your app calls `loadReactNative` in `Application.onCreate` for its own React Native usage, don't initialize Brownfield on top of it in the same process. +::: + :::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. ::: diff --git a/docs/docs/docs/api-reference/react-native-brownfield/kotlin.mdx b/docs/docs/docs/api-reference/react-native-brownfield/kotlin.mdx index 7ae7f55aa..ec1ca99ee 100644 --- a/docs/docs/docs/api-reference/react-native-brownfield/kotlin.mdx +++ b/docs/docs/docs/api-reference/react-native-brownfield/kotlin.mdx @@ -6,15 +6,16 @@ React Native Brownfield provides first-class support for Kotlin. 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 `ReactNativeBrownfield.initialize`: +You don't need to call `loadReactNative` yourself. `ReactNativeBrownfield.initialize` loads the native libraries and the React Native feature flags on its own: ```kt -import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative - -loadReactNative(application) ReactNativeBrownfield.initialize(application, packages) ``` +:::warning Important +Do not call `loadReactNative(application)` before `ReactNativeBrownfield.initialize`. Both load the React Native feature flags, and feature flags cannot be overridden more than once, so doing so crashes the app with `Feature flags cannot be overridden more than once`. If your app calls `loadReactNative` in `Application.onCreate` for its own React Native usage, don't initialize Brownfield on top of it in the same process. +::: + :::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. ::: diff --git a/docs/docs/docs/getting-started/android.mdx b/docs/docs/docs/getting-started/android.mdx index 3afaff6c9..3bae997b7 100644 --- a/docs/docs/docs/getting-started/android.mdx +++ b/docs/docs/docs/getting-started/android.mdx @@ -236,11 +236,12 @@ import android.app.Application import com.callstack.reactnativebrownfield.OnJSBundleLoaded import com.callstack.reactnativebrownfield.ReactNativeBrownfield import com.facebook.react.PackageList -import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative object ReactNativeHostManager { fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) { val packageList = PackageList(application).packages + // initialize() loads the native libs and feature flags; a preceding + // loadReactNative would override the flags twice and crash. ReactNativeBrownfield.initialize(application, packageList, onJSBundleLoaded) } } diff --git a/docs/docs/docs/guides/guidelines.mdx b/docs/docs/docs/guides/guidelines.mdx index cf2033e07..a931c117e 100644 --- a/docs/docs/docs/guides/guidelines.mdx +++ b/docs/docs/docs/guides/guidelines.mdx @@ -18,15 +18,16 @@ In your brownfield android library or iOS xcframework, create a class following ```kt // Your artifact +import com.callstack.reactnativebrownfield.ReactNativeBrownfield 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 + // initialize() loads the native libs and feature flags; a preceding + // loadReactNative would override the flags twice and crash. ReactNativeBrownfield.initialize(application, packages) } } @@ -44,13 +45,10 @@ If you do not follow this approach, then the usage in the native App would look ```kt // native App +import com.callstack.reactnativebrownfield.ReactNativeBrownfield import com.facebook.react.PackageList -import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative - -loadReactNative(application) -val packages = PackageList(this).getPackages() -ReactNativeHostManager.initialize(application) +ReactNativeBrownfield.initialize(application, PackageList(this).packages) ``` 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. diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts index 5aa9b7a78..d864e29d6 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts @@ -255,10 +255,13 @@ describe('createAndroidModule', () => { const hostManager = readLibraryHostManager(androidDir); expect(hostManager).toContain('package com.example.brownfield'); + // Host via factory + no loadReactNative: both would load the feature + // flags twice and crash on startup (issue #492). expect(hostManager).toContain( - 'ReactNativeBrownfield.initialize(application, reactHost, onJSBundleLoaded)' + 'ReactNativeBrownfield.initialize(application, onJSBundleLoaded)' ); - expect(hostManager).toContain('loadReactNative(application)'); + expect(hostManager).toContain('setEdgeToEdgeFeatureFlagOn()'); + expect(hostManager).not.toContain('ReactNativeApplicationEntryPoint'); expect(hostManager).not.toContain('{{PACKAGE_NAME}}'); // Expo-only wiring must not leak into the RN CLI variant. diff --git a/packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt b/packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt index b3059725a..7f456ac8b 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +++ b/packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt @@ -5,15 +5,20 @@ import android.content.res.Configuration import com.callstack.reactnativebrownfield.OnJSBundleLoaded import com.callstack.reactnativebrownfield.ReactNativeBrownfield import com.facebook.react.PackageList -import com.facebook.react.ReactHost -import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost +import com.facebook.react.views.view.setEdgeToEdgeFeatureFlagOn object ReactNativeHostManager { fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) { - loadReactNative(application) + // initialize() loads the native libs and feature flags itself; calling + // loadReactNative on top of it overrides the flags twice and crashes. + // Keep the edge-to-edge flag it would have set. + if (BuildConfig.IS_EDGE_TO_EDGE_ENABLED) { + setEdgeToEdgeFeatureFlagOn() + } - val reactHost: ReactHost by lazy { + // Factory so the host is built after initialize() runs SoLoader.init. + ReactNativeBrownfield.initialize(application, onJSBundleLoaded) { getDefaultReactHost( context = application, packageList = PackageList(application).packages, @@ -24,12 +29,9 @@ object ReactNativeHostManager { jsRuntimeFactory = null ) } - - ReactNativeBrownfield.initialize(application, reactHost, onJSBundleLoaded) } fun onConfigurationChanged(application: Application, newConfig: Configuration) { // no-op (kept for API symmetry with Expo variant) } } - From b1689c8e0105c7d093c593c40140a60e08ebc7a5 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 20 Oct 2026 00:09:35 -0700 Subject: [PATCH 2/2] refactor: remove comments --- docs/docs/docs/getting-started/android.mdx | 3 +-- docs/docs/docs/guides/guidelines.mdx | 2 -- .../android/__tests__/withAndroidModuleFiles.test.ts | 2 -- .../template/android/ReactNativeHostManager.vanilla.kt | 4 ---- 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/docs/docs/getting-started/android.mdx b/docs/docs/docs/getting-started/android.mdx index 3bae997b7..cd942da28 100644 --- a/docs/docs/docs/getting-started/android.mdx +++ b/docs/docs/docs/getting-started/android.mdx @@ -240,8 +240,7 @@ import com.facebook.react.PackageList object ReactNativeHostManager { fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) { val packageList = PackageList(application).packages - // initialize() loads the native libs and feature flags; a preceding - // loadReactNative would override the flags twice and crash. + ReactNativeBrownfield.initialize(application, packageList, onJSBundleLoaded) } } diff --git a/docs/docs/docs/guides/guidelines.mdx b/docs/docs/docs/guides/guidelines.mdx index a931c117e..c0f71d81a 100644 --- a/docs/docs/docs/guides/guidelines.mdx +++ b/docs/docs/docs/guides/guidelines.mdx @@ -26,8 +26,6 @@ class ReactNativeHostManager { fun initialize(application: Application) { val packages = PackageList(application).packages // imported from autogenerated PackageList - // initialize() loads the native libs and feature flags; a preceding - // loadReactNative would override the flags twice and crash. ReactNativeBrownfield.initialize(application, packages) } } diff --git a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts index d864e29d6..28e1123e0 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/android/__tests__/withAndroidModuleFiles.test.ts @@ -255,8 +255,6 @@ describe('createAndroidModule', () => { const hostManager = readLibraryHostManager(androidDir); expect(hostManager).toContain('package com.example.brownfield'); - // Host via factory + no loadReactNative: both would load the feature - // flags twice and crash on startup (issue #492). expect(hostManager).toContain( 'ReactNativeBrownfield.initialize(application, onJSBundleLoaded)' ); diff --git a/packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt b/packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt index 7f456ac8b..f05e4c811 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +++ b/packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt @@ -10,14 +10,10 @@ import com.facebook.react.views.view.setEdgeToEdgeFeatureFlagOn object ReactNativeHostManager { fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) { - // initialize() loads the native libs and feature flags itself; calling - // loadReactNative on top of it overrides the flags twice and crashes. - // Keep the edge-to-edge flag it would have set. if (BuildConfig.IS_EDGE_TO_EDGE_ENABLED) { setEdgeToEdgeFeatureFlagOn() } - // Factory so the host is built after initialize() runs SoLoader.init. ReactNativeBrownfield.initialize(application, onJSBundleLoaded) { getDefaultReactHost( context = application,