Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungNew features and improvements in Copilot for JetBrains(23.09.2026 um 02:34 Uhr)
Sichere Programmierung5 Things I Learned From Bad AI Video Generations(23.09.2026 um 02:44 Uhr)
Sichere ProgrammierungWhat Is intent.md in Claude Code? A Practical Guide with an Example(23.09.2026 um 02:48 Uhr)
Sichere ProgrammierungHow to Stop an AI Agent That Lies About Its Own Spending(23.09.2026 um 02:54 Uhr)
Sichere ProgrammierungTask-Seeded Synthetic QA Data Generation for Nemotron Pretraining(23.09.2026 um 03:00 Uhr)
IT Security Toolspentoo-overlay(23.09.2026 um 02:33 Uhr)
Malware / Trojaner / VirenAI malware just removed the human from the attack loop(23.09.2026 um 02:39 Uhr)
IT Security NachrichtenAmazon Slams the door on Meta’s Muse AI Agent(23.09.2026 um 03:02 Uhr)
Sichere ProgrammierungNew features and improvements in Copilot for JetBrains(23.09.2026 um 02:34 Uhr)
Sichere Programmierung5 Things I Learned From Bad AI Video Generations(23.09.2026 um 02:44 Uhr)
Sichere ProgrammierungWhat Is intent.md in Claude Code? A Practical Guide with an Example(23.09.2026 um 02:48 Uhr)
Sichere ProgrammierungHow to Stop an AI Agent That Lies About Its Own Spending(23.09.2026 um 02:54 Uhr)
Sichere ProgrammierungTask-Seeded Synthetic QA Data Generation for Nemotron Pretraining(23.09.2026 um 03:00 Uhr)
IT Security Toolspentoo-overlay(23.09.2026 um 02:33 Uhr)
Malware / Trojaner / VirenAI malware just removed the human from the attack loop(23.09.2026 um 02:39 Uhr)
IT Security NachrichtenAmazon Slams the door on Meta’s Muse AI Agent(23.09.2026 um 03:02 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Hotwire Native - Switch Environments

One thing I have wanted for a while in my Hotwire Native apps is the ability to switch environments (stage, production, etc.) without having to rebuild the app This can be useful for things like: Testing different environments without…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

One thing I have wanted for a while in my Hotwire Native apps is the ability to switch environments (stage, production, etc.) without having to rebuild the app


This can be useful for things like:




  • Testing different environments without having to rebuild the app.


  • Using the currently published app in a different environment.




Note: I don't go into detail on how to create a Hotwire Native app.


There are great resources available for that, like Joe Masilotti's Blog or William Kennedy.


Or more recently, the awesome Hotwire Native Book by Joe Masilotti.





Web



The basic idea is to display a list of environments with their respective URLs.


When the user selects an environment, we will send the selected URL via a JS Bridge Component to the Hotwire Native app.




<div data-controller="native--base-url">
<button data-action="click->native--base-url#updateBaseURL" data-native--base-url-url-param="http://192.168.1.42:3000">Local</button>
<button data-action="click->native--base-url#updateBaseURL" data-native--base-url-url-param="https://example.com">Production</button>
</div>






Note: In a real-world scenario, you may want to store the possible URLs somewhere, iterate over them and highlight which one is currently selected. But for the sake of simplicity, this will do for now.



Bridge Component:




import { BridgeComponent } from "@hotwired/hotwire-native-bridge"

// Connects to data-controller="native--base-url"
export default class extends BridgeComponent {
static component = "base-url"

updateBaseURL({ params: { url } }) {
this.send("updateBaseURL", { url })
}
}






On the Hotwire Native side, we need to handle the message from the JS Bridge and save the selected URL so that we can use it as the base URL for upcoming requests. To store the URL in Android we use SharedPreferences, and for iOS, we can use UserDefaults.


Let's take a look at the code for both platforms.





Hotwire Native Android



First some preparation, we need a way to store the passed URL between app sessions.


Here's how a simple class for accessing SharedPreferences might look:



SharedPreferencesAccess.kt:




object SharedPreferencesAccess {
const val SHARED_PREFERENCES_FILE_KEY = "MobileAppData"
const val BASE_URL_KEY = "BaseURL"

fun setBaseURL(context: Context, baseURL: String) {
val editor = getPreferences(context).edit()
editor.putString(BASE_URL_KEY, baseURL)
editor.apply()
}

fun getBaseURL(context: Context?): String {
return getPreferences(context!!).getString(BASE_URL_KEY, "") ?: ""
}

private fun getPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(SHARED_PREFERENCES_FILE_KEY, Context.MODE_PRIVATE)
}
}






