mirror of
https://github.com/bitwarden/android.git
synced 2026-06-07 06:49:07 -05:00
BIT-143: Add initial bottom navigation screen (#25)
Co-authored-by: Brian Yencho <brian@livefront.com>
This commit is contained in:
committed by
Álison Fernandes
parent
dc48420820
commit
69feff2dcd
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user