Merge pull request #186 from android/mv/state_holder

Add state holder for the NiaApp composable
pull/191/head
Jolanda Verhoef 2 years ago committed by GitHub
commit e53bb89671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -108,6 +108,8 @@ dependencies {
androidTestImplementation(project(":core-datastore-test"))
androidTestImplementation(project(":core-data-test"))
androidTestImplementation(project(":core-network"))
androidTestImplementation(libs.androidx.navigation.testing)
debugImplementation(libs.androidx.compose.ui.testManifest)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)

@ -0,0 +1,145 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.ui
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.compose.composable
import androidx.navigation.createGraph
import androidx.navigation.testing.TestNavHostController
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
/**
* Tests [NiaAppState].
*
* Note: This could become an unit test if Robolectric is added to the project and the Context
* is faked.
*/
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
class NiaAppStateTest {
@get:Rule
val composeTestRule = createComposeRule()
private lateinit var state: NiaAppState
@Test
fun niaAppState_currentDestination() {
var currentDestination: String? = null
composeTestRule.setContent {
val navController = rememberTestNavController()
state = remember(navController) {
NiaAppState(
windowSizeClass = getCompactWindowClass(),
navController = navController
)
}
// Update currentDestination whenever it changes
currentDestination = state.currentDestination?.route
// Navigate to destination b once
LaunchedEffect(Unit) {
navController.setCurrentDestination("b")
}
}
assertEquals("b", currentDestination)
}
@Test
fun niaAppState_destinations() {
composeTestRule.setContent {
state = rememberNiaAppState(getCompactWindowClass())
}
assertEquals(2, state.topLevelDestinations.size)
assertTrue(state.topLevelDestinations[0].destination.contains("for_you"))
assertTrue(state.topLevelDestinations[1].destination.contains("interests"))
}
@Test
fun niaAppState_showBottomBar_compact() {
composeTestRule.setContent {
state = NiaAppState(
windowSizeClass = getCompactWindowClass(),
navController = NavHostController(LocalContext.current)
)
}
assertTrue(state.shouldShowBottomBar)
assertFalse(state.shouldShowNavRail)
}
@Test
fun niaAppState_showNavRail_medium() {
composeTestRule.setContent {
state = NiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
navController = NavHostController(LocalContext.current)
)
}
assertTrue(state.shouldShowNavRail)
assertFalse(state.shouldShowBottomBar)
}
@Test
fun niaAppState_showNavRail_large() {
composeTestRule.setContent {
state = NiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
navController = NavHostController(LocalContext.current)
)
}
assertTrue(state.shouldShowNavRail)
assertFalse(state.shouldShowBottomBar)
}
private fun getCompactWindowClass() = WindowSizeClass.calculateFromSize(DpSize(500.dp, 300.dp))
}
@Composable
private fun rememberTestNavController(): TestNavHostController {
val context = LocalContext.current
val navController = remember {
TestNavHostController(context).apply {
navigatorProvider.addNavigator(ComposeNavigator())
graph = createGraph(startDestination = "a") {
composable("a") { }
composable("b") { }
composable("c") { }
}
}
}
return navController
}

@ -21,7 +21,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".NiaApp"
android:name=".NiaApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"

