diff --git a/Reply/app/build.gradle.kts b/Reply/app/build.gradle.kts index 87ba9cf01..8be533e2d 100644 --- a/Reply/app/build.gradle.kts +++ b/Reply/app/build.gradle.kts @@ -122,7 +122,9 @@ dependencies { implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.lifecycle.viewModelCompose) implementation(libs.androidx.lifecycle.runtime.compose) - implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.androidx.activity.compose) implementation(libs.androidx.window) diff --git a/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt b/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt index 6bb0d593a..d713ef980 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt @@ -21,18 +21,16 @@ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay import androidx.window.layout.DisplayFeature import androidx.window.layout.FoldingFeature -import com.example.reply.ui.navigation.ReplyNavigationActions import com.example.reply.ui.navigation.ReplyNavigationWrapper +import com.example.reply.ui.navigation.ReplyTopLevelDestination import com.example.reply.ui.navigation.Route import com.example.reply.ui.utils.DevicePosture import com.example.reply.ui.utils.ReplyContentType @@ -84,68 +82,55 @@ fun ReplyApp( else -> ReplyContentType.SINGLE_PANE } - val navController = rememberNavController() - val navigationActions = remember(navController) { - ReplyNavigationActions(navController) + val backStack = remember { mutableStateListOf(Route.Inbox) } + val currentRoute: Route? = backStack.lastOrNull() + + val navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit = { destination -> + if (backStack.lastOrNull() != destination.route) { + if (backStack.size > 1) backStack.subList(1, backStack.size).clear() + if (destination.route != Route.Inbox) { + backStack.add(destination.route) + } + } } - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination Surface { ReplyNavigationWrapper( - currentDestination = currentDestination, - navigateToTopLevelDestination = navigationActions::navigateTo, + currentRoute = currentRoute, + navigateToTopLevelDestination = navigateToTopLevelDestination, ) { - ReplyNavHost( - navController = navController, - contentType = contentType, - displayFeatures = displayFeatures, - replyHomeUIState = replyHomeUIState, - navigationType = navSuiteType.toReplyNavType(), - closeDetailScreen = closeDetailScreen, - navigateToDetail = navigateToDetail, - toggleSelectedEmail = toggleSelectedEmail, - ) - } - } -} - -@Composable -private fun ReplyNavHost( - navController: NavHostController, - contentType: ReplyContentType, - displayFeatures: List, - replyHomeUIState: ReplyHomeUIState, - navigationType: ReplyNavigationType, - closeDetailScreen: () -> Unit, - navigateToDetail: (Long, ReplyContentType) -> Unit, - toggleSelectedEmail: (Long) -> Unit, - modifier: Modifier = Modifier, -) { - NavHost( - modifier = modifier, - navController = navController, - startDestination = Route.Inbox, - ) { - composable { - ReplyInboxScreen( - contentType = contentType, - replyHomeUIState = replyHomeUIState, - navigationType = navigationType, - displayFeatures = displayFeatures, - closeDetailScreen = closeDetailScreen, - navigateToDetail = navigateToDetail, - toggleSelectedEmail = toggleSelectedEmail, + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), + entryProvider = { route -> + when (route) { + Route.Inbox -> NavEntry(route) { + ReplyInboxScreen( + contentType = contentType, + replyHomeUIState = replyHomeUIState, + navigationType = navSuiteType.toReplyNavType(), + displayFeatures = displayFeatures, + closeDetailScreen = closeDetailScreen, + navigateToDetail = navigateToDetail, + toggleSelectedEmail = toggleSelectedEmail, + ) + } + Route.DirectMessages -> NavEntry(route) { + EmptyComingSoon() + } + Route.Articles -> NavEntry(route) { + EmptyComingSoon() + } + Route.Groups -> NavEntry(route) { + EmptyComingSoon() + } + } + }, ) } - composable { - EmptyComingSoon() - } - composable { - EmptyComingSoon() - } - composable { - EmptyComingSoon() - } } } diff --git a/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationActions.kt b/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationActions.kt index ad0889c14..6337fdfa4 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationActions.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationActions.kt @@ -16,12 +16,11 @@ package com.example.reply.ui.navigation -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController +import androidx.navigation3.runtime.NavKey import com.example.reply.R import kotlinx.serialization.Serializable -sealed interface Route { +sealed interface Route : NavKey { @Serializable data object Inbox : Route @Serializable data object Articles : Route @Serializable data object DirectMessages : Route @@ -30,25 +29,6 @@ sealed interface Route { data class ReplyTopLevelDestination(val route: Route, val selectedIcon: Int, val unselectedIcon: Int, val iconTextId: Int) -class ReplyNavigationActions(private val navController: NavHostController) { - - fun navigateTo(destination: ReplyTopLevelDestination) { - navController.navigate(destination.route) { - // Pop up to the start destination of the graph to - // avoid building up a large stack of destinations - // on the back stack as users select items - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - // Avoid multiple copies of the same destination when - // reselecting the same item - launchSingleTop = true - // Restore state when reselecting a previously selected item - restoreState = true - } - } -} - val TOP_LEVEL_DESTINATIONS = listOf( ReplyTopLevelDestination( route = Route.Inbox, diff --git a/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationComponents.kt b/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationComponents.kt index 1826fd29e..61c86f497 100644 --- a/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationComponents.kt +++ b/Reply/app/src/main/java/com/example/reply/ui/navigation/ReplyNavigationComponents.kt @@ -67,8 +67,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import androidx.compose.ui.unit.toSize -import androidx.navigation.NavDestination -import androidx.navigation.NavDestination.Companion.hasRoute import androidx.window.core.layout.WindowHeightSizeClass import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowWidthSizeClass @@ -83,7 +81,7 @@ class ReplyNavSuiteScope(val navSuiteType: NavigationSuiteType) @Composable fun ReplyNavigationWrapper( - currentDestination: NavDestination?, + currentRoute: Route?, navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit, content: @Composable ReplyNavSuiteScope.() -> Unit, ) { @@ -125,7 +123,7 @@ fun ReplyNavigationWrapper( gesturesEnabled = gesturesEnabled, drawerContent = { ModalNavigationDrawerContent( - currentDestination = currentDestination, + currentRoute = currentRoute, navigationContentPosition = navContentPosition, navigateToTopLevelDestination = navigateToTopLevelDestination, onDrawerClicked = { @@ -141,11 +139,11 @@ fun ReplyNavigationWrapper( navigationSuite = { when (navLayoutType) { NavigationSuiteType.NavigationBar -> ReplyBottomNavigationBar( - currentDestination = currentDestination, + currentRoute = currentRoute, navigateToTopLevelDestination = navigateToTopLevelDestination, ) NavigationSuiteType.NavigationRail -> ReplyNavigationRail( - currentDestination = currentDestination, + currentRoute = currentRoute, navigationContentPosition = navContentPosition, navigateToTopLevelDestination = navigateToTopLevelDestination, onDrawerClicked = { @@ -155,7 +153,7 @@ fun ReplyNavigationWrapper( }, ) NavigationSuiteType.NavigationDrawer -> PermanentNavigationDrawerContent( - currentDestination = currentDestination, + currentRoute = currentRoute, navigationContentPosition = navContentPosition, navigateToTopLevelDestination = navigateToTopLevelDestination, ) @@ -169,7 +167,7 @@ fun ReplyNavigationWrapper( @Composable fun ReplyNavigationRail( - currentDestination: NavDestination?, + currentRoute: Route?, navigationContentPosition: ReplyNavigationContentPosition, navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit, onDrawerClicked: () -> Unit = {}, @@ -216,7 +214,7 @@ fun ReplyNavigationRail( ) { TOP_LEVEL_DESTINATIONS.forEach { replyDestination -> NavigationRailItem( - selected = currentDestination.hasRoute(replyDestination), + selected = currentRoute == replyDestination.route, onClick = { navigateToTopLevelDestination(replyDestination) }, icon = { Icon( @@ -233,11 +231,11 @@ fun ReplyNavigationRail( } @Composable -fun ReplyBottomNavigationBar(currentDestination: NavDestination?, navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit) { +fun ReplyBottomNavigationBar(currentRoute: Route?, navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit) { NavigationBar(modifier = Modifier.fillMaxWidth()) { TOP_LEVEL_DESTINATIONS.forEach { replyDestination -> NavigationBarItem( - selected = currentDestination.hasRoute(replyDestination), + selected = currentRoute == replyDestination.route, onClick = { navigateToTopLevelDestination(replyDestination) }, icon = { Icon( @@ -252,7 +250,7 @@ fun ReplyBottomNavigationBar(currentDestination: NavDestination?, navigateToTopL @Composable fun PermanentNavigationDrawerContent( - currentDestination: NavDestination?, + currentRoute: Route?, navigationContentPosition: ReplyNavigationContentPosition, navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit, ) { @@ -307,7 +305,7 @@ fun PermanentNavigationDrawerContent( ) { TOP_LEVEL_DESTINATIONS.forEach { replyDestination -> NavigationDrawerItem( - selected = currentDestination.hasRoute(replyDestination), + selected = currentRoute == replyDestination.route, label = { Text( text = stringResource(id = replyDestination.iconTextId), @@ -337,7 +335,7 @@ fun PermanentNavigationDrawerContent( @Composable fun ModalNavigationDrawerContent( - currentDestination: NavDestination?, + currentRoute: Route?, navigationContentPosition: ReplyNavigationContentPosition, navigateToTopLevelDestination: (ReplyTopLevelDestination) -> Unit, onDrawerClicked: () -> Unit = {}, @@ -403,7 +401,7 @@ fun ModalNavigationDrawerContent( ) { TOP_LEVEL_DESTINATIONS.forEach { replyDestination -> NavigationDrawerItem( - selected = currentDestination.hasRoute(replyDestination), + selected = currentRoute == replyDestination.route, label = { Text( text = stringResource(id = replyDestination.iconTextId), @@ -472,5 +470,3 @@ enum class LayoutType { HEADER, CONTENT, } - -fun NavDestination?.hasRoute(destination: ReplyTopLevelDestination): Boolean = this?.hasRoute(destination.route::class) ?: false diff --git a/Reply/gradle/libs.versions.toml b/Reply/gradle/libs.versions.toml index cf10a7f86..9f4212d87 100644 --- a/Reply/gradle/libs.versions.toml +++ b/Reply/gradle/libs.versions.toml @@ -17,6 +17,8 @@ androidx-lifecycle = "2.8.2" androidx-lifecycle-compose = "2.10.0" androidx-lifecycle-runtime-compose = "2.10.0" androidx-navigation = "2.9.6" +androidx-navigation3 = "1.0.0" +androidx-lifecycle-viewmodel-navigation3 = "2.10.0" androidx-palette = "1.0.0" androidx-test = "1.7.0" androidx-test-espresso = "3.7.0" @@ -106,6 +108,9 @@ androidx-material-icons-core = { module = "androidx.compose.material:material-ic androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" } androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle-viewmodel-navigation3" } androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }