diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/model/NavigationItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/model/NavigationItem.kt index fda308cc64..c88ec80ad5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/model/NavigationItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/model/NavigationItem.kt @@ -25,9 +25,14 @@ interface NavigationItem { val contentDescriptionRes: Int /** - * Route of the tab. + * Route of the tab's graph. */ - val route: String + val graphRoute: String + + /** + * Route of the tab's start destination. + */ + val startDestinationRoute: String /** * The test tag of the tab. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index 82ebaa8e6e..7520391583 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -22,7 +22,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.vault.vaultSettingsDesti import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay const val SETTINGS_GRAPH_ROUTE: String = "settings_graph" -private const val SETTINGS_ROUTE: String = "settings" +const val SETTINGS_ROUTE: String = "settings" /** * Add settings destinations to the nav graph. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index f7df72141b..a9a0e5cd1c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -13,7 +13,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.NavGraph import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.NavOptions @@ -73,32 +72,37 @@ fun VaultUnlockedNavBarScreen( EventsEffect(viewModel = viewModel) { event -> navController.apply { - val navOptions = vaultUnlockedNavBarScreenNavOptions(tabToNavigateTo = event.tab) when (event) { is VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen, is VaultUnlockedNavBarEvent.NavigateToVaultScreen, -> { - navigateToVaultGraph(navOptions) + navigateToTabOrRoot(tabToNavigateTo = event.tab) { + navigateToVaultGraph(navOptions = it) + } } VaultUnlockedNavBarEvent.Shortcut.NavigateToSendScreen, VaultUnlockedNavBarEvent.NavigateToSendScreen, -> { - navigateToSendGraph(navOptions) + navigateToTabOrRoot(tabToNavigateTo = event.tab) { + navigateToSendGraph(navOptions = it) + } } VaultUnlockedNavBarEvent.Shortcut.NavigateToGeneratorScreen, VaultUnlockedNavBarEvent.NavigateToGeneratorScreen, -> { - navigateToGeneratorGraph(navOptions) + navigateToTabOrRoot(tabToNavigateTo = event.tab) { + navigateToGeneratorGraph(navOptions = it) + } } - VaultUnlockedNavBarEvent.NavigateToSettingsScreen -> { - navigateToSettingsGraph(navOptions) - } - - VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen -> { - navigateToSettingsGraph(navOptions) + VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen, + VaultUnlockedNavBarEvent.NavigateToSettingsScreen, + -> { + navigateToTabOrRoot(tabToNavigateTo = event.tab) { + navigateToSettingsGraph(navOptions = it) + } } } } @@ -190,7 +194,7 @@ private fun VaultUnlockedNavBarScaffold( navigationData = ScaffoldNavigationData( navigationItems = navigationItems, selectedNavigationItem = navigationItems.find { - navBackStackEntry.isCurrentRoute(route = it.route) + navBackStackEntry.isCurrentRoute(route = it.graphRoute) }, onNavigationClick = { navigationItem -> when (navigationItem) { @@ -251,34 +255,32 @@ private fun VaultUnlockedNavBarScaffold( } /** - * Helper function to generate [NavOptions] for [VaultUnlockedNavBarScreen]. - * - * @param tabToNavigateTo The [VaultUnlockedNavBarTab] to prepare the NavOptions for. - * NavOptions are determined on whether or not the tab is already selected. + * Helper function to determine how to navigate to a specified [VaultUnlockedNavBarTab]. + * If direct navigation is required, the [navigate] lambda will be invoked with the appropriate + * [NavOptions]. */ -private fun NavController.vaultUnlockedNavBarScreenNavOptions( +private fun NavController.navigateToTabOrRoot( tabToNavigateTo: VaultUnlockedNavBarTab, -): NavOptions { - val returnToCurrentSubRoot = currentBackStackEntry.isCurrentRoute(tabToNavigateTo.route) - val currentSubRootGraph = currentDestination?.parent?.id - // determine the destination to navigate to, if we are navigating to the same sub-root for the - // selected tab we want to find the start destination of the sub-root and pop up to it, which - // will maintain its state (i.e. scroll position). If we are navigating to a different sub-root, - // we can safely pop up to the start of the graph, the "home" tab destination. - val popUpToDestination = graph - .getSubgraphStartDestinationOrNull(currentSubRootGraph) - .takeIf { returnToCurrentSubRoot } - ?: graph.findStartDestination().id - // If we are popping up the start of the whole nav graph we want to maintain the state of the - // the popped destinations in the other sub-roots. If we are navigating to the same sub-root, - // we want to pop off the nested destinations without maintaining their state. - val maintainStateOfPoppedDestinations = !returnToCurrentSubRoot - return navOptions { - popUpTo(popUpToDestination) { - saveState = maintainStateOfPoppedDestinations - } - launchSingleTop = true - restoreState = maintainStateOfPoppedDestinations + navigate: (NavOptions) -> Unit, +) { + if (tabToNavigateTo.startDestinationRoute == currentDestination?.route) { + // We are at the start destination already, so nothing to do. + return + } else if (currentDestination?.parent?.route == tabToNavigateTo.graphRoute) { + // We are not at the start destination but we are in the correct graph, + // so lets pop up to the start destination. + popBackStack(route = tabToNavigateTo.startDestinationRoute, inclusive = false) + } else { + // We are not in correct graph at all, so navigate there. + navigate( + navOptions { + popUpTo(id = graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + }, + ) } } @@ -290,17 +292,3 @@ private fun NavBackStackEntry?.isCurrentRoute(route: String): Boolean = ?.destination ?.hierarchy ?.any { it.route == route } == true - -/** - * Helper function to determine the start destination of a subgraph. - * - * @param subgraphId the id of the subgraph to find the start destination of. - * - * @return the ID of the start destination of the subgraph, or null if the subgraph does not exist. - */ -private fun NavGraph.getSubgraphStartDestinationOrNull(subgraphId: Int?): Int? { - subgraphId ?: return null - return nodes[subgraphId]?.let { - (it as? NavGraph)?.findStartDestination()?.id - } -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/model/VaultUnlockedNavBarTab.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/model/VaultUnlockedNavBarTab.kt index 89c5784d79..28f8d0e6bf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/model/VaultUnlockedNavBarTab.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/model/VaultUnlockedNavBarTab.kt @@ -4,9 +4,13 @@ import android.os.Parcelable import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.model.NavigationItem import com.x8bit.bitwarden.ui.platform.feature.settings.SETTINGS_GRAPH_ROUTE +import com.x8bit.bitwarden.ui.platform.feature.settings.SETTINGS_ROUTE import com.x8bit.bitwarden.ui.tools.feature.generator.GENERATOR_GRAPH_ROUTE +import com.x8bit.bitwarden.ui.tools.feature.generator.GENERATOR_ROUTE import com.x8bit.bitwarden.ui.tools.feature.send.SEND_GRAPH_ROUTE +import com.x8bit.bitwarden.ui.tools.feature.send.SEND_ROUTE import com.x8bit.bitwarden.ui.vault.feature.vault.VAULT_GRAPH_ROUTE +import com.x8bit.bitwarden.ui.vault.feature.vault.VAULT_ROUTE import kotlinx.parcelize.Parcelize /** @@ -29,7 +33,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable { override val iconRes get() = R.drawable.ic_generator override val labelRes get() = R.string.generator override val contentDescriptionRes get() = R.string.generator - override val route get() = GENERATOR_GRAPH_ROUTE + override val graphRoute: String get() = GENERATOR_GRAPH_ROUTE + override val startDestinationRoute get() = GENERATOR_ROUTE override val testTag get() = "GeneratorTab" override val notificationCount get() = 0 } @@ -43,7 +48,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable { override val iconRes get() = R.drawable.ic_send override val labelRes get() = R.string.send override val contentDescriptionRes get() = R.string.send - override val route get() = SEND_GRAPH_ROUTE + override val graphRoute: String get() = SEND_GRAPH_ROUTE + override val startDestinationRoute get() = SEND_ROUTE override val testTag get() = "SendTab" override val notificationCount get() = 0 } @@ -58,7 +64,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable { ) : VaultUnlockedNavBarTab() { override val iconResSelected get() = R.drawable.ic_vault_filled override val iconRes get() = R.drawable.ic_vault - override val route get() = VAULT_GRAPH_ROUTE + override val graphRoute: String get() = VAULT_GRAPH_ROUTE + override val startDestinationRoute get() = VAULT_ROUTE override val testTag get() = "VaultTab" override val notificationCount get() = 0 } @@ -74,7 +81,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable { override val iconRes get() = R.drawable.ic_settings override val labelRes get() = R.string.settings override val contentDescriptionRes get() = R.string.settings - override val route get() = SETTINGS_GRAPH_ROUTE + override val graphRoute: String get() = SETTINGS_GRAPH_ROUTE + override val startDestinationRoute get() = SETTINGS_ROUTE override val testTag get() = "SettingsTab" } }