From 8f69490c1124b3c398fa4119be376e7e80a23b9e Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Wed, 3 Jul 2024 14:03:48 +0530 Subject: [PATCH] Reapply "refactor(android): migrate to navigation safe-args" This reverts commit 6296afe51a56d96d1b2d9888134ea93facf406bf. --- android/build.gradle.kts | 2 + .../ui/decorations/ClawNavigationBar.kt | 25 +++--- .../ui/decorations/ClawNavigationRail.kt | 22 ++--- .../dev/msfjarvis/claw/android/ui/ext.kt | 7 +- .../claw/android/ui/navigation/Destination.kt | 27 ++++++ .../android/ui/navigation/Destinations.kt | 50 ----------- .../ui/navigation/NavigationExtensions.kt | 30 +++++++ .../android/ui/screens/LobstersPostsScreen.kt | 90 ++++++++----------- .../claw/android/ui/screens/SearchScreen.kt | 43 +++------ gradle/libs.versions.toml | 4 +- settings.gradle.kts | 1 + 11 files changed, 140 insertions(+), 161 deletions(-) create mode 100644 android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destination.kt delete mode 100644 android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destinations.kt create mode 100644 android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/NavigationExtensions.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index cd51e2a7..a55adbbe 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -24,6 +24,8 @@ plugins { alias(libs.plugins.licensee) alias(libs.plugins.tracelog) alias(libs.plugins.kotlin.composeCompiler) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.navigation.safeargs) } android { diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/decorations/ClawNavigationBar.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/decorations/ClawNavigationBar.kt index f97ac5d0..66fff5fd 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/decorations/ClawNavigationBar.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/decorations/ClawNavigationBar.kt @@ -22,7 +22,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.navigation.NavController -import dev.msfjarvis.claw.android.ui.navigation.Destinations +import androidx.navigation.compose.currentBackStackEntryAsState +import dev.msfjarvis.claw.android.ui.navigation.Destination +import dev.msfjarvis.claw.android.ui.navigation.matches import kotlinx.collections.immutable.ImmutableList const val AnimationDuration = 100 @@ -51,11 +53,13 @@ fun ClawNavigationBar( modifier = Modifier, ) { NavigationBar(modifier = modifier) { + val navBackStackEntry = navController.currentBackStackEntryAsState().value + val currentDestination = navBackStackEntry?.destination items.forEach { navItem -> - val isCurrentDestination = navController.currentDestination?.route == navItem.route + val isSelected = currentDestination.matches(navItem.destination) NavigationBarItem( icon = { - Crossfade(isCurrentDestination, label = "nav-label") { + Crossfade(isSelected, label = "nav-label") { Icon( imageVector = if (it) navItem.selectedIcon else navItem.icon, contentDescription = navItem.label.replaceFirstChar(Char::uppercase), @@ -63,16 +67,15 @@ fun ClawNavigationBar( } }, label = { Text(text = navItem.label) }, - selected = isCurrentDestination, + selected = isSelected, onClick = { - if (isCurrentDestination) { + if (isSelected) { navItem.listStateResetCallback() } else { - navController.graph.startDestinationRoute?.let { startDestination -> - navController.popBackStack(startDestination, false) - } - if (navItem.route != Destinations.startDestination.route) { - navController.navigate(navItem.route) + navController.navigate(navItem.destination) { + popUpTo(navController.graph.startDestinationId) { saveState = true } + launchSingleTop = true + restoreState = true } } }, @@ -85,7 +88,7 @@ fun ClawNavigationBar( class NavigationItem( val label: String, - val route: String, + val destination: Destination, val icon: ImageVector, val selectedIcon: ImageVector, val listStateResetCallback: () -> Unit, diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/decorations/ClawNavigationRail.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/decorations/ClawNavigationRail.kt index 04f5f329..c7781bef 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/decorations/ClawNavigationRail.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/decorations/ClawNavigationRail.kt @@ -22,7 +22,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.navigation.NavController -import dev.msfjarvis.claw.android.ui.navigation.Destinations +import androidx.navigation.compose.currentBackStackEntryAsState +import dev.msfjarvis.claw.android.ui.navigation.matches import kotlinx.collections.immutable.ImmutableList @Composable @@ -49,12 +50,14 @@ fun ClawNavigationRail( modifier = Modifier, ) { NavigationRail(modifier = modifier) { + val navBackStackEntry = navController.currentBackStackEntryAsState().value + val currentDestination = navBackStackEntry?.destination Spacer(Modifier.weight(1f)) items.forEach { navItem -> - val isCurrentDestination = navController.currentDestination?.route == navItem.route + val isSelected = currentDestination.matches(navItem.destination) NavigationRailItem( icon = { - Crossfade(isCurrentDestination, label = "nav-label") { + Crossfade(isSelected, label = "nav-label") { Icon( imageVector = if (it) navItem.selectedIcon else navItem.icon, contentDescription = navItem.label.replaceFirstChar(Char::uppercase), @@ -62,16 +65,15 @@ fun ClawNavigationRail( } }, label = { Text(text = navItem.label) }, - selected = isCurrentDestination, + selected = isSelected, onClick = { - if (isCurrentDestination) { + if (isSelected) { navItem.listStateResetCallback() } else { - navController.graph.startDestinationRoute?.let { startDestination -> - navController.popBackStack(startDestination, false) - } - if (navItem.route != Destinations.startDestination.route) { - navController.navigate(navItem.route) + navController.navigate(navItem.destination) { + popUpTo(navController.graph.startDestinationId) { saveState = true } + launchSingleTop = true + restoreState = true } } }, diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ext.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ext.kt index 01208097..8101629a 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ext.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ext.kt @@ -14,7 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.navigation.NavController import com.slack.eithernet.ApiResult -import dev.msfjarvis.claw.android.ui.navigation.Destinations +import dev.msfjarvis.claw.android.ui.navigation.Comments import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.common.posts.PostActions import dev.msfjarvis.claw.common.urllauncher.UrlLauncher @@ -47,10 +47,7 @@ fun rememberPostActions( override fun viewComments(postId: String) { viewModel.markPostAsRead(postId) - val currentRoute = navController.currentDestination?.route - val newRoute = - Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postId) - if (currentRoute != Destinations.Comments.route) navController.navigate(newRoute) + navController.navigate(Comments(postId)) } override fun viewCommentsPage(post: UIPost) { diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destination.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destination.kt new file mode 100644 index 00000000..bc4000aa --- /dev/null +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destination.kt @@ -0,0 +1,27 @@ +/* + * Copyright © 2024 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.android.ui.navigation + +import kotlinx.serialization.Serializable + +sealed interface Destination + +@Serializable data object Hottest : Destination + +@Serializable data object Newest : Destination + +@Serializable data object Saved : Destination + +@Serializable data class Comments(val postId: String) : Destination + +@Serializable data class User(val username: String) : Destination + +@Serializable data object Search : Destination + +@Serializable data object Settings : Destination + +@Serializable data object AboutLibraries : Destination diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destinations.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destinations.kt deleted file mode 100644 index d6bed1f0..00000000 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destinations.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright © 2024 Harsh Shandilya. - * Use of this source code is governed by an MIT-style - * license that can be found in the LICENSE file or at - * https://opensource.org/licenses/MIT. - */ -package dev.msfjarvis.claw.android.ui.navigation - -sealed class Destinations { - abstract val route: String - - data object Hottest : Destinations() { - override val route = "hottest" - } - - data object Newest : Destinations() { - override val route = "newest" - } - - data object Saved : Destinations() { - override val route = "saved" - } - - data object Comments : Destinations() { - const val PLACEHOLDER = "{postId}" - override val route = "comments/$PLACEHOLDER" - } - - data object User : Destinations() { - const val PLACEHOLDER = "{username}" - override val route = "user/$PLACEHOLDER" - } - - data object Search : Destinations() { - override val route = "search" - } - - data object Settings : Destinations() { - override val route = "settings" - } - - data object AboutLibraries : Destinations() { - override val route = "about_libraries" - } - - companion object { - val startDestination - get() = Hottest - } -} diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/NavigationExtensions.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/NavigationExtensions.kt new file mode 100644 index 00000000..d6a7db90 --- /dev/null +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/NavigationExtensions.kt @@ -0,0 +1,30 @@ +/* + * Copyright © 2024 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.android.ui.navigation + +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavDestination.Companion.hierarchy +import kotlinx.collections.immutable.ImmutableList + +/** + * Walk through the [NavDestination]'s [hierarchy] to see if it has any destination that matches the + * route defined by [dest]. + */ +fun NavDestination?.matches(dest: Destination): Boolean { + return this?.hierarchy?.any { it.hasRoute(dest::class) } == true +} + +/** Check if this [NavDestination] [matches] any of the potential navigation [destinations]. */ +fun NavDestination?.any(destinations: ImmutableList): Boolean { + return destinations.any { this?.matches(it) == true } +} + +/** Check if this [NavDestination] [matches] none of the potential navigation [destinations]. */ +fun NavDestination?.none(destinations: ImmutableList): Boolean { + return destinations.none { this?.matches(it) == true } +} diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt index a8abf9cd..d9b2ebb6 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt @@ -51,12 +51,11 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument +import androidx.navigation.toRoute import androidx.paging.compose.collectAsLazyPagingItems import com.deliveryhero.whetstone.compose.injectedViewModel import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer @@ -69,8 +68,16 @@ import dev.msfjarvis.claw.android.ui.decorations.NavigationItem import dev.msfjarvis.claw.android.ui.getActivity import dev.msfjarvis.claw.android.ui.lists.DatabasePosts import dev.msfjarvis.claw.android.ui.lists.NetworkPosts +import dev.msfjarvis.claw.android.ui.navigation.AboutLibraries import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType -import dev.msfjarvis.claw.android.ui.navigation.Destinations +import dev.msfjarvis.claw.android.ui.navigation.Comments +import dev.msfjarvis.claw.android.ui.navigation.Hottest +import dev.msfjarvis.claw.android.ui.navigation.Newest +import dev.msfjarvis.claw.android.ui.navigation.Saved +import dev.msfjarvis.claw.android.ui.navigation.Settings +import dev.msfjarvis.claw.android.ui.navigation.User +import dev.msfjarvis.claw.android.ui.navigation.any +import dev.msfjarvis.claw.android.ui.navigation.none import dev.msfjarvis.claw.android.ui.rememberPostActions import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.common.comments.CommentsPage @@ -79,6 +86,7 @@ import dev.msfjarvis.claw.common.urllauncher.UrlLauncher import dev.msfjarvis.claw.common.user.UserProfile import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @@ -96,11 +104,11 @@ fun LobstersPostsScreen( val newestListState = rememberLazyListState() val savedListState = rememberLazyListState() val navController = rememberNavController() + val navBackStackEntry = navController.currentBackStackEntryAsState().value + val currentDestination = navBackStackEntry?.destination val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val postActions = rememberPostActions(context, urlLauncher, navController, viewModel) - val backStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = backStackEntry?.destination?.route val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems() val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems() @@ -111,9 +119,7 @@ fun LobstersPostsScreen( val postIdOverride = context.getActivity()?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY) LaunchedEffect(Unit) { if (postIdOverride != null) { - navController.navigate( - Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postIdOverride) - ) + navController.navigate(Comments(postIdOverride)) } } @@ -121,7 +127,7 @@ fun LobstersPostsScreen( persistentListOf( NavigationItem( label = "Hottest", - route = Destinations.Hottest.route, + destination = Hottest, icon = Icons.Outlined.Whatshot, selectedIcon = Icons.Filled.Whatshot, ) { @@ -131,7 +137,7 @@ fun LobstersPostsScreen( }, NavigationItem( label = "Newest", - route = Destinations.Newest.route, + destination = Newest, icon = Icons.Outlined.NewReleases, selectedIcon = Icons.Filled.NewReleases, ) { @@ -141,13 +147,14 @@ fun LobstersPostsScreen( }, NavigationItem( label = "Saved", - route = Destinations.Saved.route, + destination = Saved, icon = Icons.Outlined.FavoriteBorder, selectedIcon = Icons.Filled.Favorite, ) { coroutineScope.launch { if (savedPosts.isNotEmpty()) savedListState.scrollToItem(0) } }, ) + val navDestinations = navItems.map(NavigationItem::destination).toPersistentList() Scaffold( topBar = { @@ -155,8 +162,7 @@ fun LobstersPostsScreen( modifier = Modifier.shadow(8.dp), navigationIcon = { if ( - navController.previousBackStackEntry != null && - navItems.none { it.route == currentDestination } + navController.previousBackStackEntry != null && currentDestination.none(navDestinations) ) { IconButton( onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() } @@ -175,18 +181,18 @@ fun LobstersPostsScreen( } }, title = { - if (navItems.any { it.route == currentDestination }) { + if (currentDestination.any(navDestinations)) { Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold) } }, actions = { - if (navItems.any { it.route == currentDestination }) { + if (currentDestination.any(navDestinations)) { IconButton( onClick = { context.startActivity(Intent(context, SearchActivity::class.java)) } ) { Icon(imageVector = Icons.Filled.Search, contentDescription = "Search posts") } - IconButton(onClick = { navController.navigate(Destinations.Settings.route) }) { + IconButton(onClick = { navController.navigate(Settings) }) { Icon(imageVector = Icons.Filled.Tune, contentDescription = "Settings") } } @@ -198,7 +204,7 @@ fun LobstersPostsScreen( ClawNavigationBar( navController = navController, items = navItems, - isVisible = navItems.any { it.route == currentDestination }, + isVisible = currentDestination.any(navDestinations), ) } }, @@ -210,18 +216,18 @@ fun LobstersPostsScreen( ClawNavigationRail( navController = navController, items = navItems, - isVisible = navItems.any { it.route == currentDestination }, + isVisible = currentDestination.any(navDestinations), ) } NavHost( navController = navController, - startDestination = Destinations.startDestination.route, + startDestination = Hottest, // Make animations 2x faster than default specs enterTransition = { fadeIn(animationSpec = tween(350)) }, exitTransition = { fadeOut(animationSpec = tween(350)) }, ) { - composable(route = Destinations.Hottest.route) { + composable { setWebUri("https://lobste.rs/") NetworkPosts( lazyPagingItems = hottestPosts, @@ -229,7 +235,7 @@ fun LobstersPostsScreen( postActions = postActions, ) } - composable(route = Destinations.Newest.route) { + composable { setWebUri("https://lobste.rs/") NetworkPosts( lazyPagingItems = newestPosts, @@ -237,18 +243,12 @@ fun LobstersPostsScreen( postActions = postActions, ) } - composable(route = Destinations.Saved.route) { + composable { setWebUri(null) DatabasePosts(items = savedPosts, listState = savedListState, postActions = postActions) } - composable( - route = Destinations.Comments.route, - arguments = listOf(navArgument("postId") { type = NavType.StringType }), - ) { backStackEntry -> - val postId = - requireNotNull(backStackEntry.arguments?.getString("postId")) { - "Navigating to ${Destinations.Comments.route} without necessary 'postId' argument" - } + composable { backStackEntry -> + val postId = backStackEntry.toRoute().postId setWebUri("https://lobste.rs/s/$postId") CommentsPage( postId = postId, @@ -256,45 +256,29 @@ fun LobstersPostsScreen( htmlConverter = htmlConverter, getSeenComments = viewModel::getSeenComments, markSeenComments = viewModel::markSeenComments, - openUserProfile = { - navController.navigate( - Destinations.User.route.replace(Destinations.User.PLACEHOLDER, it) - ) - }, + openUserProfile = { navController.navigate(User(it)) }, ) } - composable( - route = Destinations.User.route, - arguments = listOf(navArgument("username") { type = NavType.StringType }), - ) { backStackEntry -> - val username = - requireNotNull(backStackEntry.arguments?.getString("username")) { - "Navigating to ${Destinations.User.route} without necessary 'username' argument" - } + composable { backStackEntry -> + val username = backStackEntry.toRoute().username setWebUri("https://lobste.rs/u/$username") UserProfile( username = username, getProfile = viewModel::getUserProfile, - openUserProfile = { - navController.navigate( - Destinations.User.route.replace(Destinations.User.PLACEHOLDER, it) - ) - }, + openUserProfile = { navController.navigate(User(it)) }, ) } - composable(route = Destinations.Settings.route) { + composable { SettingsScreen( context = context, - openLibrariesScreen = { navController.navigate(Destinations.AboutLibraries.route) }, + openLibrariesScreen = { navController.navigate(AboutLibraries) }, importPosts = viewModel::importPosts, exportPostsAsJson = viewModel::exportPostsAsJson, exportPostsAsHtml = viewModel::exportPostsAsHtml, snackbarHostState = snackbarHostState, ) } - composable(route = Destinations.AboutLibraries.route) { - LibrariesContainer(modifier = Modifier.fillMaxSize()) - } + composable { LibrariesContainer(modifier = Modifier.fillMaxSize()) } } } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt index 3a88dac1..c6fa72f7 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt @@ -12,14 +12,15 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.deliveryhero.whetstone.compose.injectedViewModel import dev.msfjarvis.claw.android.ui.lists.SearchList -import dev.msfjarvis.claw.android.ui.navigation.Destinations +import dev.msfjarvis.claw.android.ui.navigation.Comments +import dev.msfjarvis.claw.android.ui.navigation.Search +import dev.msfjarvis.claw.android.ui.navigation.User import dev.msfjarvis.claw.android.ui.rememberPostActions import dev.msfjarvis.claw.android.viewmodel.ClawViewModel import dev.msfjarvis.claw.common.comments.CommentsPage @@ -41,10 +42,10 @@ fun SearchScreen( Scaffold(modifier = modifier) { paddingValues -> NavHost( navController = navController, - startDestination = Destinations.Search.route, + startDestination = Search, modifier = Modifier.padding(paddingValues), ) { - composable(route = Destinations.Search.route) { + composable { setWebUri("https://lobste.rs/search") SearchList( items = viewModel.searchResults, @@ -54,14 +55,8 @@ fun SearchScreen( setSearchQuery = { query -> viewModel.searchQuery = query }, ) } - composable( - route = Destinations.Comments.route, - arguments = listOf(navArgument("postId") { type = NavType.StringType }), - ) { backStackEntry -> - val postId = - requireNotNull(backStackEntry.arguments?.getString("postId")) { - "Navigating to ${Destinations.Comments.route} without necessary 'postId' argument" - } + composable { backStackEntry -> + val postId = backStackEntry.toRoute().postId setWebUri("https://lobste.rs/s/$postId") CommentsPage( postId = postId, @@ -69,30 +64,16 @@ fun SearchScreen( htmlConverter = htmlConverter, getSeenComments = viewModel::getSeenComments, markSeenComments = viewModel::markSeenComments, - openUserProfile = { username: String -> - navController.navigate( - Destinations.User.route.replace(Destinations.User.PLACEHOLDER, username) - ) - }, + openUserProfile = { navController.navigate(User(it)) }, ) } - composable( - route = Destinations.User.route, - arguments = listOf(navArgument("username") { type = NavType.StringType }), - ) { backStackEntry -> - val username = - requireNotNull(backStackEntry.arguments?.getString("username")) { - "Navigating to ${Destinations.User.route} without necessary 'username' argument" - } + composable { backStackEntry -> + val username = backStackEntry.toRoute().username setWebUri("https://lobste.rs/u/$username") UserProfile( username = username, getProfile = viewModel::getUserProfile, - openUserProfile = { - navController.navigate( - Destinations.User.route.replace(Destinations.User.PLACEHOLDER, it) - ) - }, + openUserProfile = { navController.navigate(User(it)) }, ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fbfc7375..4b04d5b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ kotlin = "2.0.0" kotlinResult = "2.0.0" leakcanary = "3.0-alpha-8" lifecycle = "2.8.3" +navigation = "2.8.0-beta04" retrofit = "2.11.0" richtext = "1.0.0-alpha01" sentry-sdk = "7.11.0" @@ -48,7 +49,7 @@ androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", ve androidx-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" } androidx-lint = "androidx.lint:lint-gradle:1.0.0-alpha01" -androidx-navigation-compose = "androidx.navigation:navigation-compose:2.8.0-beta04" +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" } androidx-paging-compose = "androidx.paging:paging-compose:3.3.0" androidx-profileinstaller = "androidx.profileinstaller:profileinstaller:1.4.0-alpha01" androidx-test-core = "androidx.test:core:1.6.1" @@ -128,6 +129,7 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi ksp = "com.google.devtools.ksp:2.0.0-1.0.22" licensee = "app.cash.licensee:1.11.0" modulegraphassert = "com.jraska.module.graph.assertion:2.5.0" +navigation-safeargs = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigation" } poko = "dev.drewhamilton.poko:0.16.0" sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } tracelog = "dev.msfjarvis.tracelog:0.1.3" diff --git a/settings.gradle.kts b/settings.gradle.kts index 141ea844..2c2ef6b5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,7 @@ pluginManagement { includeGroup("androidx.baselineprofile") includeGroup("androidx.benchmark") includeGroup("androidx.databinding") + includeGroupAndSubgroups("androidx.navigation") includeGroup("com.google.testing.platform") includeGroupAndSubgroups("com.android") }