diff --git a/JetNews/app/build.gradle.kts b/JetNews/app/build.gradle.kts index a962e86ca..a24c83556 100644 --- a/JetNews/app/build.gradle.kts +++ b/JetNews/app/build.gradle.kts @@ -21,6 +21,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.compose) + alias(libs.plugins.kotlin.serialization) } android { @@ -117,7 +118,10 @@ dependencies { implementation(libs.androidx.lifecycle.livedata.ktx) 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.kotlinx.serialization.json) implementation(libs.androidx.window) androidTestImplementation(libs.junit) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt index 9acf45b8f..51516d7a6 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt @@ -42,7 +42,7 @@ import com.example.jetnews.ui.theme.JetnewsTheme @Composable fun AppDrawer( drawerState: DrawerState, - currentRoute: String, + currentRoute: JetnewsRoute?, navigateToHome: () -> Unit, navigateToInterests: () -> Unit, closeDrawer: () -> Unit, @@ -58,7 +58,7 @@ fun AppDrawer( NavigationDrawerItem( label = { Text(stringResource(id = R.string.home_title)) }, icon = { Icon(painterResource(R.drawable.ic_home), null) }, - selected = currentRoute == JetnewsDestinations.HOME_ROUTE, + selected = currentRoute is Home, onClick = { navigateToHome() closeDrawer() @@ -68,7 +68,7 @@ fun AppDrawer( NavigationDrawerItem( label = { Text(stringResource(id = R.string.interests_title)) }, icon = { Icon(painterResource(R.drawable.ic_list_alt), null) }, - selected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, + selected = currentRoute is Interests, onClick = { navigateToInterests() closeDrawer() @@ -102,7 +102,7 @@ fun PreviewAppDrawer() { JetnewsTheme { AppDrawer( drawerState = rememberDrawerState(initialValue = DrawerValue.Open), - currentRoute = JetnewsDestinations.HOME_ROUTE, + currentRoute = Home(), navigateToHome = {}, navigateToInterests = {}, closeDrawer = { }, diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt index e0acc812e..9d8e592fe 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt @@ -23,30 +23,37 @@ import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.rememberDrawerState 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.runtime.rememberCoroutineScope -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController import com.example.jetnews.data.AppContainer import com.example.jetnews.ui.components.AppNavRail import com.example.jetnews.ui.theme.JetnewsTheme import kotlinx.coroutines.launch @Composable -fun JetnewsApp(appContainer: AppContainer, widthSizeClass: WindowWidthSizeClass) { +fun JetnewsApp( + appContainer: AppContainer, + widthSizeClass: WindowWidthSizeClass, + startRoute: Home = Home(), +) { JetnewsTheme { - val navController = rememberNavController() - val navigationActions = remember(navController) { - JetnewsNavigationActions(navController) + val backStack = remember { mutableStateListOf(startRoute) } + val currentRoute: JetnewsRoute? = backStack.lastOrNull() + + val navigateToHome: () -> Unit = { + // Pop everything back to Home (the start destination) + while (backStack.size > 1) backStack.removeLast() + } + val navigateToInterests: () -> Unit = { + if (backStack.lastOrNull() != Interests) { + while (backStack.size > 1) backStack.removeLast() + backStack.add(Interests) + } } val coroutineScope = rememberCoroutineScope() - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentRoute = - navBackStackEntry?.destination?.route ?: JetnewsDestinations.HOME_ROUTE - val isExpandedScreen = widthSizeClass == WindowWidthSizeClass.Expanded val sizeAwareDrawerState = rememberSizeAwareDrawerState(isExpandedScreen) @@ -55,8 +62,8 @@ fun JetnewsApp(appContainer: AppContainer, widthSizeClass: WindowWidthSizeClass) AppDrawer( drawerState = sizeAwareDrawerState, currentRoute = currentRoute, - navigateToHome = navigationActions.navigateToHome, - navigateToInterests = navigationActions.navigateToInterests, + navigateToHome = navigateToHome, + navigateToInterests = navigateToInterests, closeDrawer = { coroutineScope.launch { sizeAwareDrawerState.close() } }, ) }, @@ -68,14 +75,14 @@ fun JetnewsApp(appContainer: AppContainer, widthSizeClass: WindowWidthSizeClass) if (isExpandedScreen) { AppNavRail( currentRoute = currentRoute, - navigateToHome = navigationActions.navigateToHome, - navigateToInterests = navigationActions.navigateToInterests, + navigateToHome = navigateToHome, + navigateToInterests = navigateToInterests, ) } JetnewsNavGraph( appContainer = appContainer, isExpandedScreen = isExpandedScreen, - navController = navController, + backStack = backStack, openDrawer = { coroutineScope.launch { sizeAwareDrawerState.open() } }, ) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt index 8e8202e59..afc7e51b1 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt @@ -17,66 +17,61 @@ package com.example.jetnews.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navDeepLink -import com.example.jetnews.JetnewsApplication.Companion.JETNEWS_APP_URI +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay import com.example.jetnews.data.AppContainer import com.example.jetnews.ui.home.HomeRoute import com.example.jetnews.ui.home.HomeViewModel import com.example.jetnews.ui.interests.InterestsRoute import com.example.jetnews.ui.interests.InterestsViewModel -const val POST_ID = "postId" - @Composable fun JetnewsNavGraph( appContainer: AppContainer, isExpandedScreen: Boolean, + backStack: SnapshotStateList, modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController(), openDrawer: () -> Unit = {}, - startDestination: String = JetnewsDestinations.HOME_ROUTE, ) { - NavHost( - navController = navController, - startDestination = startDestination, + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, modifier = modifier, - ) { - composable( - route = JetnewsDestinations.HOME_ROUTE, - deepLinks = listOf( - navDeepLink { - uriPattern = - "$JETNEWS_APP_URI/${JetnewsDestinations.HOME_ROUTE}?$POST_ID={$POST_ID}" - }, - ), - ) { navBackStackEntry -> - val homeViewModel: HomeViewModel = viewModel( - factory = HomeViewModel.provideFactory( - postsRepository = appContainer.postsRepository, - preSelectedPostId = navBackStackEntry.arguments?.getString(POST_ID), - ), - ) - HomeRoute( - homeViewModel = homeViewModel, - isExpandedScreen = isExpandedScreen, - openDrawer = openDrawer, - ) - } - composable(JetnewsDestinations.INTERESTS_ROUTE) { - val interestsViewModel: InterestsViewModel = viewModel( - factory = InterestsViewModel.provideFactory(appContainer.interestsRepository), - ) - InterestsRoute( - interestsViewModel = interestsViewModel, - isExpandedScreen = isExpandedScreen, - openDrawer = openDrawer, - ) - } - } + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), + entryProvider = { route -> + when (route) { + is Home -> NavEntry(route) { + val homeViewModel: HomeViewModel = viewModel( + factory = HomeViewModel.provideFactory( + postsRepository = appContainer.postsRepository, + preSelectedPostId = route.preSelectedPostId, + ), + ) + HomeRoute( + homeViewModel = homeViewModel, + isExpandedScreen = isExpandedScreen, + openDrawer = openDrawer, + ) + } + is Interests -> NavEntry(route) { + val interestsViewModel: InterestsViewModel = viewModel( + factory = InterestsViewModel.provideFactory(appContainer.interestsRepository), + ) + InterestsRoute( + interestsViewModel = interestsViewModel, + isExpandedScreen = isExpandedScreen, + openDrawer = openDrawer, + ) + } + } + }, + ) } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavigation.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavigation.kt index 8dd1ee01d..e48040501 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavigation.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavigation.kt @@ -16,43 +16,17 @@ package com.example.jetnews.ui -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable /** - * Destinations used in the [JetnewsApp]. + * Route definitions used in [JetnewsApp]. */ -object JetnewsDestinations { - const val HOME_ROUTE = "home" - const val INTERESTS_ROUTE = "interests" -} +@Serializable +sealed interface JetnewsRoute : NavKey -/** - * Models the navigation actions in the app. - */ -class JetnewsNavigationActions(navController: NavHostController) { - val navigateToHome: () -> Unit = { - navController.navigate(JetnewsDestinations.HOME_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 navigateToInterests: () -> Unit = { - navController.navigate(JetnewsDestinations.INTERESTS_ROUTE) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } -} +@Serializable +data class Home(val preSelectedPostId: String? = null) : JetnewsRoute + +@Serializable +data object Interests : JetnewsRoute diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/MainActivity.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/MainActivity.kt index bd6d2ac0b..cc7bf3998 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/MainActivity.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/MainActivity.kt @@ -32,9 +32,13 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) val appContainer = (application as JetnewsApplication).container + + val postId = intent?.data?.getQueryParameter("postId") + val startRoute = Home(preSelectedPostId = postId) + setContent { val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass - JetnewsApp(appContainer, widthSizeClass) + JetnewsApp(appContainer, widthSizeClass, startRoute) } } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt index b6d48e74f..86c95ce5a 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/components/AppNavRail.kt @@ -31,11 +31,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.jetnews.R -import com.example.jetnews.ui.JetnewsDestinations +import com.example.jetnews.ui.Home +import com.example.jetnews.ui.Interests +import com.example.jetnews.ui.JetnewsRoute import com.example.jetnews.ui.theme.JetnewsTheme @Composable -fun AppNavRail(currentRoute: String, navigateToHome: () -> Unit, navigateToInterests: () -> Unit, modifier: Modifier = Modifier) { +fun AppNavRail(currentRoute: JetnewsRoute?, navigateToHome: () -> Unit, navigateToInterests: () -> Unit, modifier: Modifier = Modifier) { NavigationRail( header = { Icon( @@ -49,14 +51,14 @@ fun AppNavRail(currentRoute: String, navigateToHome: () -> Unit, navigateToInter ) { Spacer(Modifier.weight(1f)) NavigationRailItem( - selected = currentRoute == JetnewsDestinations.HOME_ROUTE, + selected = currentRoute is Home, onClick = navigateToHome, icon = { Icon(painterResource(id = R.drawable.ic_home), stringResource(R.string.home_title)) }, label = { Text(stringResource(R.string.home_title)) }, alwaysShowLabel = false, ) NavigationRailItem( - selected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, + selected = currentRoute is Interests, onClick = navigateToInterests, icon = { Icon(painterResource(id = R.drawable.ic_list_alt), stringResource(R.string.interests_title)) }, label = { Text(stringResource(R.string.interests_title)) }, @@ -72,7 +74,7 @@ fun AppNavRail(currentRoute: String, navigateToHome: () -> Unit, navigateToInter fun PreviewAppNavRail() { JetnewsTheme { AppNavRail( - currentRoute = JetnewsDestinations.HOME_ROUTE, + currentRoute = Home(), navigateToHome = {}, navigateToInterests = {}, ) diff --git a/JetNews/gradle/libs.versions.toml b/JetNews/gradle/libs.versions.toml index cf10a7f86..9f4212d87 100644 --- a/JetNews/gradle/libs.versions.toml +++ b/JetNews/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" }