@ -27,7 +27,7 @@ import dagger.hilt.android.HiltAndroidApp
* [Application] class for NiA
*/
@HiltAndroidApp
class NiaApp : Application(), ImageLoaderFactory {
class NiaApplication : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
// Initialize Sync; the system responsible for keeping data in the app up to date.

@ -21,7 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination
import com.google.samples.apps.nowinandroid.feature.author.navigation.authorGraph
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
@ -39,9 +39,11 @@ import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicGraph
*/
@Composable
fun NiaNavHost(
navController: NavHostController,
onNavigateToDestination: (NiaNavigationDestination, String) -> Unit,
onBackClick: () -> Unit,
windowSizeClass: WindowSizeClass,
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = ForYouDestination.route
) {
NavHost(
@ -53,11 +55,19 @@ fun NiaNavHost(
windowSizeClass = windowSizeClass
)
interestsGraph(
navigateToTopic = { navController.navigate("${TopicDestination.route}/$it") },
navigateToAuthor = { navController.navigate("${AuthorDestination.route}/$it") },
navigateToTopic = {
onNavigateToDestination(
TopicDestination, TopicDestination.createNavigationRoute(it)
)
},
navigateToAuthor = {
onNavigateToDestination(
AuthorDestination, AuthorDestination.createNavigationRoute(it)
)
},
nestedGraphs = {
topicGraph(onBackClick = { navController.popBackStack() })
authorGraph(onBackClick = { navController.popBackStack() })
topicGraph(onBackClick)
authorGraph(onBackClick)
}
)
}

@ -1,81 +0,0 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.navigation
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.foryou.R.string.for_you
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
import com.google.samples.apps.nowinandroid.feature.interests.R.string.interests
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsDestination
/**
* Routes for the different top level destinations in the application. Each of these destinations
* can contain one or more screens (based on the window size). Navigation from one screen to the
* next within a single destination will be handled directly in composables.
*/
/**
* Models the navigation top level actions in the app.
*/
class NiaTopLevelNavigation(private val navController: NavHostController) {
fun navigateTo(destination: TopLevelDestination) {
trace("Navigation: $destination") {
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
}
}
}
}
data class TopLevelDestination(
val route: String,
val selectedIcon: Icon,
val unselectedIcon: Icon,
val iconTextId: Int
)
val TOP_LEVEL_DESTINATIONS = listOf(
TopLevelDestination(
route = ForYouDestination.route,
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = for_you
),
TopLevelDestination(
route = InterestsDestination.route,
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
iconTextId = interests
)
)

@ -0,0 +1,33 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.navigation
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
/**
* Type for the top level destinations in the application. Each of these destinations
* can contain one or more screens (based on the window size). Navigation from one screen to the
* next within a single destination will be handled directly in composables.
*/
data class TopLevelDestination(
override val route: String,
override val destination: String,
val selectedIcon: Icon,
val unselectedIcon: Icon,
val iconTextId: Int
) : NiaNavigationDestination

@ -33,12 +33,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
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.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -46,11 +42,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBarItem
@ -59,10 +52,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavig
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.ui.JankMetricDisposableEffect
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.NiaTopLevelNavigation
import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_DESTINATIONS
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@OptIn(
@ -71,16 +61,11 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
ExperimentalComposeUiApi::class
)
@Composable
fun NiaApp(windowSizeClass: WindowSizeClass) {
fun NiaApp(
windowSizeClass: WindowSizeClass,
appState: NiaAppState = rememberNiaAppState(windowSizeClass)
) {
NiaTheme {
val navController = rememberNavController()
val niaTopLevelNavigation = remember(navController) {
NiaTopLevelNavigation(navController)
}
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
NiaBackground {
Scaffold(
modifier = Modifier.semantics {
@ -89,12 +74,11 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
bottomBar = {
if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact
) {
if (appState.shouldShowBottomBar) {
NiaBottomBar(
onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo,
currentDestination = currentDestination
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigate,
currentDestination = appState.currentDestination
)
}
}
@ -108,19 +92,20 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
)
)
) {
if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact &&
windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact
) {
if (appState.shouldShowNavRail) {
NiaNavRail(
onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo,
currentDestination = currentDestination,
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigate,
currentDestination = appState.currentDestination,
modifier = Modifier.safeDrawingPadding()
)
}
NiaNavHost(
windowSizeClass = windowSizeClass,
navController = navController,
navController = appState.navController,
onBackClick = appState::onBackClick,
onNavigateToDestination = appState::navigate,
windowSizeClass = appState.windowSizeClass,
modifier = Modifier
.padding(padding)
.consumedWindowInsets(padding)
@ -128,33 +113,23 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
}
}
}
JankMetricDisposableEffect(navController) { metricsHolder ->
val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
metricsHolder.state?.addState("Navigation", destination.route.toString())
}
navController.addOnDestinationChangedListener(listener)
onDispose {
navController.removeOnDestinationChangedListener(listener)
}
}
}
}
@Composable
private fun NiaNavRail(
onNavigateToTopLevelDestination: (TopLevelDestination) -> Unit,
destinations: List<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?,
modifier: Modifier = Modifier,
) {
NiaNavigationRail(modifier = modifier) {
TOP_LEVEL_DESTINATIONS.forEach { destination ->
destinations.forEach { destination ->
val selected =
currentDestination?.hierarchy?.any { it.route == destination.route } == true
NiaNavigationRailItem(
selected = selected,
onClick = { onNavigateToTopLevelDestination(destination) },
onClick = { onNavigateToDestination(destination) },
icon = {
val icon = if (selected) {
destination.selectedIcon
@ -180,7 +155,8 @@ private fun NiaNavRail(
@Composable
private fun NiaBottomBar(
onNavigateToTopLevelDestination: (TopLevelDestination) -> Unit,
destinations: List<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?
) {
// Wrap the navigation bar in a surface so the color behind the system
@ -193,13 +169,12 @@ private fun NiaBottomBar(
)
)
) {
TOP_LEVEL_DESTINATIONS.forEach { destination ->
destinations.forEach { destination ->
val selected =
currentDestination?.hierarchy?.any { it.route == destination.route } == true
NiaNavigationBarItem(
selected = selected,
onClick = { onNavigateToTopLevelDestination(destination) },
onClick = { onNavigateToDestination(destination) },
icon = {
val icon = if (selected) {
destination.selectedIcon

@ -0,0 +1,146 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.ui
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
import com.google.samples.apps.nowinandroid.core.ui.JankMetricDisposableEffect
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsDestination
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@Composable
fun rememberNiaAppState(
windowSizeClass: WindowSizeClass,
navController: NavHostController = rememberNavController()
): NiaAppState {
NavigationTrackingSideEffect(navController)
return remember(navController, windowSizeClass) {
NiaAppState(navController, windowSizeClass)
}
}
@Stable
class NiaAppState(
val navController: NavHostController,
val windowSizeClass: WindowSizeClass
) {
val currentDestination: NavDestination?
@Composable get() = navController
.currentBackStackEntryAsState().value?.destination
val shouldShowBottomBar: Boolean
get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact
val shouldShowNavRail: Boolean
get() = !shouldShowBottomBar
/**
* Top level destinations to be used in the BottomBar and NavRail
*/
val topLevelDestinations: List<TopLevelDestination> = listOf(
TopLevelDestination(
route = ForYouDestination.route,
destination = ForYouDestination.destination,
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = forYouR.string.for_you
),
TopLevelDestination(
route = InterestsDestination.route,
destination = InterestsDestination.destination,
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
iconTextId = interestsR.string.interests
)
)
/**
* UI logic for navigating to a particular destination in the app. The NavigationOptions to
* navigate with are based on the type of destination, which could be a top level destination or
* just a regular destination.
*
* Top level destinations have only one copy of the destination of the back stack, and save and
* restore state whenever you navigate to and from it.
* Regular destinations can have multiple copies in the back stack and state isn't saved nor
* restored.
*
* @param destination: The [NiaNavigationDestination] the app needs to navigate to.
* @param route: Optional route to navigate to in case the destination contains arguments.
*/
fun navigate(destination: NiaNavigationDestination, route: String? = null) {
trace("Navigation: $destination") {
if (destination is TopLevelDestination) {
navController.navigate(route ?: 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
}
} else {
navController.navigate(route ?: destination.route)
}
}
}
fun onBackClick() {
navController.popBackStack()
}
}
/**
* Stores information about navigation events to be used with JankStats
*/
@Composable
private fun NavigationTrackingSideEffect(navController: NavHostController) {
JankMetricDisposableEffect(navController) { metricsHolder ->
val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
metricsHolder.state?.addState("Navigation", destination.route.toString())
}
navController.addOnDestinationChangedListener(listener)
onDispose {
navController.removeOnDestinationChangedListener(listener)
}
}
}

@ -16,6 +16,8 @@
package com.google.samples.apps.nowinandroid.feature.author.navigation
import android.net.Uri
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType
import androidx.navigation.compose.composable
@ -24,20 +26,34 @@ import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestina
import com.google.samples.apps.nowinandroid.feature.author.AuthorRoute
object AuthorDestination : NiaNavigationDestination {
override val route = "author_route"
override val destination = "author_destination"
const val authorIdArg = "authorId"
override val route = "author_route/{$authorIdArg}"
override val destination = "author_destination"
/**
* Creates destination route for an authorId that could include special characters
*/
fun createNavigationRoute(authorIdArg: String): String {
val encodedId = Uri.encode(authorIdArg)
return "author_route/$encodedId"
}
/**
* Returns the authorId from a [NavBackStackEntry] after an author destination navigation call
*/
fun fromNavArgs(entry: NavBackStackEntry): String {
val encodedId = entry.arguments?.getString(authorIdArg)!!
return Uri.decode(encodedId)
}
}
fun NavGraphBuilder.authorGraph(
onBackClick: () -> Unit
) {
composable(
route = "${AuthorDestination.route}/{${AuthorDestination.authorIdArg}}",
route = AuthorDestination.route,
arguments = listOf(
navArgument(AuthorDestination.authorIdArg) {
type = NavType.StringType
}
navArgument(AuthorDestination.authorIdArg) { type = NavType.StringType }
)
) {
AuthorRoute(onBackClick = onBackClick)

@ -16,6 +16,8 @@
package com.google.samples.apps.nowinandroid.feature.topic.navigation
import android.net.Uri
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType
import androidx.navigation.compose.composable
@ -24,20 +26,34 @@ import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestina
import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute
object TopicDestination : NiaNavigationDestination {
override val route = "topic_route"
override val destination = "topic_destination"
const val topicIdArg = "topicId"
override val route = "topic_route/{$topicIdArg}"
override val destination = "topic_destination"
/**
* Creates destination route for a topicId that could include special characters
*/
fun createNavigationRoute(topicIdArg: String): String {
val encodedId = Uri.encode(topicIdArg)
return "topic_route/$encodedId"
}
/**
* Returns the topicId from a [NavBackStackEntry] after a topic destination navigation call
*/
fun fromNavArgs(entry: NavBackStackEntry): String {
val encodedId = entry.arguments?.getString(topicIdArg)!!
return Uri.decode(encodedId)
}
}
fun NavGraphBuilder.topicGraph(
onBackClick: () -> Unit
) {
composable(
route = "${TopicDestination.route}/{${TopicDestination.topicIdArg}}",
route = TopicDestination.route,
arguments = listOf(
navArgument(TopicDestination.topicIdArg) {
type = NavType.StringType
}
navArgument(TopicDestination.topicIdArg) { type = NavType.StringType }
)
) {
TopicRoute(onBackClick = onBackClick)

@ -76,6 +76,7 @@ androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "life
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" }
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" }
androidx-savedstate-ktx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref= "androidxSavedState"}
androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidxStartup" }

Loading…
Cancel
Save