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
15 changes: 15 additions & 0 deletions .changeset/olive-pumas-shave.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 5 additions & 4 deletions docs/docs/docs/api-reference/react-native-brownfield/java.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
:::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
:::
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/docs/getting-started/android.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,11 @@ 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

ReactNativeBrownfield.initialize(application, packageList, onJSBundleLoaded)
}
}
Expand Down
10 changes: 3 additions & 7 deletions docs/docs/docs/guides/guidelines.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ 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

ReactNativeBrownfield.initialize(application, packages)
Expand All @@ -44,13 +43,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,10 @@ describe('createAndroidModule', () => {
const hostManager = readLibraryHostManager(androidDir);
expect(hostManager).toContain('package com.example.brownfield');
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ 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)
if (BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
setEdgeToEdgeFeatureFlagOn()
}

val reactHost: ReactHost by lazy {
ReactNativeBrownfield.initialize(application, onJSBundleLoaded) {
getDefaultReactHost(
context = application,
packageList = PackageList(application).packages,
Expand All @@ -24,12 +25,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)
}
}

Loading