PM-21348: Type-safe navigation for authenticator (#5156)

This commit is contained in:
David Perez
2025-05-08 11:31:10 -05:00
committed by GitHub
parent 9c7270df69
commit d7671f47ea
21 changed files with 185 additions and 589 deletions

View File

@@ -4,8 +4,13 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import kotlinx.serialization.Serializable
const val UNLOCK_ROUTE: String = "unlock"
/**
* The type-safe route for the unlock screen.
*/
@Serializable
data object UnlockRoute
/**
* Navigate to the unlock screen.
@@ -13,7 +18,7 @@ const val UNLOCK_ROUTE: String = "unlock"
fun NavController.navigateToUnlock(
navOptions: NavOptions? = null,
) {
navigate(route = UNLOCK_ROUTE, navOptions = navOptions)
navigate(route = UnlockRoute, navOptions = navOptions)
}
/**
@@ -22,7 +27,7 @@ fun NavController.navigateToUnlock(
fun NavGraphBuilder.unlockDestination(
onUnlocked: () -> Unit,
) {
composable(route = UNLOCK_ROUTE) {
composable<UnlockRoute> {
UnlockScreen(onUnlocked = onUnlocked)
}
}

View File

@@ -7,21 +7,26 @@ import androidx.navigation.navigation
import com.bitwarden.authenticator.ui.authenticator.feature.edititem.navigateToEditItem
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.itemListingGraph
import com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry.navigateToManualCodeEntryScreen
import com.bitwarden.authenticator.ui.authenticator.feature.navbar.AUTHENTICATOR_NAV_BAR_ROUTE
import com.bitwarden.authenticator.ui.authenticator.feature.navbar.AuthenticatorNavbarRoute
import com.bitwarden.authenticator.ui.authenticator.feature.navbar.authenticatorNavBarDestination
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.navigateToQrCodeScanScreen
import com.bitwarden.authenticator.ui.authenticator.feature.search.navigateToSearch
import com.bitwarden.authenticator.ui.platform.feature.settings.export.navigateToExport
import com.bitwarden.authenticator.ui.platform.feature.settings.importing.navigateToImporting
import com.bitwarden.authenticator.ui.platform.feature.tutorial.navigateToSettingsTutorial
import kotlinx.serialization.Serializable
const val AUTHENTICATOR_GRAPH_ROUTE = "authenticator_graph"
/**
* The type-safe route for the authenticator graph.
*/
@Serializable
data object AuthenticatorGraphRoute
/**
* Navigate to the authenticator graph
*/
fun NavController.navigateToAuthenticatorGraph(navOptions: NavOptions? = null) {
navigate(AUTHENTICATOR_NAV_BAR_ROUTE, navOptions)
navigate(route = AuthenticatorNavbarRoute, navOptions = navOptions)
}
/**
@@ -31,9 +36,8 @@ fun NavGraphBuilder.authenticatorGraph(
navController: NavController,
onNavigateBack: () -> Unit,
) {
navigation(
startDestination = AUTHENTICATOR_NAV_BAR_ROUTE,
route = AUTHENTICATOR_GRAPH_ROUTE,
navigation<AuthenticatorGraphRoute>(
startDestination = AuthenticatorNavbarRoute,
) {
authenticatorNavBarDestination(
onNavigateBack = onNavigateBack,

View File

@@ -4,23 +4,31 @@ import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.authenticator.ui.platform.base.util.composableWithPushTransitions
import androidx.navigation.toRoute
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
private const val EDIT_ITEM_PREFIX = "edit_item"
private const val EDIT_ITEM_ITEM_ID = "item_id"
private const val EDIT_ITEM_ROUTE = "$EDIT_ITEM_PREFIX/{$EDIT_ITEM_ITEM_ID}"
/**
* The type-safe route for the edit item screen.
*/
@Serializable
data class EditItemRoute(
val itemId: String,
)
/**
* Class to retrieve authenticator item arguments from the [SavedStateHandle].
*
* @property itemId ID of the item to be edited.
*/
data class EditItemArgs(val itemId: String) {
constructor(savedStateHandle: SavedStateHandle) : this(
checkNotNull(savedStateHandle[EDIT_ITEM_ITEM_ID]) as String,
)
data class EditItemArgs(val itemId: String)
/**
* Constructs a [EditItemArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toEditItemArgs(): EditItemArgs {
val route = this.toRoute<EditItemRoute>()
return EditItemArgs(itemId = route.itemId)
}
/**
@@ -29,12 +37,7 @@ data class EditItemArgs(val itemId: String) {
fun NavGraphBuilder.editItemDestination(
onNavigateBack: () -> Unit = { },
) {
composableWithPushTransitions(
route = EDIT_ITEM_ROUTE,
arguments = listOf(
navArgument(EDIT_ITEM_ITEM_ID) { type = NavType.StringType },
),
) {
composableWithPushTransitions<EditItemRoute> {
EditItemScreen(
onNavigateBack = onNavigateBack,
)
@@ -49,7 +52,7 @@ fun NavController.navigateToEditItem(
navOptions: NavOptions? = null,
) {
navigate(
route = "$EDIT_ITEM_PREFIX/$itemId",
route = EditItemRoute(itemId = itemId),
navOptions = navOptions,
)
}

View File

@@ -64,8 +64,8 @@ import com.bitwarden.authenticator.ui.platform.components.model.IconData
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.components.stepper.BitwardenStepper
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.authenticator.ui.platform.theme.DEFAULT_FADE_TRANSITION_TIME_MS
import com.bitwarden.authenticator.ui.platform.theme.DEFAULT_STAY_TRANSITION_TIME_MS
import com.bitwarden.ui.platform.theme.DEFAULT_FADE_TRANSITION_TIME_MS
import com.bitwarden.ui.platform.theme.DEFAULT_STAY_TRANSITION_TIME_MS
import kotlinx.collections.immutable.toImmutableList
/**

View File

@@ -42,7 +42,7 @@ class EditItemViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<EditItemState, EditItemEvent, EditItemAction>(
initialState = savedStateHandle[KEY_STATE] ?: EditItemState(
itemId = EditItemArgs(savedStateHandle).itemId,
itemId = savedStateHandle.toEditItemArgs().itemId,
viewState = EditItemState.ViewState.Loading,
dialog = null,
),

View File

@@ -11,8 +11,13 @@ import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.navigateT
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.qrCodeScanDestination
import com.bitwarden.authenticator.ui.authenticator.feature.search.itemSearchDestination
import com.bitwarden.authenticator.ui.platform.feature.settings.settingsGraph
import kotlinx.serialization.Serializable
const val ITEM_LISTING_GRAPH_ROUTE = "item_listing_graph"
/**
* The type-safe route for the item listing graph.
*/
@Serializable
data object ItemListingGraphRoute
/**
* Add the item listing graph to the nav graph.
@@ -29,9 +34,8 @@ fun NavGraphBuilder.itemListingGraph(
navigateToImport: () -> Unit,
navigateToTutorial: () -> Unit,
) {
navigation(
route = ITEM_LISTING_GRAPH_ROUTE,
startDestination = ITEM_LIST_ROUTE,
navigation<ItemListingGraphRoute>(
startDestination = ItemListingRoute,
) {
itemListingDestination(
onNavigateBack = navigateBack,
@@ -76,7 +80,7 @@ fun NavController.navigateToItemListGraph(
navOptions: NavOptions? = null,
) {
navigate(
route = ITEM_LISTING_GRAPH_ROUTE,
route = ItemListingGraphRoute,
navOptions = navOptions,
)
}

View File

@@ -1,9 +1,14 @@
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
import androidx.navigation.NavGraphBuilder
import com.bitwarden.authenticator.ui.platform.base.util.composableWithPushTransitions
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
const val ITEM_LIST_ROUTE = "item_list"
/**
* The type-safe route for the item listing screen.
*/
@Serializable
data object ItemListingRoute
/**
* Add the item listing screen to the nav graph.
@@ -15,9 +20,7 @@ fun NavGraphBuilder.itemListingDestination(
onNavigateToManualKeyEntry: () -> Unit = { },
onNavigateToEditItemScreen: (id: String) -> Unit = { },
) {
composableWithPushTransitions(
route = ITEM_LIST_ROUTE,
) {
composableWithPushTransitions<ItemListingRoute> {
ItemListingScreen(
onNavigateBack = onNavigateBack,
onNavigateToSearch = onNavigateToSearch,

View File

@@ -3,9 +3,14 @@ package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.authenticator.ui.platform.base.util.composableWithSlideTransitions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable
private const val MANUAL_CODE_ENTRY_ROUTE: String = "manual_code_entry"
/**
* The type-safe route for the manual code entry screen.
*/
@Serializable
data object ManualCodeEntryRoute
/**
* Add the manual code entry screen to the nav graph.
@@ -14,9 +19,7 @@ fun NavGraphBuilder.manualCodeEntryDestination(
onNavigateBack: () -> Unit,
onNavigateToQrCodeScreen: () -> Unit,
) {
composableWithSlideTransitions(
route = MANUAL_CODE_ENTRY_ROUTE,
) {
composableWithSlideTransitions<ManualCodeEntryRoute> {
ManualCodeEntryScreen(
onNavigateBack = onNavigateBack,
onNavigateToQrCodeScreen = onNavigateToQrCodeScreen,
@@ -30,5 +33,5 @@ fun NavGraphBuilder.manualCodeEntryDestination(
fun NavController.navigateToManualCodeEntryScreen(
navOptions: NavOptions? = null,
) {
this.navigate(MANUAL_CODE_ENTRY_ROUTE, navOptions)
this.navigate(route = ManualCodeEntryRoute, navOptions = navOptions)
}

View File

@@ -1,9 +1,14 @@
package com.bitwarden.authenticator.ui.authenticator.feature.navbar
import androidx.navigation.NavGraphBuilder
import com.bitwarden.authenticator.ui.platform.base.util.composableWithStayTransitions
import com.bitwarden.ui.platform.base.util.composableWithStayTransitions
import kotlinx.serialization.Serializable
const val AUTHENTICATOR_NAV_BAR_ROUTE: String = "AuthenticatorNavBarRoute"
/**
* The type-safe route for the authenticator navbar screen.
*/
@Serializable
data object AuthenticatorNavbarRoute
/**
* Add the authenticator nav bar to the nav graph.
@@ -19,9 +24,7 @@ fun NavGraphBuilder.authenticatorNavBarDestination(
onNavigateToImport: () -> Unit,
onNavigateToTutorial: () -> Unit,
) {
composableWithStayTransitions(
route = AUTHENTICATOR_NAV_BAR_ROUTE,
) {
composableWithStayTransitions<AuthenticatorNavbarRoute> {
AuthenticatorNavBarScreen(
onNavigateBack = onNavigateBack,
onNavigateToSearch = onNavigateToSearch,

View File

@@ -43,8 +43,8 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ITEM_LISTING_GRAPH_ROUTE
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ITEM_LIST_ROUTE
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingGraphRoute
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingRoute
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.itemListingGraph
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.navigateToItemListGraph
import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect
@@ -52,9 +52,10 @@ import com.bitwarden.authenticator.ui.platform.base.util.max
import com.bitwarden.authenticator.ui.platform.base.util.toDp
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.components.scrim.BitwardenAnimatedScrim
import com.bitwarden.authenticator.ui.platform.feature.settings.SETTINGS_GRAPH_ROUTE
import com.bitwarden.authenticator.ui.platform.feature.settings.SettingsGraphRoute
import com.bitwarden.authenticator.ui.platform.feature.settings.navigateToSettingsGraph
import com.bitwarden.authenticator.ui.platform.theme.RootTransitionProviders
import com.bitwarden.ui.platform.theme.RootTransitionProviders
import com.bitwarden.ui.platform.util.toObjectNavigationRoute
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
@@ -161,7 +162,7 @@ private fun AuthenticatorNavBarScaffold(
) { innerPadding ->
NavHost(
navController = navController,
startDestination = ITEM_LISTING_GRAPH_ROUTE,
startDestination = ItemListingGraphRoute,
modifier = Modifier
.consumeWindowInsets(WindowInsets.navigationBars)
.consumeWindowInsets(WindowInsets.ime)
@@ -309,7 +310,7 @@ private sealed class AuthenticatorNavBarTab : Parcelable {
override val iconRes get() = R.drawable.ic_verification_codes
override val labelRes get() = R.string.verification_codes
override val contentDescriptionRes get() = R.string.verification_codes
override val route get() = ITEM_LIST_ROUTE
override val route get() = ItemListingRoute.toObjectNavigationRoute()
override val testTag get() = "VerificationCodesTab"
}
@@ -322,9 +323,7 @@ private sealed class AuthenticatorNavBarTab : Parcelable {
override val iconRes get() = R.drawable.ic_settings
override val labelRes get() = R.string.settings
override val contentDescriptionRes get() = R.string.settings
// TODO: Replace with constant when settings screen is complete.
override val route get() = SETTINGS_GRAPH_ROUTE
override val route get() = SettingsGraphRoute.toObjectNavigationRoute()
override val testTag get() = "SettingsTab"
}
}

View File

@@ -3,9 +3,14 @@ package com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.authenticator.ui.platform.base.util.composableWithSlideTransitions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable
private const val QR_CODE_SCAN_ROUTE: String = "qr_code_scan"
/**
* The type-safe route for the QR code scan screen.
*/
@Serializable
data object QrCodeScanRoute
/**
* Add the QR code scan screen to the nav graph.
@@ -14,9 +19,7 @@ fun NavGraphBuilder.qrCodeScanDestination(
onNavigateBack: () -> Unit,
onNavigateToManualCodeEntryScreen: () -> Unit,
) {
composableWithSlideTransitions(
route = QR_CODE_SCAN_ROUTE,
) {
composableWithSlideTransitions<QrCodeScanRoute> {
QrCodeScanScreen(
onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen,
onNavigateBack = onNavigateBack,
@@ -30,5 +33,5 @@ fun NavGraphBuilder.qrCodeScanDestination(
fun NavController.navigateToQrCodeScanScreen(
navOptions: NavOptions? = null,
) {
this.navigate(QR_CODE_SCAN_ROUTE, navOptions)
this.navigate(route = QrCodeScanRoute, navOptions = navOptions)
}

View File

@@ -2,9 +2,14 @@ package com.bitwarden.authenticator.ui.authenticator.feature.search
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import com.bitwarden.authenticator.ui.platform.base.util.composableWithSlideTransitions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable
const val ITEM_SEARCH_ROUTE = "item_search"
/**
* The type-safe route for the item search screen.
*/
@Serializable
data object ItemSearchRoute
/**
* Add item search destination to the nav graph.
@@ -12,9 +17,7 @@ const val ITEM_SEARCH_ROUTE = "item_search"
fun NavGraphBuilder.itemSearchDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = ITEM_SEARCH_ROUTE,
) {
composableWithSlideTransitions<ItemSearchRoute> {
ItemSearchScreen(
onNavigateBack = onNavigateBack,
)
@@ -25,5 +28,5 @@ fun NavGraphBuilder.itemSearchDestination(
* Navigate to the item search screen.
*/
fun NavController.navigateToSearch() {
navigate(route = ITEM_SEARCH_ROUTE)
navigate(route = ItemSearchRoute)
}

View File

@@ -1,98 +0,0 @@
package com.bitwarden.authenticator.ui.platform.base.util
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.runtime.Composable
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.bitwarden.authenticator.ui.platform.theme.TransitionProviders
/**
* A wrapper around [NavGraphBuilder.composable] that supplies slide up/down transitions.
*/
fun NavGraphBuilder.composableWithSlideTransitions(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
this.composable(
route = route,
arguments = arguments,
deepLinks = deepLinks,
enterTransition = TransitionProviders.Enter.slideUp,
exitTransition = TransitionProviders.Exit.stay,
popEnterTransition = TransitionProviders.Enter.stay,
popExitTransition = TransitionProviders.Exit.slideDown,
content = content,
)
}
/**
* A wrapper around [NavGraphBuilder.composable] that supplies "stay" transitions.
*/
fun NavGraphBuilder.composableWithStayTransitions(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
this.composable(
route = route,
arguments = arguments,
deepLinks = deepLinks,
enterTransition = TransitionProviders.Enter.stay,
exitTransition = TransitionProviders.Exit.stay,
popEnterTransition = TransitionProviders.Enter.stay,
popExitTransition = TransitionProviders.Exit.stay,
content = content,
)
}
/**
* A wrapper around [NavGraphBuilder.composable] that supplies push transitions.
*
* This is suitable for screens deeper within a hierarchy that uses push transitions; the root
* screen of such a hierarchy should use [composableWithRootPushTransitions].
*/
fun NavGraphBuilder.composableWithPushTransitions(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
this.composable(
route = route,
arguments = arguments,
deepLinks = deepLinks,
enterTransition = TransitionProviders.Enter.pushLeft,
exitTransition = TransitionProviders.Exit.stay,
popEnterTransition = TransitionProviders.Enter.stay,
popExitTransition = TransitionProviders.Exit.pushRight,
content = content,
)
}
/**
* A wrapper around [NavGraphBuilder.composable] that supplies push transitions to the root screen
* in a nested graph that uses push transitions.
*/
fun NavGraphBuilder.composableWithRootPushTransitions(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
this.composable(
route = route,
arguments = arguments,
deepLinks = deepLinks,
enterTransition = TransitionProviders.Enter.stay,
exitTransition = TransitionProviders.Exit.pushLeft,
popEnterTransition = TransitionProviders.Enter.pushRight,
popExitTransition = TransitionProviders.Exit.fadeOut,
content = content,
)
}

View File

@@ -2,15 +2,20 @@ package com.bitwarden.authenticator.ui.platform.feature.debugmenu
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import com.bitwarden.authenticator.ui.platform.base.util.composableWithPushTransitions
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
private const val DEBUG_MENU = "debug_menu"
/**
* The type-safe route for the debug screen.
*/
@Serializable
data object DebugRoute
/**
* Navigate to the setup unlock screen.
*/
fun NavController.navigateToDebugMenuScreen() {
this.navigate(DEBUG_MENU) {
this.navigate(route = DebugRoute) {
launchSingleTop = true
}
}
@@ -21,9 +26,7 @@ fun NavController.navigateToDebugMenuScreen() {
fun NavGraphBuilder.setupDebugMenuDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions(
route = DEBUG_MENU,
) {
composableWithPushTransitions<DebugRoute> {
DebugMenuScreen(onNavigateBack = onNavigateBack)
}
}

View File

@@ -13,22 +13,23 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import com.bitwarden.authenticator.ui.auth.unlock.UNLOCK_ROUTE
import com.bitwarden.authenticator.ui.auth.unlock.UnlockRoute
import com.bitwarden.authenticator.ui.auth.unlock.navigateToUnlock
import com.bitwarden.authenticator.ui.auth.unlock.unlockDestination
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.AUTHENTICATOR_GRAPH_ROUTE
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.AuthenticatorGraphRoute
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.authenticatorGraph
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.navigateToAuthenticatorGraph
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.setupDebugMenuDestination
import com.bitwarden.authenticator.ui.platform.feature.splash.SPLASH_ROUTE
import com.bitwarden.authenticator.ui.platform.feature.splash.SplashRoute
import com.bitwarden.authenticator.ui.platform.feature.splash.navigateToSplash
import com.bitwarden.authenticator.ui.platform.feature.splash.splashDestination
import com.bitwarden.authenticator.ui.platform.feature.tutorial.TUTORIAL_ROUTE
import com.bitwarden.authenticator.ui.platform.feature.tutorial.TutorialRoute
import com.bitwarden.authenticator.ui.platform.feature.tutorial.navigateToTutorial
import com.bitwarden.authenticator.ui.platform.feature.tutorial.tutorialDestination
import com.bitwarden.authenticator.ui.platform.theme.NonNullEnterTransitionProvider
import com.bitwarden.authenticator.ui.platform.theme.NonNullExitTransitionProvider
import com.bitwarden.authenticator.ui.platform.theme.RootTransitionProviders
import com.bitwarden.ui.platform.theme.NonNullEnterTransitionProvider
import com.bitwarden.ui.platform.theme.NonNullExitTransitionProvider
import com.bitwarden.ui.platform.theme.RootTransitionProviders
import com.bitwarden.ui.platform.util.toObjectNavigationRoute
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.util.concurrent.atomic.AtomicReference
@@ -64,7 +65,7 @@ fun RootNavScreen(
NavHost(
navController = navController,
startDestination = SPLASH_ROUTE,
startDestination = SplashRoute,
enterTransition = { toEnterTransition()(this) },
exitTransition = { toExitTransition()(this) },
popEnterTransition = { toEnterTransition()(this) },
@@ -93,10 +94,10 @@ fun RootNavScreen(
}
val targetRoute = when (state.navState) {
RootNavState.NavState.Splash -> SPLASH_ROUTE
RootNavState.NavState.Locked -> UNLOCK_ROUTE
RootNavState.NavState.Tutorial -> TUTORIAL_ROUTE
RootNavState.NavState.Unlocked -> AUTHENTICATOR_GRAPH_ROUTE
RootNavState.NavState.Splash -> SplashRoute
RootNavState.NavState.Locked -> UnlockRoute
RootNavState.NavState.Tutorial -> TutorialRoute
RootNavState.NavState.Unlocked -> AuthenticatorGraphRoute
}
val currentRoute = navController.currentDestination?.rootLevelRoute()
@@ -104,7 +105,9 @@ fun RootNavScreen(
// death. In this case, the NavHost already restores state, so we don't have to navigate.
// However, if the route is correct but the underlying state is different, we should still
// proceed in order to get a fresh version of that route.
if (currentRoute == targetRoute && previousStateReference.get() == state) {
if (currentRoute == targetRoute.toObjectNavigationRoute() &&
previousStateReference.get() == state
) {
previousStateReference.set(state)
return
}
@@ -167,7 +170,7 @@ private fun AnimatedContentTransitionScope<NavBackStackEntry>.toEnterTransition(
when (targetState.destination.rootLevelRoute()) {
else -> when (initialState.destination.rootLevelRoute()) {
// Disable transitions when coming from the splash screen
SPLASH_ROUTE -> RootTransitionProviders.Enter.none
SplashRoute.toObjectNavigationRoute() -> RootTransitionProviders.Enter.none
else -> RootTransitionProviders.Enter.fadeIn
}
}
@@ -179,7 +182,7 @@ private fun AnimatedContentTransitionScope<NavBackStackEntry>.toEnterTransition(
private fun AnimatedContentTransitionScope<NavBackStackEntry>.toExitTransition(): NonNullExitTransitionProvider =
when (initialState.destination.rootLevelRoute()) {
// Disable transitions when coming from the splash screen
SPLASH_ROUTE -> RootTransitionProviders.Exit.none
SplashRoute.toObjectNavigationRoute() -> RootTransitionProviders.Exit.none
else -> when (targetState.destination.rootLevelRoute()) {
else -> RootTransitionProviders.Exit.fadeOut
}

View File

@@ -4,13 +4,23 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.bitwarden.authenticator.ui.platform.base.util.composableWithRootPushTransitions
import com.bitwarden.authenticator.ui.platform.feature.settings.export.exportDestination
import com.bitwarden.authenticator.ui.platform.feature.settings.importing.importingDestination
import com.bitwarden.authenticator.ui.platform.feature.tutorial.tutorialSettingsDestination
import com.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import kotlinx.serialization.Serializable
const val SETTINGS_GRAPH_ROUTE = "settings_graph"
private const val SETTINGS_ROUTE = "settings"
/**
* The type-safe route for the settings graph.
*/
@Serializable
data object SettingsGraphRoute
/**
* The type-safe route for the settings screen.
*/
@Serializable
data object SettingsRoute
/**
* Add settings graph to the nav graph.
@@ -21,13 +31,10 @@ fun NavGraphBuilder.settingsGraph(
onNavigateToImport: () -> Unit,
onNavigateToTutorial: () -> Unit,
) {
navigation(
startDestination = SETTINGS_ROUTE,
route = SETTINGS_GRAPH_ROUTE,
navigation<SettingsGraphRoute>(
startDestination = SettingsRoute,
) {
composableWithRootPushTransitions(
route = SETTINGS_ROUTE,
) {
composableWithRootPushTransitions<SettingsRoute> {
SettingsScreen(
onNavigateToTutorial = onNavigateToTutorial,
onNavigateToExport = onNavigateToExport,
@@ -50,5 +57,5 @@ fun NavGraphBuilder.settingsGraph(
* Navigate to the settings screen.
*/
fun NavController.navigateToSettingsGraph(navOptions: NavOptions? = null) {
navigate(SETTINGS_GRAPH_ROUTE, navOptions)
navigate(route = SettingsGraphRoute, navOptions = navOptions)
}

View File

@@ -3,12 +3,14 @@ package com.bitwarden.authenticator.ui.platform.feature.settings.export
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.authenticator.ui.platform.base.util.composableWithSlideTransitions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable
/**
* Route for the export data screen.
* The type-safe route for the export screen.
*/
const val EXPORT_ROUTE = "export"
@Serializable
data object ExportRoute
/**
* Add the export data destination to the nav graph.
@@ -16,7 +18,7 @@ const val EXPORT_ROUTE = "export"
fun NavGraphBuilder.exportDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(EXPORT_ROUTE) {
composableWithSlideTransitions<ExportRoute> {
ExportScreen(
onNavigateBack = onNavigateBack,
)
@@ -27,5 +29,5 @@ fun NavGraphBuilder.exportDestination(
* Navigate to the export data screen.
*/
fun NavController.navigateToExport(navOptions: NavOptions? = null) {
navigate(EXPORT_ROUTE, navOptions)
navigate(route = ExportRoute, navOptions = navOptions)
}

View File

@@ -3,9 +3,14 @@ package com.bitwarden.authenticator.ui.platform.feature.settings.importing
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.authenticator.ui.platform.base.util.composableWithSlideTransitions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable
const val IMPORT_ROUTE = "importing"
/**
* The type-safe route for the import screen.
*/
@Serializable
data object ImportRoute
/**
* Add the import screen to the nav graph.
@@ -13,7 +18,7 @@ const val IMPORT_ROUTE = "importing"
fun NavGraphBuilder.importingDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(IMPORT_ROUTE) {
composableWithSlideTransitions<ImportRoute> {
ImportingScreen(
onNavigateBack = onNavigateBack,
)
@@ -24,5 +29,5 @@ fun NavGraphBuilder.importingDestination(
* Navigate to the Import destination.
*/
fun NavController.navigateToImporting(navOptions: NavOptions? = null) {
navigate(IMPORT_ROUTE, navOptions)
navigate(route = ImportRoute, navOptions = navOptions)
}

View File

@@ -4,14 +4,19 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import kotlinx.serialization.Serializable
const val SPLASH_ROUTE: String = "splash"
/**
* The type-safe route for the splash screen.
*/
@Serializable
data object SplashRoute
/**
* Add splash destinations to the nav graph.
*/
fun NavGraphBuilder.splashDestination() {
composable(SPLASH_ROUTE) { SplashScreen() }
composable<SplashRoute> { SplashScreen() }
}
/**
@@ -20,5 +25,5 @@ fun NavGraphBuilder.splashDestination() {
fun NavController.navigateToSplash(
navOptions: NavOptions? = null,
) {
navigate(SPLASH_ROUTE, navOptions)
navigate(route = SplashRoute, navOptions = navOptions)
}

View File

@@ -4,15 +4,25 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import kotlinx.serialization.Serializable
const val TUTORIAL_ROUTE = "tutorial"
const val SETTINGS_TUTORIAL_ROUTE = "settings/tutorial"
/**
* The type-safe route for the tutorial screen.
*/
@Serializable
data object TutorialRoute
/**
* The type-safe route for the settings tutorial screen.
*/
@Serializable
data object SettingsTutorialRoute
/**
* Add the top level Tutorial screen to the nav graph.
*/
fun NavGraphBuilder.tutorialDestination(onTutorialFinished: () -> Unit) {
composable(TUTORIAL_ROUTE) {
composable<TutorialRoute> {
TutorialScreen(
onTutorialFinished = onTutorialFinished,
)
@@ -23,7 +33,7 @@ fun NavGraphBuilder.tutorialDestination(onTutorialFinished: () -> Unit) {
* Add the Settings Tutorial screen to the nav graph.
*/
fun NavGraphBuilder.tutorialSettingsDestination(onTutorialFinished: () -> Unit) {
composable(SETTINGS_TUTORIAL_ROUTE) {
composable<SettingsTutorialRoute> {
TutorialScreen(
onTutorialFinished = onTutorialFinished,
)
@@ -34,12 +44,12 @@ fun NavGraphBuilder.tutorialSettingsDestination(onTutorialFinished: () -> Unit)
* Navigate to the top level Tutorial screen.
*/
fun NavController.navigateToTutorial(navOptions: NavOptions? = null) {
navigate(route = TUTORIAL_ROUTE, navOptions = navOptions)
navigate(route = TutorialRoute, navOptions = navOptions)
}
/**
* Navigate to the Tutorial screen within Settings.
*/
fun NavController.navigateToSettingsTutorial(navOptions: NavOptions? = null) {
navigate(route = SETTINGS_TUTORIAL_ROUTE, navOptions)
navigate(route = SettingsTutorialRoute, navOptions = navOptions)
}

View File

@@ -1,371 +0,0 @@
package com.bitwarden.authenticator.ui.platform.theme
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.NavHost
import com.bitwarden.authenticator.ui.platform.theme.RootTransitionProviders.Exit.stay
typealias EnterTransitionProvider =
(@JvmSuppressWildcards AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)
typealias ExitTransitionProvider =
(@JvmSuppressWildcards AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)
typealias NonNullEnterTransitionProvider =
(@JvmSuppressWildcards AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)
typealias NonNullExitTransitionProvider =
(@JvmSuppressWildcards AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)
/**
* The default transition time (in milliseconds) for all fade transitions in the
* [TransitionProviders].
*/
const val DEFAULT_FADE_TRANSITION_TIME_MS: Int = 300
/**
* The default transition time (in milliseconds) for all slide transitions in the
* [TransitionProviders].
*/
const val DEFAULT_SLIDE_TRANSITION_TIME_MS: Int = 450
/**
* The default transition time (in milliseconds) for all slide transitions in the
* [TransitionProviders].
*/
const val DEFAULT_PUSH_TRANSITION_TIME_MS: Int = 350
/**
* The default transition time (in milliseconds) for all "stay"/no-op transitions in the
* [TransitionProviders].
*
* This should be at least as large as any other transition that might also be happening during a
* navigation.
*/
val DEFAULT_STAY_TRANSITION_TIME_MS: Int =
maxOf(
DEFAULT_FADE_TRANSITION_TIME_MS,
DEFAULT_SLIDE_TRANSITION_TIME_MS,
DEFAULT_PUSH_TRANSITION_TIME_MS,
)
/**
* Checks if the parent of the destination before and after the navigation is the same. This is
* useful to ignore certain enter/exit transitions when navigating between distinct, nested flows.
*/
val AnimatedContentTransitionScope<NavBackStackEntry>.isSameGraphNavigation: Boolean
get() = initialState.destination.parent == targetState.destination.parent
/**
* Contains standard "transition providers" that may be used to specify the [EnterTransition] and
* [ExitTransition] used when building a typical composable destination. These may return `null`
* values in order to allow transitions between nested navigation graphs to be specified by
* components higher up in the graph.
*/
object TransitionProviders {
/**
* The standard set of "enter" transition providers.
*/
object Enter {
/**
* Fades the new screen in.
*
* Note that this represents a `null` transition when navigating between different nested
* navigation graphs.
*/
val fadeIn: EnterTransitionProvider = {
RootTransitionProviders
.Enter
.fadeIn(this)
.takeIf { isSameGraphNavigation }
}
/**
* Slides the new screen in from the left of the screen.
*/
val pushLeft: EnterTransitionProvider = {
RootTransitionProviders
.Enter
.pushLeft(this)
.takeIf { isSameGraphNavigation }
}
/**
* Slides the new screen in from the right of the screen.
*/
val pushRight: EnterTransitionProvider = {
RootTransitionProviders
.Enter
.pushRight(this)
.takeIf { isSameGraphNavigation }
}
/**
* Slides the new screen in from the bottom of the screen.
*
* Note that this represents a `null` transition when navigating between different nested
* navigation graphs.
*/
val slideUp: EnterTransitionProvider = {
RootTransitionProviders
.Enter
.slideUp(this)
.takeIf { isSameGraphNavigation }
}
/**
* A "no-op" transition: this changes nothing about the screen but "lasts" as long as
* other standard transitions in order to leave the screen in place such that it does not
* immediately appear while the other screen transitions away.
*
* Note that this represents a `null` transition when navigating between different nested
* navigation graphs.
*/
val stay: EnterTransitionProvider = {
RootTransitionProviders
.Enter
.stay(this)
.takeIf { isSameGraphNavigation }
}
}
/**
* The standard set of "exit" transition providers.
*/
object Exit {
/**
* Fades the current screen out.
*
* Note that this represents a `null` transition when navigating between different nested
* navigation graphs.
*/
val fadeOut: ExitTransitionProvider = {
RootTransitionProviders
.Exit
.fadeOut(this)
.takeIf { isSameGraphNavigation }
}
/**
* Slides the current screen out to the left of the screen.
*/
val pushLeft: ExitTransitionProvider = {
RootTransitionProviders
.Exit
.pushLeft(this)
.takeIf { isSameGraphNavigation }
}
/**
* Slides the current screen out to the right of the screen.
*/
val pushRight: ExitTransitionProvider = {
RootTransitionProviders
.Exit
.pushRight(this)
.takeIf { isSameGraphNavigation }
}
/**
* Slides the current screen down to the bottom of the screen.
*
* Note that this represents a `null` transition when navigating between different nested
* navigation graphs.
*/
val slideDown: ExitTransitionProvider = {
RootTransitionProviders
.Exit
.slideDown(this)
.takeIf { isSameGraphNavigation }
}
/**
* A "no-op" transition: this changes nothing about the screen but "lasts" as long as
* other standard transitions in order to leave the screen in place such that it does not
* immediately disappear while the other screen transitions into place.
*
* Note that this represents a `null` transition when navigating between different nested
* navigation graphs.
*/
val stay: ExitTransitionProvider = {
RootTransitionProviders
.Exit
.stay(this)
.takeIf { isSameGraphNavigation }
}
}
}
/**
* Contains standard "transition providers" that may be used to specify the [EnterTransition] and
* [ExitTransition] used when building a root [NavHost], which requires a non-null value.
*/
object RootTransitionProviders {
/**
* The standard set of "enter" transition providers.
*/
object Enter {
/**
* Fades the new screen in.
*/
val fadeIn: NonNullEnterTransitionProvider = {
fadeIn(tween(DEFAULT_FADE_TRANSITION_TIME_MS))
}
/**
* There is no transition for the entering screen.
*/
val none: NonNullEnterTransitionProvider = {
EnterTransition.None
}
/**
* Slides the new screen in from the left of the screen.
*/
val pushLeft: NonNullEnterTransitionProvider = {
val totalTransitionDurationMs = DEFAULT_PUSH_TRANSITION_TIME_MS
slideInHorizontally(
animationSpec = tween(durationMillis = totalTransitionDurationMs),
initialOffsetX = { fullWidth -> fullWidth / 2 },
) + fadeIn(
animationSpec = tween(
durationMillis = totalTransitionDurationMs / 2,
delayMillis = totalTransitionDurationMs / 2,
),
)
}
/**
* Slides the new screen in from the right of the screen.
*/
val pushRight: NonNullEnterTransitionProvider = {
val totalTransitionDurationMs = DEFAULT_PUSH_TRANSITION_TIME_MS
slideInHorizontally(
animationSpec = tween(durationMillis = totalTransitionDurationMs),
initialOffsetX = { fullWidth -> -fullWidth / 2 },
) + fadeIn(
animationSpec = tween(
durationMillis = totalTransitionDurationMs / 2,
delayMillis = totalTransitionDurationMs / 2,
),
)
}
/**
* Slides the new screen in from the bottom of the screen.
*/
val slideUp: NonNullEnterTransitionProvider = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Up,
animationSpec = tween(DEFAULT_SLIDE_TRANSITION_TIME_MS),
)
}
/**
* A "no-op" transition: this changes nothing about the screen but "lasts" as long as
* other standard transitions in order to leave the screen in place such that it does not
* immediately appear while the other screen transitions away.
*/
val stay: NonNullEnterTransitionProvider = {
fadeIn(
animationSpec = tween(DEFAULT_STAY_TRANSITION_TIME_MS),
initialAlpha = 1f,
)
}
}
/**
* The standard set of "exit" transition providers.
*/
object Exit {
/**
* Fades the current screen out.
*/
val fadeOut: NonNullExitTransitionProvider = {
fadeOut(tween(DEFAULT_FADE_TRANSITION_TIME_MS))
}
/**
* There is no transition for the exiting screen.
*
* Unlike the [stay] transition, this will immediately remove the outgoing screen even if
* there is an ongoing enter transition happening for the new screen.
*/
val none: NonNullExitTransitionProvider = {
ExitTransition.None
}
/**
* Slides the current screen out to the left of the screen.
*/
@Suppress("MagicNumber")
val pushLeft: NonNullExitTransitionProvider = {
val totalTransitionDurationMs = DEFAULT_PUSH_TRANSITION_TIME_MS
val delayMs = totalTransitionDurationMs / 7
val slideWithoutDelayMs = totalTransitionDurationMs - delayMs
slideOutHorizontally(
animationSpec = tween(
durationMillis = slideWithoutDelayMs,
delayMillis = delayMs,
),
targetOffsetX = { fullWidth -> -fullWidth / 2 },
) + fadeOut(
animationSpec = tween(
durationMillis = totalTransitionDurationMs / 2,
delayMillis = delayMs,
),
)
}
/**
* Slides the current screen out to the right of the screen.
*/
@Suppress("MagicNumber")
val pushRight: NonNullExitTransitionProvider = {
val totalTransitionDurationMs = DEFAULT_PUSH_TRANSITION_TIME_MS
val delayMs = totalTransitionDurationMs / 7
val slideWithoutDelayMs = totalTransitionDurationMs - delayMs
slideOutHorizontally(
animationSpec = tween(
durationMillis = slideWithoutDelayMs,
delayMillis = delayMs,
),
targetOffsetX = { fullWidth -> fullWidth / 2 },
) + fadeOut(
animationSpec = tween(
durationMillis = totalTransitionDurationMs / 2,
delayMillis = delayMs,
),
)
}
/**
* Slides the current screen down to the bottom of the screen.
*/
val slideDown: NonNullExitTransitionProvider = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Down,
animationSpec = tween(DEFAULT_SLIDE_TRANSITION_TIME_MS),
)
}
/**
* A "no-op" transition: this changes nothing about the screen but "lasts" as long as
* other standard transitions in order to leave the screen in place such that it does not
* immediately disappear while the other screen transitions into place.
*/
val stay: NonNullExitTransitionProvider = {
fadeOut(
animationSpec = tween(DEFAULT_STAY_TRANSITION_TIME_MS),
targetAlpha = 0.99f,
)
}
}
}