Skip to content

Commit cb1d4b0

Browse files
coadofacebook-github-bot
authored andcommitted
Android: Add setBundleSource method to ReactHost for changing bundle URL at runtime (facebook#54139)
Summary: Following the [RFC](react-native-community/discussions-and-proposals#933), this PR adds new `setBundleSource` methods to `ReactHost` for modifying bundle URL at runtime. The first one with signature: ```Kotlin public fun setBundleSource(debugServerHost: String, moduleName: String, queryBuilder: (Map<String, String>) -> Map<String, String> = { it }) ``` takes debugServerHost (set in packager connection settings), moduleName (set in DevSupportManager's jsAppBundleName), and queryBuilder (set in packager connection settings). Before updating settings, the packager connection is closed to reset the packager client, which will be newly created during reload with updated configuration. The second one for loading bundle from the file takes single `filePath` argument: ```Kotlin public fun setBundleSource(filePath: String) ``` It sets `customBundleFilePath` in `DevSupportManager` which has priority over other methods of loading the bundle in `jsBundleLoader` and reloads `ReactHost`. ## Changelog: [ANDROID][ADDED] - added new `setBundleSource` method to `ReactHost` for changing bundle URL at runtime. Test Plan: Started with running two Metro instances on ports `8081` and `8082` (first with white background, second with blue). Created a native button that toggles `debugServerHost` port and invokes `setBundleSource`. https://github.com/user-attachments/assets/7afe2cbc-6fef-44bc-930c-e9f9c4edd2bd For setting bundle file path, generated JS bundle with different background comparing to the one serving by Metro. Moved file to the `app/files` directory in android emulator and configured native button to invoke a `setBundleSource(filePath)`. https://github.com/user-attachments/assets/5e59d7b7-c6ae-475c-94e3-50d4ac69cf24 <details> <summary>code:</summary> Changing debug server host: `RNTesterActivity.kt`: ```Kotlin package com.facebook.react.uiapp import android.content.res.Configuration import android.graphics.Color import android.os.Bundle import android.view.View import android.widget.Button import android.widget.FrameLayout import androidx.core.graphics.drawable.toDrawable import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import com.facebook.common.logging.FLog import com.facebook.react.FBRNTesterEndToEndHelper import com.facebook.react.ReactActivity import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate import java.io.FileDescriptor import java.io.PrintWriter internal class RNTesterActivity : ReactActivity() { private var activePort = "8081" class RNTesterActivityDelegate(val activity: ReactActivity, mainComponentName: String) : DefaultReactActivityDelegate(activity, mainComponentName, fabricEnabled) { private val PARAM_ROUTE = "route" private lateinit var initialProps: Bundle override fun onCreate(savedInstanceState: Bundle?) { // Get remote param before calling super which uses it val bundle = activity.intent?.extras if (bundle != null && bundle.containsKey(PARAM_ROUTE)) { val routeUri = "rntester://example/${bundle.getString(PARAM_ROUTE)}Example" initialProps = Bundle().apply { putString("exampleFromAppetizeParams", routeUri) } } FBRNTesterEndToEndHelper.onCreate(activity.application) super.onCreate(savedInstanceState) } override fun getLaunchOptions() = if (this::initialProps.isInitialized) initialProps else Bundle() } private fun getButtonText(): String { return "Port: $activePort" } private fun setupPortButton(onClick: () -> Unit) { val portButton = Button(this).apply { text = getButtonText() setBackgroundColor(Color.rgb(0, 123, 255)) // Blue background setTextColor(Color.WHITE) setPadding(32, 16, 32, 16) textSize = 16f elevation = 8f } // Get the root view and add button to it val rootView = this.findViewById<FrameLayout>(android.R.id.content) val layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT ).apply { gravity = android.view.Gravity.TOP or android.view.Gravity.CENTER_HORIZONTAL topMargin = 200 // 50dp from top } rootView.addView(portButton, layoutParams) portButton.setOnClickListener { onClick() portButton.text = getButtonText() } } // set background color so it will show below transparent system bars on forced edge-to-edge private fun maybeUpdateBackgroundColor() { val isDarkMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES val color = if (isDarkMode) { Color.rgb(11, 6, 0) } else { Color.rgb(243, 248, 255) } window?.setBackgroundDrawable(color.toDrawable()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) fullyDrawnReporter.addReporter() maybeUpdateBackgroundColor() reactDelegate?.reactHost?.let { setupPortButton { activePort = if (activePort == "8081") "8082" else "8081" reactHost.setBundleSource("10.0.2.2:$activePort", "js/RNTesterApp.android") // reactHost.setBundleSource("/data/user/0/com.facebook.react.uiapp/files/android.bundle") } } // register insets listener to update margins on the ReactRootView to avoid overlap w/ system // bars reactDelegate?.reactRootView?.let { rootView -> val insetsType: Int = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() val windowInsetsListener = { view: View, windowInsets: WindowInsetsCompat -> val insets = windowInsets.getInsets(insetsType) (view.layoutParams as FrameLayout.LayoutParams).apply { setMargins(insets.left, insets.top, insets.right, insets.bottom) } WindowInsetsCompat.CONSUMED } ViewCompat.setOnApplyWindowInsetsListener(rootView, windowInsetsListener) } } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) // update background color on UI mode change maybeUpdateBackgroundColor() } override fun createReactActivityDelegate() = RNTesterActivityDelegate(this, mainComponentName) override fun getMainComponentName() = "RNTesterApp" override fun dump( prefix: String, fd: FileDescriptor?, writer: PrintWriter, args: Array<String>?, ) { FBRNTesterEndToEndHelper.maybeDump(prefix, writer, args) } } ``` </detail> Differential Revision: D84713639 Pulled By: coado
1 parent 93c17cd commit cb1d4b0

File tree

7 files changed

+104
-21
lines changed

7 files changed

+104
-21
lines changed

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ public abstract interface class com/facebook/react/ReactHost {
239239
public abstract fun reload (Ljava/lang/String;)Lcom/facebook/react/interfaces/TaskInterface;
240240
public abstract fun removeBeforeDestroyListener (Lkotlin/jvm/functions/Function0;)V
241241
public abstract fun removeReactInstanceEventListener (Lcom/facebook/react/ReactInstanceEventListener;)V
242+
public fun setBundleSource (Ljava/lang/String;)V
243+
public fun setBundleSource (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
244+
public static synthetic fun setBundleSource$default (Lcom/facebook/react/ReactHost;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
242245
public fun setDevMenuConfiguration (Lcom/facebook/react/devsupport/DevMenuConfiguration;)V
243246
public abstract fun start ()Lcom/facebook/react/interfaces/TaskInterface;
244247
}
@@ -1908,7 +1911,7 @@ public final class com/facebook/react/devsupport/DevMenuConfiguration {
19081911
public class com/facebook/react/devsupport/DevServerHelper {
19091912
public fun <init> (Lcom/facebook/react/modules/debug/interfaces/DeveloperSettings;Landroid/content/Context;Lcom/facebook/react/packagerconnection/PackagerConnectionSettings;)V
19101913
public final fun closeInspectorConnection ()V
1911-
public final fun closePackagerConnection ()V
1914+
public final fun closePackagerConnection ()Landroid/os/AsyncTask;
19121915
public final fun disableDebugger ()V
19131916
public final fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;)V
19141917
public final fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;)V
@@ -1943,6 +1946,7 @@ public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/
19431946
public fun downloadBundleResourceFromUrlSync (Ljava/lang/String;Ljava/io/File;)Ljava/io/File;
19441947
public final fun fetchSplitBundleAndCreateBundleLoader (Ljava/lang/String;Lcom/facebook/react/devsupport/DevSupportManagerBase$CallbackWithBundleLoader;)V
19451948
protected final fun getApplicationContext ()Landroid/content/Context;
1949+
public fun getBundleFilePath ()Ljava/lang/String;
19461950
public fun getCurrentActivity ()Landroid/app/Activity;
19471951
public final fun getCurrentReactContext ()Lcom/facebook/react/bridge/ReactContext;
19481952
public final fun getDevLoadingViewManager ()Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;
@@ -1977,11 +1981,13 @@ public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/
19771981
public fun reloadJSFromServer (Ljava/lang/String;Lcom/facebook/react/devsupport/interfaces/BundleLoadCallback;)V
19781982
public fun reloadSettings ()V
19791983
public fun setAdditionalOptionForPackager (Ljava/lang/String;Ljava/lang/String;)V
1984+
public fun setBundleFilePath (Ljava/lang/String;)V
19801985
public final fun setDevLoadingViewManager (Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;)V
19811986
public fun setDevMenuEnabled (Z)V
19821987
public final fun setDevSupportEnabled (Z)V
19831988
public fun setFpsDebugEnabled (Z)V
19841989
public fun setHotModuleReplacementEnabled (Z)V
1990+
public final fun setJsAppBundleName (Ljava/lang/String;)V
19851991
public fun setKeyboardShortcutsEnabled (Z)V
19861992
public final fun setLastErrorCookie (I)V
19871993
public final fun setLastErrorStack ([Lcom/facebook/react/devsupport/interfaces/StackFrame;)V
@@ -2151,6 +2157,7 @@ public abstract interface class com/facebook/react/devsupport/interfaces/DevSupp
21512157
public abstract fun createSurfaceDelegate (Ljava/lang/String;)Lcom/facebook/react/common/SurfaceDelegate;
21522158
public abstract fun destroyRootView (Landroid/view/View;)V
21532159
public abstract fun downloadBundleResourceFromUrlSync (Ljava/lang/String;Ljava/io/File;)Ljava/io/File;
2160+
public fun getBundleFilePath ()Ljava/lang/String;
21542161
public abstract fun getCurrentActivity ()Landroid/app/Activity;
21552162
public abstract fun getCurrentReactContext ()Lcom/facebook/react/bridge/ReactContext;
21562163
public fun getDevMenuEnabled ()Z
@@ -2180,6 +2187,7 @@ public abstract interface class com/facebook/react/devsupport/interfaces/DevSupp
21802187
public abstract fun reloadJSFromServer (Ljava/lang/String;Lcom/facebook/react/devsupport/interfaces/BundleLoadCallback;)V
21812188
public abstract fun reloadSettings ()V
21822189
public abstract fun setAdditionalOptionForPackager (Ljava/lang/String;Ljava/lang/String;)V
2190+
public fun setBundleFilePath (Ljava/lang/String;)V
21832191
public fun setDevMenuEnabled (Z)V
21842192
public abstract fun setDevSupportEnabled (Z)V
21852193
public abstract fun setFpsDebugEnabled (Z)V
@@ -3021,6 +3029,8 @@ public class com/facebook/react/packagerconnection/PackagerConnectionSettings {
30213029
public fun resetDebugServerHost ()V
30223030
public final fun setAdditionalOptionForPackager (Ljava/lang/String;Ljava/lang/String;)V
30233031
public fun setDebugServerHost (Ljava/lang/String;)V
3032+
public final fun setPackagerOptionsUpdater (Lkotlin/jvm/functions/Function1;)V
3033+
public final fun updatePackagerOptions (Ljava/util/Map;)Ljava/util/Map;
30243034
}
30253035

30263036
public final class com/facebook/react/packagerconnection/ReconnectingWebSocket : okhttp3/WebSocketListener {
@@ -3099,6 +3109,8 @@ public final class com/facebook/react/runtime/ReactHostImpl : com/facebook/react
30993109
public fun reload (Ljava/lang/String;)Lcom/facebook/react/interfaces/TaskInterface;
31003110
public fun removeBeforeDestroyListener (Lkotlin/jvm/functions/Function0;)V
31013111
public fun removeReactInstanceEventListener (Lcom/facebook/react/ReactInstanceEventListener;)V
3112+
public fun setBundleSource (Ljava/lang/String;)V
3113+
public fun setBundleSource (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
31023114
public fun setDevMenuConfiguration (Lcom/facebook/react/devsupport/DevMenuConfiguration;)V
31033115
public fun start ()Lcom/facebook/react/interfaces/TaskInterface;
31043116
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactHost.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,21 @@ public interface ReactHost {
193193

194194
/** Set the DevMenu configuration. */
195195
public fun setDevMenuConfiguration(config: DevMenuConfiguration): Unit = Unit
196+
197+
/** Sets the source of the bundle to be loaded from the file system. */
198+
public fun setBundleSource(filePath: String): Unit = Unit
199+
200+
/**
201+
* Sets the source of the bundle to be loaded from the packager server and updates the packager
202+
* connection.
203+
*
204+
* @param debugServerHost host and port of the server, for example "localhost:8081"
205+
* @param moduleName the module name to load, for example "js/RNTesterApp.android"
206+
* @param queryMapper a function that takes current packager options and returns updated options
207+
*/
208+
public fun setBundleSource(
209+
debugServerHost: String,
210+
moduleName: String,
211+
queryMapper: (Map<String, String>) -> Map<String, String> = { it },
212+
): Unit = Unit
196213
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,16 +196,18 @@ public open class DevServerHelper(
196196
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
197197
}
198198

199-
public fun closePackagerConnection() {
200-
object : AsyncTask<Void, Void, Void>() {
199+
public fun closePackagerConnection(): AsyncTask<Void, Void, Void> {
200+
val task =
201+
object : AsyncTask<Void, Void, Void>() {
201202
@Deprecated("This class needs to be rewritten to don't use AsyncTasks")
202203
override fun doInBackground(vararg params: Void): Void? {
203204
packagerClient?.close()
204205
packagerClient = null
205206
return null
206207
}
207208
}
208-
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
209+
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
210+
return task
209211
}
210212

211213
public fun openInspectorConnection() {
@@ -280,7 +282,11 @@ public open class DevServerHelper(
280282
): String {
281283
val dev = devMode
282284
val additionalOptionsBuilder = StringBuilder()
283-
for ((key, value) in packagerConnectionSettings.additionalOptionsForPackager) {
285+
val packagerOptions =
286+
packagerConnectionSettings.updatePackagerOptions(
287+
packagerConnectionSettings.additionalOptionsForPackager
288+
)
289+
for ((key, value) in packagerOptions) {
284290
if (value.isEmpty()) {
285291
continue
286292
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ import java.util.Locale
8585
public abstract class DevSupportManagerBase(
8686
protected val applicationContext: Context,
8787
public val reactInstanceDevHelper: ReactInstanceDevHelper,
88-
@get:JvmName("getJSAppBundleName") public val jsAppBundleName: String?,
88+
@get:JvmName("getJSAppBundleName") public var jsAppBundleName: String?,
8989
enableOnCreate: Boolean,
9090
public override val redBoxHandler: RedBoxHandler?,
9191
private val devBundleDownloadListener: DevBundleDownloadListener?,
@@ -148,6 +148,12 @@ public abstract class DevSupportManagerBase(
148148
field = value
149149
}
150150

151+
override var bundleFilePath: String? = null
152+
get() = field
153+
set(value) {
154+
field = value
155+
}
156+
151157
override val sourceMapUrl: String
152158
get() = jsAppBundleName?.let { devServerHelper.getSourceMapUrl(it) } ?: ""
153159

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public interface DevSupportManager : JSExceptionHandler {
4848
get() = true
4949
set(value) = Unit
5050

51+
public var bundleFilePath: String?
52+
get() = null
53+
set(value) = Unit
54+
5155
public var devSupportEnabled: Boolean
5256

5357
public fun showNewJavaError(message: String?, e: Throwable)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/PackagerConnectionSettings.kt

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,24 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*/
7-
8-
@file:Suppress("DEPRECATION") // PreferenceManager should be migrated to androidx
9-
107
package com.facebook.react.packagerconnection
118

129
import android.content.Context
13-
import android.content.SharedPreferences
14-
import android.preference.PreferenceManager
1510
import com.facebook.common.logging.FLog
1611
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers
1712

1813
public open class PackagerConnectionSettings(private val appContext: Context) {
19-
private val preferences: SharedPreferences =
20-
PreferenceManager.getDefaultSharedPreferences(appContext)
2114
public val packageName: String = appContext.packageName
2215
private val _additionalOptionsForPackager: MutableMap<String, String> = mutableMapOf()
16+
private var _packagerOptionsUpdater: (Map<String, String>) -> Map<String, String> = { it }
17+
private var cachedHost: String? = null
2318

2419
public open var debugServerHost: String
2520
get() {
26-
// Check host setting first. If empty try to detect emulator type and use default
21+
// Check cached host first. If empty try to detect emulator type and use default
2722
// hostname for those
28-
val hostFromSettings = preferences.getString(PREFS_DEBUG_SERVER_HOST_KEY, null)
29-
if (!hostFromSettings.isNullOrEmpty()) {
30-
return hostFromSettings
23+
cachedHost?.let {
24+
return it
3125
}
3226
val host = AndroidInfoHelpers.getServerHost(appContext)
3327
if (host == AndroidInfoHelpers.DEVICE_LOCALHOST) {
@@ -36,20 +30,29 @@ public open class PackagerConnectionSettings(private val appContext: Context) {
3630
"You seem to be running on device. Run '${AndroidInfoHelpers.getAdbReverseTcpCommand(appContext)}' to forward the debug server's port to the device.",
3731
)
3832
}
33+
34+
cachedHost = host
3935
return host
4036
}
4137
set(host) {
4238
if (host.isEmpty()) {
43-
preferences.edit().remove(PREFS_DEBUG_SERVER_HOST_KEY).apply()
39+
cachedHost = null
4440
} else {
45-
preferences.edit().putString(PREFS_DEBUG_SERVER_HOST_KEY, host).apply()
41+
cachedHost = host
4642
}
4743
}
4844

4945
public open fun resetDebugServerHost() {
50-
preferences.edit().remove(PREFS_DEBUG_SERVER_HOST_KEY).apply()
46+
cachedHost = null
5147
}
5248

49+
public fun setPackagerOptionsUpdater(queryMapper: (Map<String, String>) -> Map<String, String>) {
50+
_packagerOptionsUpdater = queryMapper
51+
}
52+
53+
public fun updatePackagerOptions(options: Map<String, String>): Map<String, String> =
54+
_packagerOptionsUpdater(options)
55+
5356
public fun setAdditionalOptionForPackager(key: String, value: String) {
5457
_additionalOptionsForPackager[key] = value
5558
}
@@ -59,6 +62,5 @@ public open class PackagerConnectionSettings(private val appContext: Context) {
5962

6063
private companion object {
6164
private val TAG = PackagerConnectionSettings::class.java.simpleName
62-
private const val PREFS_DEBUG_SERVER_HOST_KEY = "debug_http_host"
6365
}
6466
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ import java.util.concurrent.atomic.AtomicInteger
7474
import java.util.concurrent.atomic.AtomicReference
7575
import kotlin.Unit
7676
import kotlin.concurrent.Volatile
77+
import kotlinx.coroutines.CoroutineScope
78+
import kotlinx.coroutines.Dispatchers
79+
import kotlinx.coroutines.launch
7780

7881
/**
7982
* A ReactHost is an object that manages a single [ReactInstance]. A ReactHost can be constructed
@@ -650,6 +653,29 @@ public class ReactHostImpl(
650653
}
651654
}
652655

656+
@ThreadConfined(value = ThreadConfined.UI)
657+
override fun setBundleSource(filePath: String) {
658+
devSupportManager.bundleFilePath = filePath
659+
reload("Change bundle source")
660+
}
661+
662+
@ThreadConfined(value = ThreadConfined.UI)
663+
override fun setBundleSource(
664+
debugServerHost: String,
665+
moduleName: String,
666+
queryMapper: (Map<String, String>) -> Map<String, String>,
667+
) {
668+
CoroutineScope(Dispatchers.Default).launch {
669+
(devSupportManager as DevSupportManagerBase).devServerHelper.closePackagerConnection()
670+
devSupportManager.devSettings.packagerConnectionSettings.let { it ->
671+
it.setPackagerOptionsUpdater(queryMapper)
672+
it.debugServerHost = debugServerHost
673+
}
674+
devSupportManager.jsAppBundleName = moduleName
675+
reload("Changed bundle source")
676+
}
677+
}
678+
653679
@ThreadConfined(ThreadConfined.UI)
654680
override fun onConfigurationChanged(context: Context) {
655681
val currentReactContext = this.currentReactContext
@@ -1064,6 +1090,16 @@ public class ReactHostImpl(
10641090
get() {
10651091
stateTracker.enterState("getJSBundleLoader()")
10661092

1093+
if (devSupportManager.bundleFilePath != null) {
1094+
return try {
1095+
Task.forResult(
1096+
JSBundleLoader.createFileLoader(checkNotNull(devSupportManager.bundleFilePath))
1097+
)
1098+
} catch (e: Exception) {
1099+
Task.forError(e)
1100+
}
1101+
}
1102+
10671103
if (useDevSupport && allowPackagerServerAccess) {
10681104
return isMetroRunning.onSuccessTask(
10691105
{ task ->

0 commit comments

Comments
 (0)