From 961a8bc34654dd26f5d863de057b3fa8ef3f1f23 Mon Sep 17 00:00:00 2001 From: vadymv-mendix Date: Tue, 2 Jun 2026 14:37:01 +0200 Subject: [PATCH 1/2] fix: update JS bundle loading for OTA reload to prevent app loop --- .../mendixnative/react/NativeReloadHandler.kt | 22 +++++++++++-------- package.json | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt b/android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt index 43f0a0b..1e9c737 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt @@ -51,21 +51,25 @@ class NativeReloadHandler(val context: ReactApplicationContext) { private fun handleJSBundleLoading() { val bundle = (context.applicationContext as MendixApplication).jsBundleFile - val instanceManager = - (context.applicationContext as ReactApplication).reactNativeHost.reactInstanceManager val latestJSBundleLoader = if (bundle != null) { getAssetLoader(bundle) } else { getAssetLoader("assets://index.android.bundle") - } + } ?: return - ReflectionUtils.setField(instanceManager, "mBundleLoader", latestJSBundleLoader) - ReflectionUtils.setField( - instanceManager, - "mUseDeveloperSupport", - (context.applicationContext as MendixApplication).useDeveloperSupport - ) + // New Architecture (Bridgeless): on reload, ReactHostImpl loads the bundle from + // its ReactHostDelegate.jsBundleLoader, which is captured once at host-creation + // time and never re-consults getJSBundleFile(). Without updating it here, a warm + // reload keeps loading the original bundle, so a freshly-deployed OTA bundle is + // never picked up and the app loops: download -> deploy -> reload -> same bundle. + val reactHost = (context.applicationContext as? ReactApplication)?.reactHost ?: return + try { + val delegate = ReflectionUtils.getField(reactHost, "mReactHostDelegate") + ReflectionUtils.setField(delegate, "jsBundleLoader", latestJSBundleLoader) + } catch (e: Exception) { + FLog.e(javaClass, "Failed to update JS bundle loader for OTA reload", e) + } } private fun getAssetLoader(bundle: String): JSBundleLoader? { diff --git a/package.json b/package.json index 7c72433..928a2c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mendix-native", - "version": "0.3.0", + "version": "0.3.2", "description": "Mendix native mobile package", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", From 84b87a0fa28e9e41d9209b90c6c74d794f2c63cc Mon Sep 17 00:00:00 2001 From: vadymv-mendix Date: Tue, 2 Jun 2026 14:55:19 +0200 Subject: [PATCH 2/2] fix: update JS bundle loading for OTA reload to prevent app loop --- .../mendixnative/MendixReactApplication.kt | 57 ++++++++++++++++++- .../mendixnative/react/NativeReloadHandler.kt | 45 ++------------- 2 files changed, 59 insertions(+), 43 deletions(-) diff --git a/android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt b/android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt index 9fecec9..69f6c53 100644 --- a/android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt +++ b/android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt @@ -4,7 +4,16 @@ import android.app.Application import com.facebook.react.ReactHost import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage +import com.facebook.react.bridge.JSBundleLoader +import com.facebook.react.bridge.JSBundleLoaderDelegate +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.defaults.DefaultComponentsRegistry +import com.facebook.react.defaults.DefaultReactHostDelegate +import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate import com.facebook.react.devsupport.interfaces.RedBoxHandler +import com.facebook.react.fabric.ComponentFactory +import com.facebook.react.runtime.ReactHostImpl +import com.facebook.react.runtime.hermes.HermesInstance import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader import com.mendix.mendixnative.error.ErrorHandler @@ -16,7 +25,6 @@ import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter import com.mendixnative.MendixNativePackage import java.util.* import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load -import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost @@ -56,8 +64,51 @@ abstract class MendixReactApplication : Application(), MendixApplication, ErrorH override val isHermesEnabled: Boolean = true } - override val reactHost: ReactHost - get() = getDefaultReactHost(applicationContext, reactNativeHost) + // Build the bridgeless ReactHost ourselves (instead of DefaultReactHost.getDefaultReactHost) + // so we can supply a *dynamic* JSBundleLoader. reactHost.reload() destroys and recreates the + // React instance, re-invoking this loader's loadScript() on every reload. By resolving + // getJSBundleFile() inside loadScript() each time, a freshly-deployed OTA bundle is picked up + // automatically — without this the loader is fixed at construction time and the app loops: + // download -> deploy -> reload -> same old bundle. `by lazy` keeps it a single host instance. + @OptIn(UnstableReactNativeAPI::class) + override val reactHost: ReactHost by lazy { + val dynamicBundleLoader = object : JSBundleLoader() { + override fun loadScript(delegate: JSBundleLoaderDelegate): String { + val bundle = jsBundleFile + if (bundle != null) { + if (bundle.startsWith("assets://")) { + delegate.loadScriptFromAssets(assets, bundle, true) + } else { + delegate.loadScriptFromFile(bundle, bundle, false) + } + return bundle + } + val defaultBundle = "assets://index.android.bundle" + delegate.loadScriptFromAssets(assets, defaultBundle, true) + return defaultBundle + } + } + + val hostPackages: MutableList = ArrayList(this@MendixReactApplication.packages) + applyInternalPackageAugmentations(hostPackages) + + val delegate = DefaultReactHostDelegate( + jsMainModulePath = "index", + jsBundleLoader = dynamicBundleLoader, + reactPackages = hostPackages, + jsRuntimeFactory = HermesInstance(), + turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(), + ) + val componentFactory = ComponentFactory() + DefaultComponentsRegistry.register(componentFactory) + ReactHostImpl( + applicationContext, + delegate, + componentFactory, + true /* allowPackagerServerAccess */, + useDeveloperSupport, + ) + } /** * Apply internal augmentations to packages (e.g., attach presenters) without instantiating diff --git a/android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt b/android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt index 1e9c737..15bee97 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt @@ -4,12 +4,8 @@ import android.os.Handler import android.os.Looper import com.facebook.common.logging.FLog import com.facebook.react.ReactApplication -import com.facebook.react.bridge.JSBundleLoader import com.facebook.react.bridge.ReactApplicationContext -import com.mendix.mendixnative.MendixApplication import com.mendix.mendixnative.activity.LaunchScreenHandler -import com.mendix.mendixnative.util.ReflectionUtils -import com.op.sqlite.OPSQLiteModule class NativeReloadHandler(val context: ReactApplicationContext) { @@ -24,7 +20,6 @@ class NativeReloadHandler(val context: ReactApplicationContext) { javaClass, "Activity does not implement LaunchScreenHandler, skipping showing launch screen" ) - handleJSBundleLoading() reloadWithoutState() } @@ -32,6 +27,11 @@ class NativeReloadHandler(val context: ReactApplicationContext) { context.currentActivity?.finishAffinity() } + // In the New Architecture (Bridgeless), reactHost.reload() destroys and recreates the + // React instance, which re-invokes the JSBundleLoader provided to ReactHostImpl at + // construction time. MendixReactApplication supplies a *dynamic* JSBundleLoader whose + // loadScript() calls MendixReactApplication.getJSBundleFile() on every reload, so OTA + // bundle changes are picked up automatically — no manual bundle swapping is needed. private fun reloadWithoutState() { val reactHost = (context.applicationContext as? ReactApplication)?.reactHost postOnMainThread { @@ -48,39 +48,4 @@ class NativeReloadHandler(val context: ReactApplicationContext) { cb.invoke() } } - - private fun handleJSBundleLoading() { - val bundle = (context.applicationContext as MendixApplication).jsBundleFile - - val latestJSBundleLoader = if (bundle != null) { - getAssetLoader(bundle) - } else { - getAssetLoader("assets://index.android.bundle") - } ?: return - - // New Architecture (Bridgeless): on reload, ReactHostImpl loads the bundle from - // its ReactHostDelegate.jsBundleLoader, which is captured once at host-creation - // time and never re-consults getJSBundleFile(). Without updating it here, a warm - // reload keeps loading the original bundle, so a freshly-deployed OTA bundle is - // never picked up and the app loops: download -> deploy -> reload -> same bundle. - val reactHost = (context.applicationContext as? ReactApplication)?.reactHost ?: return - try { - val delegate = ReflectionUtils.getField(reactHost, "mReactHostDelegate") - ReflectionUtils.setField(delegate, "jsBundleLoader", latestJSBundleLoader) - } catch (e: Exception) { - FLog.e(javaClass, "Failed to update JS bundle loader for OTA reload", e) - } - } - - private fun getAssetLoader(bundle: String): JSBundleLoader? { - return when { - bundle.startsWith("assets://") -> JSBundleLoader.createAssetLoader( - context, - bundle, - false - ) - - else -> JSBundleLoader.createFileLoader(bundle) - } - } }