Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion JetNews/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -102,7 +102,7 @@ fun PreviewAppDrawer() {
JetnewsTheme {
AppDrawer(
drawerState = rememberDrawerState(initialValue = DrawerValue.Open),
currentRoute = JetnewsDestinations.HOME_ROUTE,
currentRoute = Home(),
navigateToHome = {},
navigateToInterests = {},
closeDrawer = { },
Expand Down
38 changes: 22 additions & 16 deletions JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,36 @@ 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<JetnewsRoute>(startRoute) }
val currentRoute: JetnewsRoute = backStack.lastOrNull() ?: Home()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the backStack becomes empty (e.g., when the user presses back on the last screen), currentRoute defaults to Home(). This causes AppDrawer and AppNavRail to show "Home" as selected, while NavDisplay shows a blank screen because its backstack is empty. This creates a UI inconsistency.

To fix this, currentRoute should be nullable. When it's null, no navigation item will be selected, which correctly reflects the empty state.

You'll also need to update the currentRoute parameter in AppDrawer and AppNavRail to be nullable (JetnewsRoute?). The is checks for selection will continue to work correctly.

Suggested change
val currentRoute: JetnewsRoute = backStack.lastOrNull() ?: Home()
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 = {
// Pop to Home, then add Interests on top (single top behavior)
while (backStack.size > 1) backStack.removeLast()
backStack.add(Interests)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation for navigateToInterests always pops back to the home route before adding the Interests route. If the user is already on the Interests screen, this causes the screen to be unnecessarily removed and re-added, which can lead to inefficient recomposition and potential UI flicker. This behavior doesn't fully match the launchSingleTop flag from Navigation 2, which would prevent this.

To optimize this and align closer with launchSingleTop, you can add a check to do nothing if Interests is already the current route.

Suggested change
// Pop to Home, then add Interests on top (single top behavior)
while (backStack.size > 1) backStack.removeLast()
backStack.add(Interests)
if (backStack.lastOrNull() == Interests) return@lambda
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)

Expand All @@ -55,8 +61,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() } },
)
},
Expand All @@ -68,14 +74,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() } },
)
}
Expand Down
87 changes: 41 additions & 46 deletions JetNews/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<JetnewsRoute>,
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,
)
}
}
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)) },
Expand All @@ -72,7 +74,7 @@ fun AppNavRail(currentRoute: String, navigateToHome: () -> Unit, navigateToInter
fun PreviewAppNavRail() {
JetnewsTheme {
AppNavRail(
currentRoute = JetnewsDestinations.HOME_ROUTE,
currentRoute = Home(),
navigateToHome = {},
navigateToInterests = {},
)
Expand Down
5 changes: 5 additions & 0 deletions JetNews/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down