Add state holder for NiaApp composable

pull/186/head
Manuel Vivo 3 years ago
parent c35e530d92
commit 0eee3620b8

@ -108,6 +108,8 @@ dependencies {
androidTestImplementation(project(":core-datastore-test")) androidTestImplementation(project(":core-datastore-test"))
androidTestImplementation(project(":core-data-test")) androidTestImplementation(project(":core-data-test"))
androidTestImplementation(project(":core-network")) androidTestImplementation(project(":core-network"))
androidTestImplementation(libs.androidx.navigation.testing)
debugImplementation(libs.androidx.compose.ui.testManifest)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat) 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" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".NiaApp" android:name=".NiaApplication"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"

@ -27,7 +27,7 @@ import dagger.hilt.android.HiltAndroidApp
* [Application] class for NiA * [Application] class for NiA
*/ */
@HiltAndroidApp @HiltAndroidApp
class NiaApp : Application(), ImageLoaderFactory { class NiaApplication : Application(), ImageLoaderFactory {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Initialize Sync; the system responsible for keeping data in the app up to date. // 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.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost 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.AuthorDestination
import com.google.samples.apps.nowinandroid.feature.author.navigation.authorGraph import com.google.samples.apps.nowinandroid.feature.author.navigation.authorGraph
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination 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 @Composable
fun NiaNavHost( fun NiaNavHost(
navController: NavHostController,
onNavigateToDestination: (NiaNavigationDestination, String) -> Unit,
onBackClick: () -> Unit,
windowSizeClass: WindowSizeClass, windowSizeClass: WindowSizeClass,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = ForYouDestination.route startDestination: String = ForYouDestination.route
) { ) {
NavHost( NavHost(
@ -53,11 +55,19 @@ fun NiaNavHost(
windowSizeClass = windowSizeClass windowSizeClass = windowSizeClass
) )
interestsGraph( interestsGraph(
navigateToTopic = { navController.navigate("${TopicDestination.route}/$it") }, navigateToTopic = {
navigateToAuthor = { navController.navigate("${AuthorDestination.route}/$it") }, onNavigateToDestination(
TopicDestination, TopicDestination.createNavigationRoute(it)
)
},
navigateToAuthor = {
onNavigateToDestination(
AuthorDestination, AuthorDestination.createNavigationRoute(it)
)
},
nestedGraphs = { nestedGraphs = {
topicGraph(onBackClick = { navController.popBackStack() }) topicGraph(onBackClick)
authorGraph(onBackClick = { navController.popBackStack() }) 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.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.res.stringResource
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy 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.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBarItem 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.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon 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.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.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 import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@OptIn( @OptIn(
@ -71,16 +61,11 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
ExperimentalComposeUiApi::class ExperimentalComposeUiApi::class
) )
@Composable @Composable
fun NiaApp(windowSizeClass: WindowSizeClass) { fun NiaApp(
windowSizeClass: WindowSizeClass,
appState: NiaAppState = rememberNiaAppState(windowSizeClass)
) {
NiaTheme { NiaTheme {
val navController = rememberNavController()
val niaTopLevelNavigation = remember(navController) {
NiaTopLevelNavigation(navController)
}
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
NiaBackground { NiaBackground {
Scaffold( Scaffold(
modifier = Modifier.semantics { modifier = Modifier.semantics {
@ -89,12 +74,11 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
containerColor = Color.Transparent, containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
bottomBar = { bottomBar = {
if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact || if (appState.shouldShowBottomBar) {
windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact
) {
NiaBottomBar( NiaBottomBar(
onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo, destinations = appState.topLevelDestinations,
currentDestination = currentDestination onNavigateToDestination = appState::navigate,
currentDestination = appState.currentDestination
) )
} }
} }
@ -108,19 +92,20 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
) )
) )
) { ) {
if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact && if (appState.shouldShowNavRail) {
windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact
) {
NiaNavRail( NiaNavRail(
onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo, destinations = appState.topLevelDestinations,
currentDestination = currentDestination, onNavigateToDestination = appState::navigate,
currentDestination = appState.currentDestination,
modifier = Modifier.safeDrawingPadding() modifier = Modifier.safeDrawingPadding()
) )
} }
NiaNavHost( NiaNavHost(
windowSizeClass = windowSizeClass, navController = appState.navController,
navController = navController, onBackClick = appState::onBackClick,
onNavigateToDestination = appState::navigate,
windowSizeClass = appState.windowSizeClass,
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumedWindowInsets(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 @Composable
private fun NiaNavRail( private fun NiaNavRail(
onNavigateToTopLevelDestination: (TopLevelDestination) -> Unit, destinations: List<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?, currentDestination: NavDestination?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
NiaNavigationRail(modifier = modifier) { NiaNavigationRail(modifier = modifier) {
TOP_LEVEL_DESTINATIONS.forEach { destination -> destinations.forEach { destination ->
val selected = val selected =
currentDestination?.hierarchy?.any { it.route == destination.route } == true currentDestination?.hierarchy?.any { it.route == destination.route } == true
NiaNavigationRailItem( NiaNavigationRailItem(
selected = selected, selected = selected,
onClick = { onNavigateToTopLevelDestination(destination) }, onClick = { onNavigateToDestination(destination) },
icon = { icon = {
val icon = if (selected) { val icon = if (selected) {
destination.selectedIcon destination.selectedIcon
@ -180,7 +155,8 @@ private fun NiaNavRail(
@Composable @Composable
private fun NiaBottomBar( private fun NiaBottomBar(
onNavigateToTopLevelDestination: (TopLevelDestination) -> Unit, destinations: List<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination? currentDestination: NavDestination?
) { ) {
// Wrap the navigation bar in a surface so the color behind the system // Wrap the navigation bar in a surface so the color behind the system
@ -193,13 +169,12 @@ private fun NiaBottomBar(
) )
) )
) { ) {
destinations.forEach { destination ->
TOP_LEVEL_DESTINATIONS.forEach { destination ->
val selected = val selected =
currentDestination?.hierarchy?.any { it.route == destination.route } == true currentDestination?.hierarchy?.any { it.route == destination.route } == true
NiaNavigationBarItem( NiaNavigationBarItem(
selected = selected, selected = selected,
onClick = { onNavigateToTopLevelDestination(destination) }, onClick = { onNavigateToDestination(destination) },
icon = { icon = {
val icon = if (selected) { val icon = if (selected) {
destination.selectedIcon destination.selectedIcon

@ -0,0 +1,129 @@
/*
* 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.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)
}
}
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
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
)
)
// --------------------
// NAVIGATION LOGIC
// --------------------
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()
}
}
@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,7 @@
package com.google.samples.apps.nowinandroid.feature.author.navigation package com.google.samples.apps.nowinandroid.feature.author.navigation
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@ -24,20 +25,26 @@ import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestina
import com.google.samples.apps.nowinandroid.feature.author.AuthorRoute import com.google.samples.apps.nowinandroid.feature.author.AuthorRoute
object AuthorDestination : NiaNavigationDestination { object AuthorDestination : NiaNavigationDestination {
override val route = "author_route"
override val destination = "author_destination"
const val authorIdArg = "authorId" const val authorIdArg = "authorId"
override val route = "author_route/{$authorIdArg}"
override val destination = "author_destination"
fun createNavigationRoute(authorIdArg: String): String {
return "author_route/$authorIdArg"
}
fun fromNavArgs(entry: NavBackStackEntry): String {
return entry.arguments?.getString(authorIdArg)!!
}
} }
fun NavGraphBuilder.authorGraph( fun NavGraphBuilder.authorGraph(
onBackClick: () -> Unit onBackClick: () -> Unit
) { ) {
composable( composable(
route = "${AuthorDestination.route}/{${AuthorDestination.authorIdArg}}", route = AuthorDestination.route,
arguments = listOf( arguments = listOf(
navArgument(AuthorDestination.authorIdArg) { navArgument(AuthorDestination.authorIdArg) { type = NavType.StringType }
type = NavType.StringType
}
) )
) { ) {
AuthorRoute(onBackClick = onBackClick) AuthorRoute(onBackClick = onBackClick)

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.topic.navigation package com.google.samples.apps.nowinandroid.feature.topic.navigation
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@ -24,20 +25,26 @@ import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestina
import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute
object TopicDestination : NiaNavigationDestination { object TopicDestination : NiaNavigationDestination {
override val route = "topic_route"
override val destination = "topic_destination"
const val topicIdArg = "topicId" const val topicIdArg = "topicId"
override val route = "topic_route/{$topicIdArg}"
override val destination = "topic_destination"
fun createNavigationRoute(topicIdArg: String): String {
return "topic_route/$topicIdArg"
}
fun fromNavArgs(entry: NavBackStackEntry): String {
return entry.arguments?.getString(topicIdArg)!!
}
} }
fun NavGraphBuilder.topicGraph( fun NavGraphBuilder.topicGraph(
onBackClick: () -> Unit onBackClick: () -> Unit
) { ) {
composable( composable(
route = "${TopicDestination.route}/{${TopicDestination.topicIdArg}}", route = TopicDestination.route,
arguments = listOf( arguments = listOf(
navArgument(TopicDestination.topicIdArg) { navArgument(TopicDestination.topicIdArg) { type = NavType.StringType }
type = NavType.StringType
}
) )
) { ) {
TopicRoute(onBackClick = onBackClick) TopicRoute(onBackClick = onBackClick)

@ -75,6 +75,7 @@ androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navig
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } 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-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } 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-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" }
androidx-savedstate-ktx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref= "androidxSavedState"} androidx-savedstate-ktx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref= "androidxSavedState"}
androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidxStartup" } androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidxStartup" }

Loading…
Cancel
Save