Skip to content

Commit 9beea0b

Browse files
authored
Merge pull request #24 from hotwired/session-config
Simplify initial app setup with a `SessionConfiguration`
2 parents 3172ece + e626889 commit 9beea0b

File tree

23 files changed

+257
-217
lines changed

23 files changed

+257
-217
lines changed

core/src/main/kotlin/dev/hotwire/core/config/Hotwire.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ object Hotwire {
3131

3232
val config: HotwireConfig = HotwireConfig()
3333

34-
/**
35-
* The base url of your web app.
36-
*/
37-
var appUrl: String = ""
38-
3934
/**
4035
* The path configuration that defines your navigation rules.
4136
*/
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package dev.hotwire.core.navigation.activities
2+
3+
import androidx.appcompat.app.AppCompatActivity
4+
import dev.hotwire.core.navigation.session.SessionConfiguration
5+
import dev.hotwire.core.navigation.session.SessionNavHostFragment
6+
7+
/**
8+
* Interface that should be implemented by any Activity using Turbo. Ensures that the
9+
* Activity provides a [HotwireActivityDelegate] so the framework can initialize the
10+
* [SessionNavHostFragment] hosted in your Activity's layout resource.
11+
*/
12+
interface HotwireActivity {
13+
val delegate: HotwireActivityDelegate
14+
val appCompatActivity: AppCompatActivity
15+
fun sessionConfigurations(): List<SessionConfiguration>
16+
}

core/src/main/kotlin/dev/hotwire/core/turbo/delegates/HotwireActivityDelegate.kt renamed to core/src/main/kotlin/dev/hotwire/core/navigation/activities/HotwireActivityDelegate.kt

Lines changed: 38 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,27 @@
1-
package dev.hotwire.core.turbo.delegates
1+
package dev.hotwire.core.navigation.activities
22

33
import android.os.Bundle
44
import androidx.activity.OnBackPressedCallback
55
import androidx.annotation.IdRes
6-
import androidx.appcompat.app.AppCompatActivity
76
import androidx.fragment.app.Fragment
87
import androidx.navigation.NavController
8+
import dev.hotwire.core.navigation.session.SessionConfiguration
9+
import dev.hotwire.core.navigation.session.SessionNavHostFragment
910
import dev.hotwire.core.turbo.nav.HotwireNavDestination
1011
import dev.hotwire.core.turbo.observers.HotwireActivityObserver
11-
import dev.hotwire.core.turbo.session.SessionNavHostFragment
1212
import dev.hotwire.core.turbo.visit.VisitOptions
1313

1414
/**
15-
* Initializes the Activity for Turbo navigation and provides all the hooks for an
16-
* Activity to communicate with Turbo (and vice versa).
15+
* Initializes the Activity for Hotwire navigation and provides all the hooks for an
16+
* Activity to communicate with Hotwire Native (and vice versa).
1717
*
1818
* @property activity The Activity to bind this delegate to.
1919
* @property currentNavHostFragmentId The resource ID of the [SessionNavHostFragment]
2020
* instance hosted in your Activity's layout resource.
2121
*/
2222
@Suppress("unused", "MemberVisibilityCanBePrivate")
23-
class HotwireActivityDelegate(
24-
val activity: AppCompatActivity,
25-
currentNavHostFragmentId: Int
26-
) {
23+
class HotwireActivityDelegate(val activity: HotwireActivity) {
24+
private val appCompatActivity = activity.appCompatActivity
2725
private val navHostFragments = mutableMapOf<Int, SessionNavHostFragment>()
2826

2927
private val onBackPressedCallback = object : OnBackPressedCallback(enabled = true) {
@@ -32,22 +30,28 @@ class HotwireActivityDelegate(
3230
}
3331
}
3432

35-
/**
36-
* Gets or sets the currently active resource ID of the [SessionNavHostFragment]
37-
* instance hosted in your Activity's layout resource. If you use multiple nav host
38-
* fragments in your app (such as for bottom tabs), you must update this whenever
39-
* the currently active nav host fragment changes.
40-
*/
41-
var currentNavHostFragmentId = currentNavHostFragmentId
33+
private var currentNavHostFragmentId = activity.sessionConfigurations().first().navHostFragmentId
4234
set(value) {
4335
field = value
44-
updateOnBackPressedCallback(currentSessionNavHostFragment.navController)
36+
updateOnBackPressedCallback(currentNavHostFragment.navController)
4537
}
4638

39+
/**
40+
* Initializes the Activity with a BackPressedDispatcher that properly
41+
* handles Fragment navigation with the back button.
42+
*/
43+
init {
44+
appCompatActivity.lifecycle.addObserver(HotwireActivityObserver())
45+
appCompatActivity.onBackPressedDispatcher.addCallback(
46+
owner = appCompatActivity,
47+
onBackPressedCallback = onBackPressedCallback
48+
)
49+
}
50+
4751
/**
4852
* Gets the Activity's currently active [SessionNavHostFragment].
4953
*/
50-
val currentSessionNavHostFragment: SessionNavHostFragment
54+
val currentNavHostFragment: SessionNavHostFragment
5155
get() = navHostFragment(currentNavHostFragmentId)
5256

5357
/**
@@ -58,31 +62,25 @@ class HotwireActivityDelegate(
5862
get() = currentFragment as HotwireNavDestination?
5963

6064
/**
61-
* Registers the provided nav host fragment and initializes the
62-
* Activity with a BackPressedDispatcher that properly handles Fragment
63-
* navigation with the back button.
65+
* Sets the currently active session in your Activity. If you use multiple
66+
* [SessionNavHostFragment] instances in your app (such as for bottom tabs),
67+
* you must update this whenever the current session changes.
6468
*/
65-
init {
66-
registerNavHostFragment(currentNavHostFragmentId)
67-
activity.lifecycle.addObserver(HotwireActivityObserver())
68-
activity.onBackPressedDispatcher.addCallback(activity, onBackPressedCallback)
69+
fun setCurrentSession(sessionConfiguration: SessionConfiguration) {
70+
currentNavHostFragmentId = sessionConfiguration.navHostFragmentId
6971
}
7072

71-
/**
72-
* Provides the ability to register additional nav host fragments.
73-
*
74-
* @param navHostFragmentId
75-
* @return
76-
*/
77-
fun registerNavHostFragment(@IdRes navHostFragmentId: Int): SessionNavHostFragment {
78-
return findNavHostFragment(navHostFragmentId).also {
79-
if (navHostFragments[navHostFragmentId] == null) {
80-
navHostFragments[navHostFragmentId] = it
81-
listenToDestinationChanges(it.navController)
82-
}
73+
internal fun registerNavHostFragment(navHostFragment: SessionNavHostFragment) {
74+
if (navHostFragments[navHostFragment.id] == null) {
75+
navHostFragments[navHostFragment.id] = navHostFragment
76+
listenToDestinationChanges(navHostFragment.navController)
8377
}
8478
}
8579

80+
internal fun unregisterNavHostFragment(navHostFragment: SessionNavHostFragment) {
81+
navHostFragments.remove(navHostFragment.id)
82+
}
83+
8684
/**
8785
* Finds the nav host fragment associated with the provided resource ID.
8886
*
@@ -163,22 +161,17 @@ class HotwireActivityDelegate(
163161
}
164162

165163
private fun updateOnBackPressedCallback(navController: NavController) {
166-
if (navController == currentSessionNavHostFragment.navController) {
164+
if (navController == currentNavHostFragment.navController) {
167165
onBackPressedCallback.isEnabled = navController.previousBackStackEntry != null
168166
}
169167
}
170168

171169
private val currentFragment: Fragment?
172170
get() {
173-
return if (currentSessionNavHostFragment.isAdded && !currentSessionNavHostFragment.isDetached) {
174-
currentSessionNavHostFragment.childFragmentManager.primaryNavigationFragment
171+
return if (currentNavHostFragment.isAdded && !currentNavHostFragment.isDetached) {
172+
currentNavHostFragment.childFragmentManager.primaryNavigationFragment
175173
} else {
176174
null
177175
}
178176
}
179-
180-
private fun findNavHostFragment(@IdRes navHostFragmentId: Int): SessionNavHostFragment {
181-
return activity.supportFragmentManager.findFragmentById(navHostFragmentId) as? SessionNavHostFragment
182-
?: throw IllegalStateException("No SessionNavHostFragment found with ID: $navHostFragmentId")
183-
}
184177
}
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
package dev.hotwire.core.navigation.routing
22

3-
import androidx.appcompat.app.AppCompatActivity
43
import androidx.core.net.toUri
4+
import dev.hotwire.core.navigation.activities.HotwireActivity
5+
import dev.hotwire.core.navigation.session.SessionConfiguration
56

67
class AppNavigationRoute : Router.Route {
78
override val name = "app-navigation"
89

910
override val result = Router.RouteResult.NAVIGATE
1011

11-
override fun matches(location: String): Boolean {
12-
return appUrl.toUri().host == location.toUri().host
12+
override fun matches(
13+
location: String,
14+
sessionConfiguration: SessionConfiguration
15+
): Boolean {
16+
return sessionConfiguration.startLocation.toUri().host == location.toUri().host
1317
}
1418

15-
override fun handle(location: String, activity: AppCompatActivity) {
19+
override fun handle(
20+
location: String,
21+
sessionConfiguration: SessionConfiguration,
22+
activity: HotwireActivity
23+
) {
1624
// No-op
1725
}
1826
}

core/src/main/kotlin/dev/hotwire/core/navigation/routing/BrowserRoute.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,32 @@ package dev.hotwire.core.navigation.routing
22

33
import android.content.ActivityNotFoundException
44
import android.content.Intent
5-
import androidx.appcompat.app.AppCompatActivity
65
import androidx.core.net.toUri
76
import dev.hotwire.core.lib.logging.logError
7+
import dev.hotwire.core.navigation.activities.HotwireActivity
8+
import dev.hotwire.core.navigation.session.SessionConfiguration
89

910
class BrowserRoute : Router.Route {
1011
override val name = "browser"
1112

1213
override val result = Router.RouteResult.STOP
1314

14-
override fun matches(location: String): Boolean {
15-
return appUrl.toUri().host != location.toUri().host
15+
override fun matches(
16+
location: String,
17+
sessionConfiguration: SessionConfiguration
18+
): Boolean {
19+
return sessionConfiguration.startLocation.toUri().host != location.toUri().host
1620
}
1721

18-
override fun handle(location: String, activity: AppCompatActivity) {
22+
override fun handle(
23+
location: String,
24+
sessionConfiguration: SessionConfiguration,
25+
activity: HotwireActivity
26+
) {
1927
val intent = Intent(Intent.ACTION_VIEW, location.toUri())
2028

2129
try {
22-
activity.startActivity(intent)
30+
activity.appCompatActivity.startActivity(intent)
2331
} catch (e: ActivityNotFoundException) {
2432
logError("BrowserRoute", e)
2533
}
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
package dev.hotwire.core.navigation.routing
22

3-
import androidx.appcompat.app.AppCompatActivity
43
import androidx.browser.customtabs.CustomTabColorSchemeParams
54
import androidx.browser.customtabs.CustomTabsIntent
65
import androidx.core.net.toUri
76
import com.google.android.material.R
7+
import dev.hotwire.core.navigation.activities.HotwireActivity
8+
import dev.hotwire.core.navigation.session.SessionConfiguration
89
import dev.hotwire.core.turbo.util.colorFromThemeAttr
910

1011
class BrowserTabRoute : Router.Route {
1112
override val name = "browser-tab"
1213

1314
override val result = Router.RouteResult.STOP
1415

15-
override fun matches(location: String): Boolean {
16-
return appUrl.toUri().host != location.toUri().host
16+
override fun matches(
17+
location: String,
18+
sessionConfiguration: SessionConfiguration
19+
): Boolean {
20+
return sessionConfiguration.startLocation.toUri().host != location.toUri().host
1721
}
1822

19-
override fun handle(location: String, activity: AppCompatActivity) {
20-
val color = activity.colorFromThemeAttr(R.attr.colorSurface)
23+
override fun handle(
24+
location: String,
25+
sessionConfiguration: SessionConfiguration,
26+
activity: HotwireActivity
27+
) {
28+
val color = activity.appCompatActivity.colorFromThemeAttr(R.attr.colorSurface)
2129
val colorParams = CustomTabColorSchemeParams.Builder()
2230
.setToolbarColor(color)
2331
.setNavigationBarColor(color)
@@ -29,6 +37,6 @@ class BrowserTabRoute : Router.Route {
2937
.setUrlBarHidingEnabled(false)
3038
.setDefaultColorSchemeParams(colorParams)
3139
.build()
32-
.launchUrl(activity, location.toUri())
40+
.launchUrl(activity.appCompatActivity, location.toUri())
3341
}
3442
}

core/src/main/kotlin/dev/hotwire/core/navigation/routing/Router.kt

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package dev.hotwire.core.navigation.routing
22

3-
import androidx.appcompat.app.AppCompatActivity
4-
import dev.hotwire.core.config.Hotwire
53
import dev.hotwire.core.lib.logging.logEvent
4+
import dev.hotwire.core.navigation.activities.HotwireActivity
5+
import dev.hotwire.core.navigation.session.SessionConfiguration
66
import dev.hotwire.core.navigation.routing.Router.Route
77

88
/**
@@ -15,12 +15,6 @@ class Router(private val routes: List<Route>) {
1515
* An interface to implement to provide custom route behaviors in your app.
1616
*/
1717
interface Route {
18-
/**
19-
* The configured app url. You can use this to determine if a location
20-
* exists on the same domain.
21-
*/
22-
val appUrl get() = Hotwire.appUrl
23-
2418
/**
2519
* The route name used in debug logging.
2620
*/
@@ -38,13 +32,20 @@ class Router(private val routes: List<Route>) {
3832
* rules based on the location's domain, protocol, path, or any other
3933
* factors.
4034
*/
41-
fun matches(location: String): Boolean
35+
fun matches(
36+
location: String,
37+
sessionConfiguration: SessionConfiguration
38+
): Boolean
4239

4340
/**
4441
* Handle custom routing behavior when a match is found. For example,
4542
* open an external browser or app for external domain urls.
4643
*/
47-
fun handle(location: String, activity: AppCompatActivity)
44+
fun handle(
45+
location: String,
46+
sessionConfiguration: SessionConfiguration,
47+
activity: HotwireActivity
48+
)
4849
}
4950

5051
enum class RouteResult {
@@ -59,15 +60,19 @@ class Router(private val routes: List<Route>) {
5960
STOP
6061
}
6162

62-
internal fun route(location: String, activity: AppCompatActivity): RouteResult {
63+
internal fun route(
64+
location: String,
65+
sessionConfiguration: SessionConfiguration,
66+
activity: HotwireActivity
67+
): RouteResult {
6368
routes.forEach { route ->
64-
if (route.matches(location)) {
69+
if (route.matches(location, sessionConfiguration)) {
6570
logEvent("routeMatch", listOf(
6671
"route" to route.name,
6772
"location" to location
6873
))
6974

70-
route.handle(location, activity)
75+
route.handle(location, sessionConfiguration, activity)
7176
return route.result
7277
}
7378
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package dev.hotwire.core.navigation.session
2+
3+
import androidx.annotation.IdRes
4+
5+
data class SessionConfiguration(
6+
val name: String,
7+
val startLocation: String,
8+
@IdRes val navHostFragmentId: Int,
9+
)

0 commit comments

Comments
 (0)