Skip to content
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
6 changes: 5 additions & 1 deletion demo/src/main/kotlin/dev/hotwire/demo/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ class MainActivity : HotwireActivity() {
private fun initializeBottomTabs() {
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)

bottomNavigationController = HotwireBottomNavigationController(this, bottomNavigationView)
bottomNavigationController = HotwireBottomNavigationController(
activity = this,
view = bottomNavigationView,
lazyLoadTabs = true
)
bottomNavigationController.load(mainTabs, viewModel.selectedTabIndex)
bottomNavigationController.setOnTabSelectedListener { index, _ ->
viewModel.selectedTabIndex = index
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import dev.hotwire.navigation.observers.HotwireActivityObserver
@Suppress("unused", "MemberVisibilityCanBePrivate")
class HotwireActivityDelegate(val activity: HotwireActivity) {
private val navigatorHosts = mutableMapOf<Int, NavigatorHost>()
private val lazyNavigatorHostIds = mutableSetOf<Int>()

private val onBackPressedCallback = object : OnBackPressedCallback(enabled = true) {
override fun handleOnBackPressed() {
Expand Down Expand Up @@ -66,10 +67,22 @@ class HotwireActivityDelegate(val activity: HotwireActivity) {

val navigatorHost = navigatorHosts[currentNavigatorHostId]
if (navigatorHost != null) {
navigatorHost.initControllerGraphIfNeeded()
updateOnBackPressedCallback(navigatorHost)
}
}

/**
* Sets the given navigator hosts as lazy, meaning their start destination
* won't be loaded until the host first becomes the current navigator (e.g.
* when its bottom tab is first selected). Any host not marked as lazy is
* loaded eagerly as soon as its view is created.
*/
internal fun setLazyNavigatorHosts(navigatorHostIds: Collection<Int>) {
lazyNavigatorHostIds.clear()
lazyNavigatorHostIds.addAll(navigatorHostIds)
}

internal fun registerNavigatorHost(host: NavigatorHost) {
logEvent("navigatorRegistered", listOf("navigator" to host.navigator.configuration.name))

Expand All @@ -80,6 +93,13 @@ class HotwireActivityDelegate(val activity: HotwireActivity) {
if (currentNavigatorHostId == host.id) {
updateOnBackPressedCallback(host)
}

// Load the host's start destination unless it's a lazy host that
// isn't currently selected. Lazy hosts are loaded when they first
// become the current navigator.
if (host.id !in lazyNavigatorHostIds || currentNavigatorHostId == host.id) {
host.initControllerGraphIfNeeded()
}
}
}

Expand All @@ -106,16 +126,22 @@ class HotwireActivityDelegate(val activity: HotwireActivity) {

/**
* Resets the sessions associated with all registered navigator hosts.
* Hosts that haven't loaded their start destination yet are skipped.
*/
fun resetSessions() {
navigatorHosts.forEach { it.value.navigator.session.reset() }
navigatorHosts.values
.filter { it.isGraphInitialized }
.forEach { it.navigator.session.reset() }
}

/**
* Resets all registered navigators via [Navigator.reset].
* Hosts that haven't loaded their start destination yet are skipped.
*/
fun resetNavigators() {
navigatorHosts.forEach { it.value.navigator.reset() }
navigatorHosts.values
.filter { it.isGraphInitialized }
.forEach { it.navigator.reset() }
}

private fun listenToDestinationChanges(host: NavigatorHost) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ class Navigator(
navigateWhenReady {
clearAll {
session.reset()
host.initControllerGraph()
host.resetControllerGraph()

if (host.view == null) {
onReset()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener {
lateinit var navigator: Navigator
private set

/**
* Whether the navigation graph has been built and the start destination
* loaded. See [initControllerGraphIfNeeded].
*/
internal var isGraphInitialized = false
private set

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

activity = requireActivity() as HotwireActivity
navigator = Navigator(this, configuration, activity)
childFragmentManager.addFragmentOnAttachListener(this)

initControllerGraph()
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand All @@ -53,10 +58,29 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener {
* has not been created yet.
*/
fun isReady(): Boolean {
return isAdded && !isDetached && childFragmentManager.primaryNavigationFragment != null
return isGraphInitialized
&& isAdded
&& !isDetached
&& childFragmentManager.primaryNavigationFragment != null
}

/**
* Builds the navigation graph and loads the start destination if it hasn't
* been built already. This is idempotent, so it's safe to call whenever the
* host may need to become ready for navigation (e.g. when its tab is selected).
*/
internal fun initControllerGraphIfNeeded() {
if (!isGraphInitialized) {
initControllerGraph()
}
}

internal fun resetControllerGraph() {
initControllerGraph()
}

internal fun initControllerGraph() {
private fun initControllerGraph() {
isGraphInitialized = true
ensureDeeplinkStartLocationValid()

navController.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,28 @@ import dev.hotwire.navigation.navigator.presentationContext
/**
* A [BottomNavigationView] controller that manages multiple [HotwireBottomTab]s, each associated
* with its own [NavigatorHost] instance in the Activity layout.
*
* @param activity The [HotwireActivity] that hosts the [BottomNavigationView] and its tabs.
* @param view The [BottomNavigationView] in your layout to manage.
* @param initialVisibility The initial [Visibility] mode for the [BottomNavigationView]. Defaults
* to [Visibility.DEFAULT].
* @param clearNavigationOnTabReselection When `true` (the default), reselecting the already-active
* tab clears its navigation backstack to the start destination. When `false`, reselecting a tab
* has no effect.
* @param animateVisibilityChanges When `true` (the default), the [BottomNavigationView] animates in
* and out when its visibility changes. When `false`, visibility changes are applied instantly.
* @param lazyLoadTabs When `false` (the default), every tab's [NavigatorHost] loads its
* start location as soon as its view is created, so all tabs load up front. When `true`, a tab's
* start location is not loaded until that tab is first selected, avoiding loading (and creating a
* web view visit for) every tab at once. The initially selected tab still loads immediately.
*/
class HotwireBottomNavigationController(
val activity: HotwireActivity,
val view: BottomNavigationView,
val initialVisibility: Visibility = Visibility.DEFAULT,
val clearNavigationOnTabReselection: Boolean = true,
val animateVisibilityChanges: Boolean = true
val animateVisibilityChanges: Boolean = true,
val lazyLoadTabs: Boolean = false
) : NavController.OnDestinationChangedListener {

/**
Expand Down Expand Up @@ -100,6 +115,13 @@ class HotwireBottomNavigationController(

this.tabs = tabs

// Mark the tab hosts as lazy before their views are created and
// registered with the delegate. The initially selected tab is still
// loaded immediately when it becomes the current navigator.
if (lazyLoadTabs) {
activity.delegate.setLazyNavigatorHosts(tabs.map { it.configuration.navigatorHostId })
}

val initialIndex = selectedTabIndex.coerceIn(0, tabs.lastIndex)
val initialTab = tabs[initialIndex]

Expand Down
Loading