BIT-143: Add initial bottom navigation screen (#25)

Co-authored-by: Brian Yencho <brian@livefront.com>
This commit is contained in:
Ramsey Smith
2023-09-05 14:44:50 -06:00
committed by Álison Fernandes
parent dc48420820
commit 69feff2dcd
16 changed files with 666 additions and 0 deletions

View File

@@ -14,6 +14,8 @@ import androidx.navigation.navOptions
import com.x8bit.bitwarden.ui.auth.feature.auth.authDestinations
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuth
import com.x8bit.bitwarden.ui.platform.components.PlaceholderComposable
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlocked
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedDestinations
/**
* Controls root level [NavHost] for the app.
@@ -31,6 +33,7 @@ fun RootNavScreen(
) {
splashDestinations()
authDestinations(navController)
vaultUnlockedDestinations()
}
// When state changes, navigate to different root navigation state
@@ -43,6 +46,7 @@ fun RootNavScreen(
when (state) {
RootNavState.Auth -> navController.navigateToAuth(rootNavOptions)
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
RootNavState.VaultUnlocked -> navController.navigateToVaultUnlocked(rootNavOptions)
}
}

View File

@@ -31,6 +31,11 @@ class RootNavViewModel @Inject constructor() :
* Models state of the root level navigation of the app.
*/
sealed class RootNavState {
/**
* Show the vault unlocked screen.
*/
data object VaultUnlocked : RootNavState()
/**
* Show the auth screens.
*/

View File

@@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlocked
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination
private const val VAULT_UNLOCKED_ROUTE = "VaultUnlocked"
/**
* Navigate to the vault unlocked screen. Note this will only work if vault unlocked destinations were added
* via [vaultUnlockedDestinations].
*/
fun NavController.navigateToVaultUnlocked(navOptions: NavOptions? = null) {
navigate(VAULT_UNLOCKED_ROUTE, navOptions)
}
/**
* Add vault unlocked destinations to the root nav graph.
*/
fun NavGraphBuilder.vaultUnlockedDestinations() {
navigation(
startDestination = VAULT_UNLOCKED_NAV_BAR_ROUTE,
route = VAULT_UNLOCKED_ROUTE,
) {
vaultUnlockedNavBarDestination()
}
}

View File

@@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedDestinations
/**
* The functions below pertain to entry into the [VaultUnlockedNavBarScreen].
*/
const val VAULT_UNLOCKED_NAV_BAR_ROUTE: String = "VaultUnlockedNavBar"
/**
* Navigate to the vault unlocked nav bar screen.
* Note this will only work if vault unlocked nav bar destination was added
* via [vaultUnlockedDestinations].
*/
fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null) {
navigate(VAULT_UNLOCKED_NAV_BAR_ROUTE, navOptions)
}
/**
* Add vault unlocked destination to the root nav graph.
*/
fun NavGraphBuilder.vaultUnlockedNavBarDestination() {
composable(VAULT_UNLOCKED_NAV_BAR_ROUTE) {
VaultUnlockedNavBarScreen()
}
}

View File

@@ -0,0 +1,327 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import android.os.Parcelable
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.base.util.EventsEffect
import com.x8bit.bitwarden.ui.components.PlaceholderComposable
import kotlinx.parcelize.Parcelize
/**
* Top level composable for the Vault Unlocked Screen.
*/
@Composable
fun VaultUnlockedNavBarScreen(
viewModel: VaultUnlockedNavBarViewModel = viewModel(),
navController: NavHostController = rememberNavController(),
) {
EventsEffect(viewModel = viewModel) { event ->
navController.apply {
val navOptions = vaultUnlockedNavBarScreenNavOptions()
when (event) {
VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar -> navigateToVault(navOptions)
VaultUnlockedNavBarEvent.NavigateToSendScreen -> navigateToSend(navOptions)
VaultUnlockedNavBarEvent.NavigateToGeneratorScreen -> navigateToGenerator(navOptions)
VaultUnlockedNavBarEvent.NavigateToSettingsScreen -> navigateToSettings(navOptions)
}
}
}
VaultUnlockedNavBarScaffold(
navController = navController,
generatorTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) },
sendTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) },
vaultTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) },
settingsTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) },
)
}
/**
* Scaffold that contains the bottom nav bar for the [VaultUnlockedNavBarScreen]
*/
@Composable
private fun VaultUnlockedNavBarScaffold(
navController: NavHostController,
vaultTabClickedAction: () -> Unit,
sendTabClickedAction: () -> Unit,
generatorTabClickedAction: () -> Unit,
settingsTabClickedAction: () -> Unit,
) {
var state by rememberSaveable {
mutableStateOf<VaultUnlockedNavBarTab>(VaultUnlockedNavBarTab.Vault)
}
Scaffold(
bottomBar = {
BottomAppBar {
val destinations = listOf(
VaultUnlockedNavBarTab.Vault,
VaultUnlockedNavBarTab.Send,
VaultUnlockedNavBarTab.Generator,
VaultUnlockedNavBarTab.Settings,
)
destinations.forEach { destination ->
NavigationBarItem(
modifier = Modifier.testTag(destination.route),
icon = {
Icon(
painter = painterResource(id = destination.iconRes),
contentDescription = stringResource(id = destination.contentDescriptionRes),
)
},
label = {
Text(text = stringResource(id = destination.labelRes))
},
selected = destination == state,
onClick = {
state = destination
when (destination) {
VaultUnlockedNavBarTab.Vault -> vaultTabClickedAction()
VaultUnlockedNavBarTab.Send -> sendTabClickedAction()
VaultUnlockedNavBarTab.Generator -> generatorTabClickedAction()
VaultUnlockedNavBarTab.Settings -> settingsTabClickedAction()
}
},
)
}
}
},
) { innerPadding ->
NavHost(
navController = navController,
startDestination = state.route,
modifier = Modifier.padding(innerPadding),
) {
vaultDestination()
sendDestination()
generatorDestination()
settingsDestination()
}
}
}
/**
* Models tabs for the nav bar of the vault unlocked portion of the app.
*/
@Parcelize
private sealed class VaultUnlockedNavBarTab : Parcelable {
/**
* Resource id for the icon representing the tab.
*/
abstract val iconRes: Int
/**
* Resource id for the label describing the tab.
*/
abstract val labelRes: Int
/**
* Resource id for the content description describing the tab.
*/
abstract val contentDescriptionRes: Int
/**
* Route of the tab.
*/
abstract val route: String
/**
* Show the Generator screen.
*/
@Parcelize
data object Generator : VaultUnlockedNavBarTab() {
override val iconRes get() = R.drawable.generator_icon
override val labelRes get() = R.string.generator_label
override val contentDescriptionRes get() = R.string.generator_tab_content_description
override val route get() = GENERATOR_ROUTE
}
/**
* Show the Send screen.
*/
@Parcelize
data object Send : VaultUnlockedNavBarTab() {
override val iconRes get() = R.drawable.send_icon
override val labelRes get() = R.string.send_label
override val contentDescriptionRes get() = R.string.send_tab_content_description
override val route get() = SEND_ROUTE
}
/**
* Show the Vault screen.
*/
@Parcelize
data object Vault : VaultUnlockedNavBarTab() {
override val iconRes get() = R.drawable.sheild_icon
override val labelRes get() = R.string.vault_label
override val contentDescriptionRes get() = R.string.vault_tab_content_description
override val route get() = VAULT_ROUTE
}
/**
* Show the Settings screen.
*/
@Parcelize
data object Settings : VaultUnlockedNavBarTab() {
override val iconRes get() = R.drawable.settings_icon
override val labelRes get() = R.string.settings_label
override val contentDescriptionRes get() = R.string.settings_tab_content_description
override val route get() = SETTINGS_ROUTE
}
}
/**
* Helper function to generate [NavOptions] for [VaultUnlockedNavBarScreen].
*/
private fun NavController.vaultUnlockedNavBarScreenNavOptions(): NavOptions =
navOptions {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
/**
* The functions below should be moved to their respective feature packages once they exist.
*
* For an example of how to setup these nav extensions, see NIA project.
*/
// #region Generator
/**
* TODO: move to generator package (BIT-148)
*/
private const val GENERATOR_ROUTE = "generator"
/**
* Add generator destination to the nav graph.
*
* TODO: move to generator package (BIT-148)
*/
private fun NavGraphBuilder.generatorDestination() {
composable(GENERATOR_ROUTE) {
PlaceholderComposable(text = "Generator")
}
}
/**
* Navigate to the generator screen. Note this will only work if generator screen was added
* via [generatorDestination].
*
* TODO: move to generator package (BIT-148)
*
*/
private fun NavController.navigateToGenerator(navOptions: NavOptions? = null) {
navigate(GENERATOR_ROUTE, navOptions)
}
// #endregion Generator
// #region Send
/**
* TODO: move to send package (BIT-149)
*/
private const val SEND_ROUTE = "send"
/**
* Add send destination to the nav graph.
*
* TODO: move to send package (BIT-149)
*/
private fun NavGraphBuilder.sendDestination() {
composable(SEND_ROUTE) {
PlaceholderComposable(text = "Send")
}
}
/**
* Navigate to the send screen. Note this will only work if send screen was added
* via [sendDestination].
*
* TODO: move to send package (BIT-149)
*
*/
private fun NavController.navigateToSend(navOptions: NavOptions? = null) {
navigate(SEND_ROUTE, navOptions)
}
// #endregion Send
// #region Settings
/**
* TODO: move to settings package (BIT-147)
*/
private const val SETTINGS_ROUTE = "settings"
/**
* Add settings destination to the nav graph.
*
* TODO: move to settings package (BIT-147)
*/
private fun NavGraphBuilder.settingsDestination() {
composable(SETTINGS_ROUTE) {
PlaceholderComposable(text = "Settings")
}
}
/**
* Navigate to the generator screen. Note this will only work if generator screen was added
* via [settingsDestination].
*
* TODO: move to settings package (BIT-147)
*
*/
private fun NavController.navigateToSettings(navOptions: NavOptions? = null) {
navigate(SETTINGS_ROUTE, navOptions)
}
// #endregion Settings
// #region Vault
/**
* TODO: move to vault package (BIT-178)
*/
private const val VAULT_ROUTE = "vault"
/**
* Add vault destination to the nav graph.
*
* TODO: move to vault package (BIT-178)
*/
private fun NavGraphBuilder.vaultDestination() {
composable(VAULT_ROUTE) {
PlaceholderComposable(text = "Vault")
}
}
/**
* Navigate to the vault screen. Note this will only work if vault screen was added
* via [vaultDestination].
*
* TODO: move to vault package (BIT-178)
*
*/
private fun NavController.navigateToVault(navOptions: NavOptions? = null) {
navigate(VAULT_ROUTE, navOptions)
}
// #endregion Vault

View File

@@ -0,0 +1,103 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import com.x8bit.bitwarden.ui.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* Manages bottom tab navigation of the application.
*/
@HiltViewModel
class VaultUnlockedNavBarViewModel @Inject constructor() :
BaseViewModel<Unit, VaultUnlockedNavBarEvent, VaultUnlockedNavBarAction>(
initialState = Unit,
) {
override fun handleAction(action: VaultUnlockedNavBarAction) {
when (action) {
VaultUnlockedNavBarAction.GeneratorTabClick -> handleGeneratorTabClicked()
VaultUnlockedNavBarAction.SendTabClick -> handleSendTabClicked()
VaultUnlockedNavBarAction.SettingsTabClick -> handleSettingsTabClicked()
VaultUnlockedNavBarAction.VaultTabClick -> handleVaultTabClicked()
}
}
// #region BottomTabViewModel Action Handlers
/**
* Attempts to send [VaultUnlockedNavBarEvent.NavigateToGeneratorScreen] event
*/
private fun handleGeneratorTabClicked() {
sendEvent(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen)
}
/**
* Attempts to send [VaultUnlockedNavBarEvent.NavigateToSendScreen] event
*/
private fun handleSendTabClicked() {
sendEvent(VaultUnlockedNavBarEvent.NavigateToSendScreen)
}
/**
* Attempts to send [VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar] event
*/
private fun handleVaultTabClicked() {
sendEvent(VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar)
}
/**
* Attempts to send [VaultUnlockedNavBarEvent.NavigateToSettingsScreen] event
*/
private fun handleSettingsTabClicked() {
sendEvent(VaultUnlockedNavBarEvent.NavigateToSettingsScreen)
}
// #endregion BottomTabViewModel Action Handlers
}
/**
* Models actions for the bottom tab of the vault unlocked portion of the app.
*/
sealed class VaultUnlockedNavBarAction {
/**
* click Generator tab.
*/
data object GeneratorTabClick : VaultUnlockedNavBarAction()
/**
* click Send tab.
*/
data object SendTabClick : VaultUnlockedNavBarAction()
/**
* click Vault tab.
*/
data object VaultTabClick : VaultUnlockedNavBarAction()
/**
* click Settings tab.
*/
data object SettingsTabClick : VaultUnlockedNavBarAction()
}
/**
* Models events for the bottom tab of the vault unlocked portion of the app.
*/
sealed class VaultUnlockedNavBarEvent {
/**
* Navigate to the Generator screen.
*/
data object NavigateToGeneratorScreen : VaultUnlockedNavBarEvent()
/**
* Navigate to the Send screen.
*/
data object NavigateToSendScreen : VaultUnlockedNavBarEvent()
/**
* Navigate to the Vault screen.
*/
data object NavigateToVaultScreenNavBar : VaultUnlockedNavBarEvent()
/**
* Navigate to the Settings screen.
*/
data object NavigateToSettingsScreen : VaultUnlockedNavBarEvent()
}