Skip to content

Commit 84b87a0

Browse files
committed
fix: update JS bundle loading for OTA reload to prevent app loop
1 parent 961a8bc commit 84b87a0

2 files changed

Lines changed: 59 additions & 43 deletions

File tree

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

Lines changed: 54 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,51 @@ 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+
// Build the bridgeless ReactHost ourselves (instead of DefaultReactHost.getDefaultReactHost)
68+
// so we can supply a *dynamic* JSBundleLoader. reactHost.reload() destroys and recreates the
69+
// React instance, re-invoking this loader's loadScript() on every reload. By resolving
70+
// getJSBundleFile() inside loadScript() each time, a freshly-deployed OTA bundle is picked up
71+
// automatically — without this the loader is fixed at construction time and the app loops:
72+
// download -> deploy -> reload -> same old bundle. `by lazy` keeps it a single host instance.
73+
@OptIn(UnstableReactNativeAPI::class)
74+
override val reactHost: ReactHost by lazy {
75+
val dynamicBundleLoader = object : JSBundleLoader() {
76+
override fun loadScript(delegate: JSBundleLoaderDelegate): String {
77+
val bundle = jsBundleFile
78+
if (bundle != null) {
79+
if (bundle.startsWith("assets://")) {
80+
delegate.loadScriptFromAssets(assets, bundle, true)
81+
} else {
82+
delegate.loadScriptFromFile(bundle, bundle, false)
83+
}
84+
return bundle
85+
}
86+
val defaultBundle = "assets://index.android.bundle"
87+
delegate.loadScriptFromAssets(assets, defaultBundle, true)
88+
return defaultBundle
89+
}
90+
}
91+
92+
val hostPackages: MutableList<ReactPackage> = ArrayList(this@MendixReactApplication.packages)
93+
applyInternalPackageAugmentations(hostPackages)
94+
95+
val delegate = DefaultReactHostDelegate(
96+
jsMainModulePath = "index",
97+
jsBundleLoader = dynamicBundleLoader,
98+
reactPackages = hostPackages,
99+
jsRuntimeFactory = HermesInstance(),
100+
turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(),
101+
)
102+
val componentFactory = ComponentFactory()
103+
DefaultComponentsRegistry.register(componentFactory)
104+
ReactHostImpl(
105+
applicationContext,
106+
delegate,
107+
componentFactory,
108+
true /* allowPackagerServerAccess */,
109+
useDeveloperSupport,
110+
)
111+
}
61112

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

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

Lines changed: 5 additions & 40 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,18 @@ 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.
3535
private fun reloadWithoutState() {
3636
val reactHost = (context.applicationContext as? ReactApplication)?.reactHost
3737
postOnMainThread {
@@ -48,39 +48,4 @@ class NativeReloadHandler(val context: ReactApplicationContext) {
4848
cb.invoke()
4949
}
5050
}
51-
52-
private fun handleJSBundleLoading() {
53-
val bundle = (context.applicationContext as MendixApplication).jsBundleFile
54-
55-
val latestJSBundleLoader = if (bundle != null) {
56-
getAssetLoader(bundle)
57-
} else {
58-
getAssetLoader("assets://index.android.bundle")
59-
} ?: return
60-
61-
// New Architecture (Bridgeless): on reload, ReactHostImpl loads the bundle from
62-
// its ReactHostDelegate.jsBundleLoader, which is captured once at host-creation
63-
// time and never re-consults getJSBundleFile(). Without updating it here, a warm
64-
// reload keeps loading the original bundle, so a freshly-deployed OTA bundle is
65-
// never picked up and the app loops: download -> deploy -> reload -> same bundle.
66-
val reactHost = (context.applicationContext as? ReactApplication)?.reactHost ?: return
67-
try {
68-
val delegate = ReflectionUtils.getField<Any>(reactHost, "mReactHostDelegate")
69-
ReflectionUtils.setField(delegate, "jsBundleLoader", latestJSBundleLoader)
70-
} catch (e: Exception) {
71-
FLog.e(javaClass, "Failed to update JS bundle loader for OTA reload", e)
72-
}
73-
}
74-
75-
private fun getAssetLoader(bundle: String): JSBundleLoader? {
76-
return when {
77-
bundle.startsWith("assets://") -> JSBundleLoader.createAssetLoader(
78-
context,
79-
bundle,
80-
false
81-
)
82-
83-
else -> JSBundleLoader.createFileLoader(bundle)
84-
}
85-
}
8651
}

0 commit comments

Comments
 (0)