Skip to content

Commit cbc99cf

Browse files
fix: make sure lib are loaded for expo apps (#322)
* fix: make sure android libs are loaded for expo apps * docs: clean up chapter 4 and mention one ReactNativeBrownfield::initialize variant is deprecated
1 parent 388663d commit cbc99cf

10 files changed

Lines changed: 88 additions & 200 deletions

File tree

.changeset/five-signs-fix.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@callstack/react-native-brownfield': patch
3+
'@callstack/brownfield-navigation': patch
4+
'@callstack/brownie': patch
5+
---
6+
7+
fix: make sure android libs are loaded for expo apps

apps/RNApp/android/BrownfieldLib/consumer-rules.pro

Whitespace-only changes.

docs/docs/docs/getting-started/android.mdx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,14 +185,17 @@ import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
185185

186186
object ReactNativeHostManager {
187187
fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) {
188-
loadReactNative(application) // **Only required for RN >= 0.80.0**
189-
190188
val packageList = PackageList(application).packages
191189
ReactNativeBrownfield.initialize(application, packageList, onJSBundleLoaded)
192190
}
193191
}
194192
```
195193

194+
:::warning Important
195+
The `ReactNativeBrownfield::initialize(Application, ReactHost, OnJSBundleLoaded? = null)` function variant is deprecated and shouldn't be used any more. It will be removed in a future release.
196+
This version is unsafe because passing a pre-constructed `ReactHost` can trigger `SoLoader` (e.g., via `ExpoReactHostFactory`) before native libraries are fully loaded, leading to potential runtime crashes.
197+
:::
198+
196199
## 5. Add Build Config Fields
197200

198201
Add to `reactnativeapp/build.gradle.kts`:

packages/brownfield-navigation/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,15 @@
6666
"@babel/core": "^7.25.2",
6767
"@babel/preset-env": "^7.25.3",
6868
"@babel/runtime": "^7.25.0",
69-
"@react-native/babel-preset": "0.82.1",
69+
"@react-native/babel-preset": "0.83.2",
7070
"@types/jest": "^30.0.0",
7171
"@types/react": "^19.1.1",
7272
"eslint": "^9.28.0",
7373
"globals": "^16.2.0",
7474
"import": "^0.0.6",
7575
"nodemon": "^3.1.14",
7676
"react": "19.1.1",
77-
"react-native": "0.82.1",
77+
"react-native": "0.83.2",
7878
"react-native-builder-bob": "^0.41.0",
7979
"typescript": "5.9.3"
8080
},

packages/brownie/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@
8484
"@babel/preset-env": "^7.25.3",
8585
"@babel/preset-typescript": "^7.27.1",
8686
"@babel/runtime": "^7.25.0",
87-
"@react-native/babel-preset": "0.82.1",
88-
"@react-native/eslint-config": "0.82.1",
87+
"@react-native/babel-preset": "0.83.2",
88+
"@react-native/eslint-config": "0.83.2",
8989
"@types/node": "^25.5.0",
9090
"@types/react": "^19.1.1",
9191
"eslint": "^9.39.3",
9292
"globals": "^17.3.0",
9393
"import": "^0.0.6",
9494
"nodemon": "^3.1.14",
95-
"react-native": "0.82.1",
95+
"react-native": "0.83.2",
9696
"react-native-builder-bob": "^0.41.0",
9797
"typescript": "5.9.3"
9898
},

packages/react-native-brownfield/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
3434
private lateinit var instance: ReactNativeBrownfield
3535
private val initialized = AtomicBoolean()
3636
private val nativeLibsLoaded = AtomicBoolean()
37-
private const val LOG_TAG = "ReactNativeBrownfield"
3837

3938
@JvmStatic
4039
val shared: ReactNativeBrownfield get() = instance
@@ -50,6 +49,12 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
5049
load()
5150
}
5251

52+
@Deprecated(
53+
message = "Unsafe when reactHost construction triggers SoLoader (e.g. ExpoReactHostFactory): " +
54+
"the parameter is evaluated by the caller before loadNativeLibs() runs. " +
55+
"Use initialize(application, onJSBundleLoaded) { reactHostFactory } instead.",
56+
replaceWith = ReplaceWith("initialize(application, onJSBundleLoaded) { reactHost }")
57+
)
5358
@JvmStatic
5459
@JvmOverloads
5560
fun initialize(
@@ -59,25 +64,32 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
5964
) {
6065
if (!initialized.getAndSet(true)) {
6166
loadNativeLibs(application)
62-
instance = ReactNativeBrownfield(reactHost)
63-
64-
preloadReactNative {
65-
onJSBundleLoaded?.invoke(true)
66-
}
67+
installAndPreload(reactHost, onJSBundleLoaded)
6768
}
6869
}
6970

71+
@JvmStatic
72+
fun initialize(
73+
application: Application,
74+
onJSBundleLoaded: OnJSBundleLoaded? = null,
75+
reactHostFactory: () -> ReactHost
76+
) {
77+
if (!initialized.getAndSet(true)) {
78+
loadNativeLibs(application)
79+
installAndPreload(reactHostFactory(), onJSBundleLoaded)
80+
}
81+
}
82+
7083
@JvmStatic
7184
@JvmOverloads
7285
fun initialize(
7386
application: Application,
7487
options: HashMap<String, Any>,
7588
onJSBundleLoaded: OnJSBundleLoaded? = null
7689
) {
77-
loadNativeLibs(application)
78-
79-
val reactHost: ReactHost by lazy {
80-
getDefaultReactHost(
90+
if (!initialized.getAndSet(true)) {
91+
loadNativeLibs(application)
92+
val reactHost = getDefaultReactHost(
8193
context = application,
8294
packageList = (options["packages"] as? List<*> ?: emptyList<ReactPackage>())
8395
.filterIsInstance<ReactPackage>(),
@@ -89,9 +101,8 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
89101
?: ReactBuildConfig.DEBUG,
90102
jsRuntimeFactory = null
91103
)
104+
installAndPreload(reactHost, onJSBundleLoaded)
92105
}
93-
94-
initialize(application, reactHost, onJSBundleLoaded)
95106
}
96107

97108
@JvmStatic
@@ -116,6 +127,13 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
116127
})
117128
shared.reactHost.start()
118129
}
130+
131+
private fun installAndPreload(reactHost: ReactHost, onJSBundleLoaded: OnJSBundleLoaded?) {
132+
instance = ReactNativeBrownfield(reactHost)
133+
preloadReactNative {
134+
onJSBundleLoaded?.invoke(true)
135+
}
136+
}
119137
}
120138

121139
/**

packages/react-native-brownfield/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
"@babel/preset-env": "^7.25.3",
9494
"@babel/runtime": "^7.25.0",
9595
"@expo/config-plugins": "^54.0.4",
96-
"@react-native/babel-preset": "0.82.1",
96+
"@react-native/babel-preset": "0.83.2",
9797
"@types/jest": "^30.0.0",
9898
"@types/react": "^19.1.1",
9999
"@vitest/coverage-v8": "^4.1.0",
@@ -102,7 +102,7 @@
102102
"import": "^0.0.6",
103103
"nodemon": "^3.1.14",
104104
"react": "19.1.1",
105-
"react-native": "0.82.1",
105+
"react-native": "0.83.2",
106106
"react-native-builder-bob": "^0.41.0",
107107
"typescript": "5.9.3",
108108
"vitest": "^4.1.4"

packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.post55.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,19 @@ import android.content.res.Configuration
55
import com.callstack.reactnativebrownfield.OnJSBundleLoaded
66
import com.callstack.reactnativebrownfield.ReactNativeBrownfield
77
import com.facebook.react.PackageList
8-
import com.facebook.react.ReactHost
98
import expo.modules.ApplicationLifecycleDispatcher
109
import expo.modules.ExpoReactHostFactory
1110

1211
object ReactNativeHostManager {
1312
fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) {
1413
ApplicationLifecycleDispatcher.onApplicationCreate(application)
1514

16-
val reactHost: ReactHost by lazy {
15+
ReactNativeBrownfield.initialize(application, onJSBundleLoaded) {
1716
ExpoReactHostFactory.getDefaultReactHost(
1817
context = application.applicationContext,
1918
packageList = PackageList(application).packages,
2019
)
2120
}
22-
23-
ReactNativeBrownfield.initialize(application, reactHost, onJSBundleLoaded)
2421
}
2522

2623
fun onConfigurationChanged(application: Application, newConfig: Configuration) {

packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.pre55.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import android.content.res.Configuration
55
import com.callstack.reactnativebrownfield.OnJSBundleLoaded
66
import com.callstack.reactnativebrownfield.ReactNativeBrownfield
77
import com.facebook.react.PackageList
8-
import com.facebook.react.ReactHost
98
import com.facebook.react.ReactPackage
109
import com.facebook.react.defaults.DefaultReactNativeHost
1110
import expo.modules.ApplicationLifecycleDispatcher
@@ -34,15 +33,12 @@ object ReactNativeHostManager {
3433
override fun getBundleAssetName(): String = "index.android.bundle"
3534
})
3635

37-
38-
val reactHost: ReactHost by lazy {
36+
ReactNativeBrownfield.initialize(application, onJSBundleLoaded) {
3937
ExpoReactHostFactory.createFromReactNativeHost(
4038
context = application.applicationContext,
4139
reactNativeHost = reactNativeHost
4240
)
4341
}
44-
45-
ReactNativeBrownfield.initialize(application, reactHost, onJSBundleLoaded)
4642
}
4743

4844
fun onConfigurationChanged(application: Application, newConfig: Configuration) {

0 commit comments

Comments
 (0)