Skip to content

Commit

Permalink
Merge pull request #24 from hotwired/session-config
Browse files Browse the repository at this point in the history
Simplify initial app setup with a `SessionConfiguration`
  • Loading branch information
jayohms committed Mar 22, 2024
2 parents 3172ece + e626889 commit 9beea0b
Show file tree
Hide file tree
Showing 23 changed files with 257 additions and 217 deletions.
5 changes: 0 additions & 5 deletions core/src/main/kotlin/dev/hotwire/core/config/Hotwire.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ object Hotwire {

val config: HotwireConfig = HotwireConfig()

/**
* The base url of your web app.
*/
var appUrl: String = ""

/**
* The path configuration that defines your navigation rules.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.hotwire.core.navigation.activities

import androidx.appcompat.app.AppCompatActivity
import dev.hotwire.core.navigation.session.SessionConfiguration
import dev.hotwire.core.navigation.session.SessionNavHostFragment

/**
* Interface that should be implemented by any Activity using Turbo. Ensures that the
* Activity provides a [HotwireActivityDelegate] so the framework can initialize the
* [SessionNavHostFragment] hosted in your Activity's layout resource.
*/
interface HotwireActivity {
val delegate: HotwireActivityDelegate
val appCompatActivity: AppCompatActivity
fun sessionConfigurations(): List<SessionConfiguration>
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
package dev.hotwire.core.turbo.delegates
package dev.hotwire.core.navigation.activities

import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.annotation.IdRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import dev.hotwire.core.navigation.session.SessionConfiguration
import dev.hotwire.core.navigation.session.SessionNavHostFragment
import dev.hotwire.core.turbo.nav.HotwireNavDestination
import dev.hotwire.core.turbo.observers.HotwireActivityObserver
import dev.hotwire.core.turbo.session.SessionNavHostFragment
import dev.hotwire.core.turbo.visit.VisitOptions

/**
* Initializes the Activity for Turbo navigation and provides all the hooks for an
* Activity to communicate with Turbo (and vice versa).
* Initializes the Activity for Hotwire navigation and provides all the hooks for an
* Activity to communicate with Hotwire Native (and vice versa).
*
* @property activity The Activity to bind this delegate to.
* @property currentNavHostFragmentId The resource ID of the [SessionNavHostFragment]
* instance hosted in your Activity's layout resource.
*/
@Suppress("unused", "MemberVisibilityCanBePrivate")
class HotwireActivityDelegate(
val activity: AppCompatActivity,
currentNavHostFragmentId: Int
) {
class HotwireActivityDelegate(val activity: HotwireActivity) {
private val appCompatActivity = activity.appCompatActivity
private val navHostFragments = mutableMapOf<Int, SessionNavHostFragment>()

private val onBackPressedCallback = object : OnBackPressedCallback(enabled = true) {
Expand All @@ -32,22 +30,28 @@ class HotwireActivityDelegate(
}
}

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

/**
* Initializes the Activity with a BackPressedDispatcher that properly
* handles Fragment navigation with the back button.
*/
init {
appCompatActivity.lifecycle.addObserver(HotwireActivityObserver())
appCompatActivity.onBackPressedDispatcher.addCallback(
owner = appCompatActivity,
onBackPressedCallback = onBackPressedCallback
)
}

/**
* Gets the Activity's currently active [SessionNavHostFragment].
*/
val currentSessionNavHostFragment: SessionNavHostFragment
val currentNavHostFragment: SessionNavHostFragment
get() = navHostFragment(currentNavHostFragmentId)

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

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

/**
* Provides the ability to register additional nav host fragments.
*
* @param navHostFragmentId
* @return
*/
fun registerNavHostFragment(@IdRes navHostFragmentId: Int): SessionNavHostFragment {
return findNavHostFragment(navHostFragmentId).also {
if (navHostFragments[navHostFragmentId] == null) {
navHostFragments[navHostFragmentId] = it
listenToDestinationChanges(it.navController)
}
internal fun registerNavHostFragment(navHostFragment: SessionNavHostFragment) {
if (navHostFragments[navHostFragment.id] == null) {
navHostFragments[navHostFragment.id] = navHostFragment
listenToDestinationChanges(navHostFragment.navController)
}
}

internal fun unregisterNavHostFragment(navHostFragment: SessionNavHostFragment) {
navHostFragments.remove(navHostFragment.id)
}

/**
* Finds the nav host fragment associated with the provided resource ID.
*
Expand Down Expand Up @@ -163,22 +161,17 @@ class HotwireActivityDelegate(
}

private fun updateOnBackPressedCallback(navController: NavController) {
if (navController == currentSessionNavHostFragment.navController) {
if (navController == currentNavHostFragment.navController) {
onBackPressedCallback.isEnabled = navController.previousBackStackEntry != null
}
}

private val currentFragment: Fragment?
get() {
return if (currentSessionNavHostFragment.isAdded && !currentSessionNavHostFragment.isDetached) {
currentSessionNavHostFragment.childFragmentManager.primaryNavigationFragment
return if (currentNavHostFragment.isAdded && !currentNavHostFragment.isDetached) {
currentNavHostFragment.childFragmentManager.primaryNavigationFragment
} else {
null
}
}

private fun findNavHostFragment(@IdRes navHostFragmentId: Int): SessionNavHostFragment {
return activity.supportFragmentManager.findFragmentById(navHostFragmentId) as? SessionNavHostFragment
?: throw IllegalStateException("No SessionNavHostFragment found with ID: $navHostFragmentId")
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
package dev.hotwire.core.navigation.routing

import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import dev.hotwire.core.navigation.activities.HotwireActivity
import dev.hotwire.core.navigation.session.SessionConfiguration

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

override val result = Router.RouteResult.NAVIGATE

override fun matches(location: String): Boolean {
return appUrl.toUri().host == location.toUri().host
override fun matches(
location: String,
sessionConfiguration: SessionConfiguration
): Boolean {
return sessionConfiguration.startLocation.toUri().host == location.toUri().host
}

override fun handle(location: String, activity: AppCompatActivity) {
override fun handle(
location: String,
sessionConfiguration: SessionConfiguration,
activity: HotwireActivity
) {
// No-op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@ package dev.hotwire.core.navigation.routing

import android.content.ActivityNotFoundException
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import dev.hotwire.core.lib.logging.logError
import dev.hotwire.core.navigation.activities.HotwireActivity
import dev.hotwire.core.navigation.session.SessionConfiguration

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

override val result = Router.RouteResult.STOP

override fun matches(location: String): Boolean {
return appUrl.toUri().host != location.toUri().host
override fun matches(
location: String,
sessionConfiguration: SessionConfiguration
): Boolean {
return sessionConfiguration.startLocation.toUri().host != location.toUri().host
}

override fun handle(location: String, activity: AppCompatActivity) {
override fun handle(
location: String,
sessionConfiguration: SessionConfiguration,
activity: HotwireActivity
) {
val intent = Intent(Intent.ACTION_VIEW, location.toUri())

try {
activity.startActivity(intent)
activity.appCompatActivity.startActivity(intent)
} catch (e: ActivityNotFoundException) {
logError("BrowserRoute", e)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package dev.hotwire.core.navigation.routing

import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import com.google.android.material.R
import dev.hotwire.core.navigation.activities.HotwireActivity
import dev.hotwire.core.navigation.session.SessionConfiguration
import dev.hotwire.core.turbo.util.colorFromThemeAttr

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

override val result = Router.RouteResult.STOP

override fun matches(location: String): Boolean {
return appUrl.toUri().host != location.toUri().host
override fun matches(
location: String,
sessionConfiguration: SessionConfiguration
): Boolean {
return sessionConfiguration.startLocation.toUri().host != location.toUri().host
}

override fun handle(location: String, activity: AppCompatActivity) {
val color = activity.colorFromThemeAttr(R.attr.colorSurface)
override fun handle(
location: String,
sessionConfiguration: SessionConfiguration,
activity: HotwireActivity
) {
val color = activity.appCompatActivity.colorFromThemeAttr(R.attr.colorSurface)
val colorParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(color)
.setNavigationBarColor(color)
Expand All @@ -29,6 +37,6 @@ class BrowserTabRoute : Router.Route {
.setUrlBarHidingEnabled(false)
.setDefaultColorSchemeParams(colorParams)
.build()
.launchUrl(activity, location.toUri())
.launchUrl(activity.appCompatActivity, location.toUri())
}
}
31 changes: 18 additions & 13 deletions core/src/main/kotlin/dev/hotwire/core/navigation/routing/Router.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package dev.hotwire.core.navigation.routing

import androidx.appcompat.app.AppCompatActivity
import dev.hotwire.core.config.Hotwire
import dev.hotwire.core.lib.logging.logEvent
import dev.hotwire.core.navigation.activities.HotwireActivity
import dev.hotwire.core.navigation.session.SessionConfiguration
import dev.hotwire.core.navigation.routing.Router.Route

/**
Expand All @@ -15,12 +15,6 @@ class Router(private val routes: List<Route>) {
* An interface to implement to provide custom route behaviors in your app.
*/
interface Route {
/**
* The configured app url. You can use this to determine if a location
* exists on the same domain.
*/
val appUrl get() = Hotwire.appUrl

/**
* The route name used in debug logging.
*/
Expand All @@ -38,13 +32,20 @@ class Router(private val routes: List<Route>) {
* rules based on the location's domain, protocol, path, or any other
* factors.
*/
fun matches(location: String): Boolean
fun matches(
location: String,
sessionConfiguration: SessionConfiguration
): Boolean

/**
* Handle custom routing behavior when a match is found. For example,
* open an external browser or app for external domain urls.
*/
fun handle(location: String, activity: AppCompatActivity)
fun handle(
location: String,
sessionConfiguration: SessionConfiguration,
activity: HotwireActivity
)
}

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

internal fun route(location: String, activity: AppCompatActivity): RouteResult {
internal fun route(
location: String,
sessionConfiguration: SessionConfiguration,
activity: HotwireActivity
): RouteResult {
routes.forEach { route ->
if (route.matches(location)) {
if (route.matches(location, sessionConfiguration)) {
logEvent("routeMatch", listOf(
"route" to route.name,
"location" to location
))

route.handle(location, activity)
route.handle(location, sessionConfiguration, activity)
return route.result
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.hotwire.core.navigation.session

import androidx.annotation.IdRes

data class SessionConfiguration(
val name: String,
val startLocation: String,
@IdRes val navHostFragmentId: Int,
)
Loading

0 comments on commit 9beea0b

Please sign in to comment.