Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce top-level Navigator as the entry point for all navigation #41

Merged
merged 14 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class BridgeDelegate(
}

private fun shouldReloadBridge(): Boolean {
return destination.session.isReady && bridge?.isReady() == false
return destination.navigator.session.isReady && bridge?.isReady() == false
}

// Lifecycle events
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/kotlin/dev/hotwire/core/config/HotwireConfig.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package dev.hotwire.core.config

import android.content.Context
import android.webkit.WebView
import dev.hotwire.core.bridge.StradaJsonConverter
import dev.hotwire.core.turbo.http.TurboHttpClient
import dev.hotwire.core.turbo.views.TurboWebView

class HotwireConfig internal constructor() {
/**
Expand Down Expand Up @@ -36,6 +38,15 @@ class HotwireConfig internal constructor() {
WebView.setWebContentsDebuggingEnabled(value)
}

/**
* Called whenever a new WebView instance needs to be (re)created. Provide
* your own implementation and subclass [TurboWebView] if you need
* custom behaviors.
*/
var makeCustomWebView: (context: Context) -> TurboWebView = { context ->
TurboWebView(context, null)
}

/**
* Provides a standard substring to be included in your WebView's user agent
* to identify itself as a Hotwire Native app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package dev.hotwire.core.navigation.activities

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dev.hotwire.core.navigation.session.SessionConfiguration
import dev.hotwire.core.navigation.navigator.NavigatorConfiguration

/**
* Activity that should be implemented by any Activity using Hotwire.
Expand All @@ -11,7 +11,7 @@ abstract class HotwireActivity : AppCompatActivity() {
lateinit var delegate: HotwireActivityDelegate
private set

abstract fun sessionConfigurations(): List<SessionConfiguration>
abstract fun navigatorConfigurations(): List<NavigatorConfiguration>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
package dev.hotwire.core.navigation.activities

import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.annotation.IdRes
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.navigation.navigator.NavigatorHost
import dev.hotwire.core.navigation.navigator.NavigatorConfiguration
import dev.hotwire.core.navigation.navigator.Navigator
import dev.hotwire.core.turbo.observers.HotwireActivityObserver
import dev.hotwire.core.turbo.visit.VisitOptions

/**
* 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: HotwireActivity) {
private val navHostFragments = mutableMapOf<Int, SessionNavHostFragment>()
private val navigatorHosts = mutableMapOf<Int, NavigatorHost>()

private val onBackPressedCallback = object : OnBackPressedCallback(enabled = true) {
override fun handleOnBackPressed() {
navigateBack()
currentNavigator?.pop()
}
}

private var currentNavHostFragmentId = activity.sessionConfigurations().first().navHostFragmentId
private var currentNavigatorHostId = activity.navigatorConfigurations().first().navigatorHostId
set(value) {
field = value
updateOnBackPressedCallback(currentNavHostFragment.navController)
updateOnBackPressedCallback(currentNavigatorHost.navController)
}

/**
Expand All @@ -48,109 +43,61 @@ class HotwireActivityDelegate(val activity: HotwireActivity) {
}

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

/**
* Gets the currently active Fragment destination hosted in the current
* [SessionNavHostFragment].
*/
val currentNavDestination: HotwireNavDestination?
get() = currentFragment as HotwireNavDestination?
val currentNavigator: Navigator?
get() {
return if (currentNavigatorHost.isAdded && !currentNavigatorHost.isDetached) {
currentNavigatorHost.navigator
} else {
null
}
}

/**
* 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.
* Sets the currently active navigator in your Activity. If you use multiple
* [NavigatorHost] instances in your app (such as for bottom tabs),
* you must update this whenever the current navigator changes.
*/
fun setCurrentSession(sessionConfiguration: SessionConfiguration) {
currentNavHostFragmentId = sessionConfiguration.navHostFragmentId
fun setCurrentNavigator(configuration: NavigatorConfiguration) {
currentNavigatorHostId = configuration.navigatorHostId
}

internal fun registerNavHostFragment(navHostFragment: SessionNavHostFragment) {
if (navHostFragments[navHostFragment.id] == null) {
navHostFragments[navHostFragment.id] = navHostFragment
listenToDestinationChanges(navHostFragment.navController)
internal fun registerNavigatorHost(host: NavigatorHost) {
if (navigatorHosts[host.id] == null) {
navigatorHosts[host.id] = host
listenToDestinationChanges(host.navController)
}
}

internal fun unregisterNavHostFragment(navHostFragment: SessionNavHostFragment) {
navHostFragments.remove(navHostFragment.id)
internal fun unregisterNavigatorHost(host: NavigatorHost) {
navigatorHosts.remove(host.id)
}

/**
* Finds the nav host fragment associated with the provided resource ID.
* Finds the navigator host associated with the provided resource ID.
*
* @param navHostFragmentId
* @param navigatorHostId
* @return
*/
fun navHostFragment(@IdRes navHostFragmentId: Int): SessionNavHostFragment {
return requireNotNull(navHostFragments[navHostFragmentId]) {
"No registered SessionNavHostFragment found"
fun navigatorHost(@IdRes navigatorHostId: Int): NavigatorHost {
return requireNotNull(navigatorHosts[navigatorHostId]) {
"No registered NavigatorHost found"
}
}

/**
* Resets the Turbo sessions associated with all registered nav host fragments.
* Resets the sessions associated with all registered navigator hosts.
*/
fun resetSessions() {
navHostFragments.forEach { it.value.session.reset() }
}

/**
* Resets all registered nav host fragments via [SessionNavHostFragment.reset].
*/
fun resetNavHostFragments() {
navHostFragments.forEach { it.value.reset() }
}

/**
* Navigates to the specified location. The resulting destination and its presentation
* will be determined using the path configuration rules.
*
* @param location The location to navigate to.
* @param options Visit options to apply to the visit. (optional)
* @param bundle Bundled arguments to pass to the destination. (optional)
*/
fun navigate(
location: String,
options: VisitOptions = VisitOptions(),
bundle: Bundle? = null
) {
currentNavDestination?.navigate(location, options, bundle)
navigatorHosts.forEach { it.value.navigator.session.reset() }
}

/**
* Navigates up to the previous destination. See [NavController.navigateUp] for
* more details.
* Resets all registered navigators via [Navigator.reset].
*/
fun navigateUp() {
currentNavDestination?.navigateUp()
}

/**
* Navigates back to the previous destination. See [NavController.popBackStack] for
* more details.
*/
fun navigateBack() {
currentNavDestination?.navigateBack()
}

/**
* Clears the navigation back stack to the start destination.
*/
fun clearBackStack(onCleared: () -> Unit = {}) {
currentNavDestination?.clearBackStack(onCleared)
}

/**
* Refresh the current destination. See [HotwireNavDestination.refresh] for
* more details.
*/
fun refresh(displayProgress: Boolean = true) {
currentNavDestination?.refresh(displayProgress)
fun resetNavigators() {
navigatorHosts.forEach { it.value.navigator.reset() }
}

private fun listenToDestinationChanges(navController: NavController) {
Expand All @@ -160,17 +107,11 @@ class HotwireActivityDelegate(val activity: HotwireActivity) {
}

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

private val currentFragment: Fragment?
get() {
return if (currentNavHostFragment.isAdded && !currentNavHostFragment.isDetached) {
currentNavHostFragment.childFragmentManager.primaryNavigationFragment
} else {
null
}
}
private val currentNavigatorHost: NavigatorHost
get() = navigatorHost(currentNavigatorHostId)
}
Loading
Loading