Skip to content

Commit f4bf4e2

Browse files
committed
feat: implement dynamic JSBundleLoader for ReactHost to support OTA updates
1 parent 0e440a9 commit f4bf4e2

2 files changed

Lines changed: 66 additions & 39 deletions

File tree

android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ import android.app.Application
44
import com.facebook.react.ReactHost
55
import com.facebook.react.ReactNativeHost
66
import com.facebook.react.ReactPackage
7+
import com.facebook.react.bridge.JSBundleLoader
8+
import com.facebook.react.bridge.JSBundleLoaderDelegate
9+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
10+
import com.facebook.react.defaults.DefaultComponentsRegistry
11+
import com.facebook.react.defaults.DefaultReactHostDelegate
12+
import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate
713
import com.facebook.react.devsupport.interfaces.RedBoxHandler
14+
import com.facebook.react.fabric.ComponentFactory
15+
import com.facebook.react.runtime.ReactHostImpl
16+
import com.facebook.react.runtime.hermes.HermesInstance
817
import com.facebook.react.soloader.OpenSourceMergedSoMapping
918
import com.facebook.soloader.SoLoader
1019
import com.mendix.mendixnative.error.ErrorHandler
@@ -16,7 +25,6 @@ import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter
1625
import com.mendixnative.MendixNativePackage
1726
import java.util.*
1827
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
19-
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
2028

2129
import com.facebook.react.defaults.DefaultReactNativeHost
2230

@@ -56,8 +64,55 @@ abstract class MendixReactApplication : Application(), MendixApplication, ErrorH
5664
override val isHermesEnabled: Boolean = true
5765
}
5866

59-
override val reactHost: ReactHost
60-
get() = getDefaultReactHost(applicationContext, reactNativeHost)
67+
/**
68+
* Build the [ReactHost] ourselves instead of using [DefaultReactHost.getDefaultReactHost],
69+
* because that factory evaluates [ReactNativeHost.getJSBundleFile] once at creation time and
70+
* bakes the result into a fixed [JSBundleLoader]. After an OTA update deploys a new bundle,
71+
* a subsequent [ReactHost.reload] would still load the stale bundle.
72+
*
73+
* By providing a **dynamic** [JSBundleLoader] whose [JSBundleLoader.loadScript] calls
74+
* [getJSBundleFile] on every invocation, each reload picks up the latest bundle path —
75+
* whether it comes from OTA, a custom [JSBundleFileProvider], or the default asset bundle.
76+
*/
77+
@OptIn(UnstableReactNativeAPI::class)
78+
override val reactHost: ReactHost by lazy {
79+
val dynamicBundleLoader = object : JSBundleLoader() {
80+
override fun loadScript(delegate: JSBundleLoaderDelegate): String {
81+
val bundle = jsBundleFile
82+
if (bundle != null) {
83+
if (bundle.startsWith("assets://")) {
84+
delegate.loadScriptFromAssets(assets, bundle, true)
85+
} else {
86+
delegate.loadScriptFromFile(bundle, bundle, false)
87+
}
88+
return bundle
89+
}
90+
val defaultBundle = "assets://index.android.bundle"
91+
delegate.loadScriptFromAssets(assets, defaultBundle, true)
92+
return defaultBundle
93+
}
94+
}
95+
96+
val hostPackages: MutableList<ReactPackage> = ArrayList(this@MendixReactApplication.packages)
97+
applyInternalPackageAugmentations(hostPackages)
98+
99+
val delegate = DefaultReactHostDelegate(
100+
jsMainModulePath = "index",
101+
jsBundleLoader = dynamicBundleLoader,
102+
reactPackages = hostPackages,
103+
jsRuntimeFactory = HermesInstance(),
104+
turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(),
105+
)
106+
val componentFactory = ComponentFactory()
107+
DefaultComponentsRegistry.register(componentFactory)
108+
ReactHostImpl(
109+
applicationContext,
110+
delegate,
111+
componentFactory,
112+
true /* allowPackagerServerAccess */,
113+
useDeveloperSupport,
114+
)
115+
}
61116

62117
/**
63118
* Apply internal augmentations to packages (e.g., attach presenters) without instantiating

android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,8 @@ import android.os.Handler
44
import android.os.Looper
55
import com.facebook.common.logging.FLog
66
import com.facebook.react.ReactApplication
7-
import com.facebook.react.bridge.JSBundleLoader
87
import com.facebook.react.bridge.ReactApplicationContext
9-
import com.mendix.mendixnative.MendixApplication
108
import com.mendix.mendixnative.activity.LaunchScreenHandler
11-
import com.mendix.mendixnative.util.ReflectionUtils
12-
import com.op.sqlite.OPSQLiteModule
139

1410
class NativeReloadHandler(val context: ReactApplicationContext) {
1511

@@ -24,14 +20,21 @@ class NativeReloadHandler(val context: ReactApplicationContext) {
2420
javaClass,
2521
"Activity does not implement LaunchScreenHandler, skipping showing launch screen"
2622
)
27-
handleJSBundleLoading()
2823
reloadWithoutState()
2924
}
3025

3126
fun exitApp() {
3227
context.currentActivity?.finishAffinity()
3328
}
3429

30+
// In the New Architecture (Bridgeless), reactHost.reload() destroys and recreates the
31+
// React instance, which re-invokes the JSBundleLoader provided to ReactHostImpl at
32+
// construction time. MendixReactApplication supplies a *dynamic* JSBundleLoader whose
33+
// loadScript() calls MendixReactApplication.getJSBundleFile() on every reload, so OTA
34+
// bundle changes are picked up automatically — no manual bundle swapping is needed.
35+
//
36+
// See ReactHostImpl.getOrCreateReloadTask() → getOrCreateReactInstanceTask() →
37+
// jsBundleLoader.onSuccess { instance.loadJSBundle(bundleLoader) }
3538
private fun reloadWithoutState() {
3639
val reactHost = (context.applicationContext as? ReactApplication)?.reactHost
3740
postOnMainThread {
@@ -48,35 +51,4 @@ class NativeReloadHandler(val context: ReactApplicationContext) {
4851
cb.invoke()
4952
}
5053
}
51-
52-
private fun handleJSBundleLoading() {
53-
val bundle = (context.applicationContext as MendixApplication).jsBundleFile
54-
val instanceManager =
55-
(context.applicationContext as ReactApplication).reactNativeHost.reactInstanceManager
56-
57-
val latestJSBundleLoader = if (bundle != null) {
58-
getAssetLoader(bundle)
59-
} else {
60-
getAssetLoader("assets://index.android.bundle")
61-
}
62-
63-
ReflectionUtils.setField(instanceManager, "mBundleLoader", latestJSBundleLoader)
64-
ReflectionUtils.setField(
65-
instanceManager,
66-
"mUseDeveloperSupport",
67-
(context.applicationContext as MendixApplication).useDeveloperSupport
68-
)
69-
}
70-
71-
private fun getAssetLoader(bundle: String): JSBundleLoader? {
72-
return when {
73-
bundle.startsWith("assets://") -> JSBundleLoader.createAssetLoader(
74-
context,
75-
bundle,
76-
false
77-
)
78-
79-
else -> JSBundleLoader.createFileLoader(bundle)
80-
}
81-
}
8254
}

0 commit comments

Comments
 (0)