After that, we need a way to build our URLs based on the selected environment and whether we have a saved URL or not


I use a viewmodel called EndpointModel for that:




class EndpointModel(application: Application):AndroidViewModel(application) {
private var baseURL: String

init {
this.baseURL = loadBaseURL()
}

fun setBaseURL(url: String) {
this.baseURL = url
}

private fun loadBaseURL(): String {
val savedURL = SharedPreferencesAccess.getBaseURL(getApplication<Application>().applicationContext)

// Here is the basic idea of this article.
// If we have a saved URL, we use it.
if (savedURL.isNotEmpty()) {
return savedURL
}

// Otherwise we use the default URL based on the build type.
if (BuildConfig.DEBUG) {
return LOCAL_URL
}
return PRODUCTION_URL
}

val startURL: String
get() { return "$baseURL/home" }

val pathConfigurationURL: String
get() {return "$baseURL/api/v1/android/path_configuration.json"}
}






The used constants are defined in a separate file called Constants.kt.



Constants.kt:




const val PRODUCTION_URL = "https://myapp.com"
const val LOCAL_URL = "http://192.168.1.42:3000"






Ok, so far so good.


We have stored the selected URL and have a way to build our URLs based on if we have a saved URL or not.


Now we simply need to tell Hotwire Native to use the selected URL as the start location.



Per default, Hotwire Native expects a startLocation to be defined in the MainActivity.


To access the endpointModel, we have to initialize it first. This can be done in our “MainApplication” class (In the Hotwire Native Demo project, this class is called DemoApplication\):




class MainApplication : Application() {
val endpointModel: EndpointModel by lazy {
ViewModelProvider.AndroidViewModelFactory.getInstance(this)
.create(EndpointModel::class.java)
}

override fun onCreate() {
super.onCreate()

// Load the path configuration
Hotwire.loadPathConfiguration(
context = this,
location = PathConfiguration.Location(
assetFilePath = "json/configuration.json",
remoteFileUrl = endpointModel.pathConfigurationURL
)
)
}
}






With the endpointModel in place, we can use it to define the startLocation in the MainActivity:




class MainActivity : HotwireActivity() {
lateinit var endpointModel: EndpointModel

override fun onCreate(savedInstanceState: Bundle?) {
this.endpointModel = (application as MainApplication).endpointModel

super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

override fun navigatorConfigurations() = listOf(
NavigatorConfiguration(
name = "main",
startLocation = endpointModel.startURL,
navigatorHostId = R.id.main_nav_host
)
)
}






Now we have all the pieces in place to dynamically switch between different environments in our Hotwire Native app.


The only thing missing is the Bridge Component which handles the message from the web and updates the base URL:




class BaseURLComponent(
name: String,
private val hotwireDelegate: BridgeDelegate<HotwireDestination>
) : BridgeComponent<HotwireDestination>(name, hotwireDelegate) {

private val fragment: Fragment
get() = hotwireDelegate.destination.fragment

override fun onReceive(message: Message) {
when (message.event) {
"updateBaseURL" -> updateBaseURL(message)
else -> Log.w("BaseURLComponent", "Unknown event for message: $message")
}
}

private fun updateBaseURL(message: Message) {
val data = message.data<MessageData>() ?: return
val url = data.url

// Save the new base URL to SharedPreferences
SharedPreferencesAccess.setBaseURL(fragment.requireContext(), url)

// Apply the new base URL and reset the navigators
val mainActivity = fragment.activity as? MainActivity
mainActivity?.endpointModel?.setBaseURL(url)
mainActivity?.delegate?.resetNavigators()
}

@Serializable
data class MessageData(
@SerialName("url") val url: String
)
}






Done 🎉


This should outline all the steps needed to switch between different environments in a Hotwire Native Android app.


But there is one catch. When you try this, you may notice that environment switch only works when you restart the app.


Problem is that Hotwire Native doesn't recognize the new base URL as an 'in-app navigation' and will open the URLs with the new base URL in an external browser.


This is because the AppNavigationRouteDecisionHandler doesn't know about the new base URL. Not sure if this is expected behavior or a bug in Hotwire Native Android to be honest. But we easily can fix this by adding a custom Router.RouteDecisionHandler. You can learn more about route handlers at the official documentation.




class NavigationDecisionHandler : Router.RouteDecisionHandler {
override val name = "app-navigation-router"

override val decision = Router.Decision.NAVIGATE

override fun matches(
location: String,
configuration: NavigatorConfiguration
): Boolean {
val baseURL = MainApplication().endpointModel.homeURL
return baseURL.toUri().host == location.toUri().host
}

override fun handle(
location: String,
configuration: NavigatorConfiguration,
activity: HotwireActivity
) {
// No-op
}
}






The only missing piece now, is to inform Hotwire Native about our new router and register it together with the default routers we wanna use. This can be done in the MainApplication.kt file, where we have the other Hotwire Native configuration as well:




// Register route decision handlers
// https://native.hotwired.dev/android/reference#handling-url-routes
Hotwire.registerRouteDecisionHandlers(
NavigationDecisionHandler(),
AppNavigationRouteDecisionHandler(),
BrowserRouteDecisionHandler()
)






Aaand now we are done 🎉🎉.


This time for real.


The iOS side is a bit easier, I promise.





Hotwire Native iOS



The key idea is the same: save the selected URL and use it as the base URL for upcoming requests. A basic UserDefaultsAccess class could look like this:



UserDefaultsAccess:




import Foundation

class UserDefaultsAccess {
static let KEY_BASE_URL = "BaseURL"

private init(){}

static func setBaseURL(url: String) {
UserDefaults.standard.set(url, forKey: KEY_BASE_URL)
}

static func getBaseURL() -> String {
return UserDefaults.standard.string(forKey: KEY_BASE_URL) ?? ""
}
}






Now we need a way to build our URL’s based on the selected environment and whether we have a saved URL or not.


Similar to the Android side, I created a class called Endpoint that contains this logic.



Endpoint:




// Learn more about this Endpoint class at this Joe Masilotti's Blog post: https://masilotti.com/turbo-ios/tips-and-tricks/
import Foundation

class Endpoint {
static let instance = Endpoint()

private var baseURL: URL {
let baseURL = UserDefaultsAccess.getBaseURL()

// Same as in Android.
// If we have a saved URL, we use it.
if !baseURL.isEmpty {
return URL(string: baseURL)!
}

// Otherwise we use the default URL based on the current environment.
switch Environment.current {
case .development:
return URL(string: "http://192.168.1.42:3000")!
case .production:
return URL(string: "https://myapp.com")!
}
}

private init() {}

var start: URL {
return baseURL.appendingPathComponent("/home")
}

var pathConfiguration: URL {
return baseURL.appendingPathComponent("/api/v1/ios/path_configuration.json")
}
}






Wherever you make the initial request:




func didStart() {
navigator.route(Endpoint.instance.start)
}






With that in place, we can implement the Bridge Component that handles the message from the web and updates the base URL.




import Foundation
import HotwireNative
import UIKit

final class BaseURLComponent: BridgeComponent {
override class var name: String { "base-url" }

override func onReceive(message: Message) {
guard let event = Event(rawValue: message.event) else {
return
}

switch event {
case .updateBaseURL:
handleupdateBaseURL(message: message)
}
}

// MARK: Private

private func handleupdateBaseURL(message: Message) {
guard let data: MessageData = message.data() else { return }
let url = data.url
UserDefaultsAccess.setBaseURL(url: url)
HotwireCentral.instance.resetNavigator()
}
}

// MARK: Events

private extension BaseURLComponent {
enum Event: String {
case updateBaseURL
}
}

// MARK: Message data

private extension BaseURLComponent {
struct MessageData: Decodable {
let url: String
}
}






Almost finished. The Bridge Component calls resetNavigator to apply the new base URL and reset the navigators. The job of resetNavigator is to set the new PathConfiguration properties, based on the new base URL, and reinitialize the Navigator to apply the new settings.



This function doesn't exist yet, so we need to add it to the HotwireCentral class:




func resetNavigator() {
self.pathConfiguration = PathConfiguration(sources: [
.server(Endpoint.instance.pathConfiguration)
])
self.navigator = Navigator(pathConfiguration: pathConfiguration)
navigator.route(Endpoint.instance.start)
}






That's it! 🎉


This way you can easily switch between different environments in your Hotwire Native app without having to rebuild the app.



You can find the full code from this post in this PR: https://github.com/leonvogt/example-42/pull/1



If you have any questions, feedback or ideas on how to improve this approach, feel free to reach out!

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Hotwire Native - Switch Environments

Thematisch verwandte Begriffe: Hotwire, Native, Switch, Environments · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-17636 | IBM Financial Transaction Manager (FTM) for RedHat OpenShift could allow…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick