Merge remote-tracking branch 'github/main' into jul22automerger

Change-Id: Ic54190a9d825ae9649d34a4e2bd9c83855511b71
pull/205/head
Jolanda Verhoef 3 years ago
commit 37ad3e6299

@ -109,6 +109,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)

@ -46,19 +46,19 @@ class NavigationTest {
@get:Rule(order = 0) @get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this) val hiltRule = HiltAndroidRule(this)
/**
* Use the primary activity to initialize the app normally.
*/
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<MainActivity>()
/** /**
* Create a temporary folder used to create a Data Store file. This guarantees that * Create a temporary folder used to create a Data Store file. This guarantees that
* the file is removed in between each test, preventing a crash. * the file is removed in between each test, preventing a crash.
*/ */
@BindValue @get:Rule(order = 2) @BindValue @get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/**
* Use the primary activity to initialize the app normally.
*/
@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<MainActivity>()
// The strings used for matching in these tests // The strings used for matching in these tests
private lateinit var done: String private lateinit var done: String
private lateinit var navigateUp: String private lateinit var navigateUp: String

@ -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.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(3, state.topLevelDestinations.size)
assertTrue(state.topLevelDestinations[0].destination.contains("for_you"))
assertTrue(state.topLevelDestinations[1].destination.contains("bookmarks"))
assertTrue(state.topLevelDestinations[2].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.bookmarks.navigation.bookmarksGraph import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksGraph
@ -40,9 +40,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(
@ -55,11 +57,19 @@ fun NiaNavHost(
) )
bookmarksGraph(windowSizeClass) bookmarksGraph(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,89 +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.bookmarks.navigation.BookmarksDestination
import com.google.samples.apps.nowinandroid.feature.foryou.R.string.for_you
import com.google.samples.apps.nowinandroid.feature.foryou.R.string.saved
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 = BookmarksDestination.route,
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
iconTextId = saved
),
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,155 @@
/*
* 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.bookmarks.R as bookmarksR
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksDestination
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 = BookmarksDestination.route,
destination = BookmarksDestination.destination,
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
iconTextId = bookmarksR.string.saved
),
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 package com.google.samples.apps.nowinandroid.feature.author.navigation
import android.net.Uri
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 +26,34 @@ 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"
/**
* 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( 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)

@ -15,6 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<string name="saved">Saved</string>
<string name="saved_loading">Loading saved…</string> <string name="saved_loading">Loading saved…</string>
<string name="top_app_bar_title_saved">Saved</string> <string name="top_app_bar_title_saved">Saved</string>
<string name="top_app_bar_action_search">Search</string> <string name="top_app_bar_action_search">Search</string>

@ -17,7 +17,6 @@
<resources> <resources>
<string name="for_you">For you</string> <string name="for_you">For you</string>
<string name="episodes">Episodes</string> <string name="episodes">Episodes</string>
<string name="saved">Saved</string>
<string name="done">Done</string> <string name="done">Done</string>
<string name="for_you_loading">Loading for you…</string> <string name="for_you_loading">Loading for you…</string>
<string name="navigate_up">Navigate up</string> <string name="navigate_up">Navigate up</string>

@ -16,6 +16,8 @@
package com.google.samples.apps.nowinandroid.feature.topic.navigation package com.google.samples.apps.nowinandroid.feature.topic.navigation
import android.net.Uri
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 +26,34 @@ 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"
/**
* 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( 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)

@ -4,7 +4,7 @@ androidDesugarJdkLibs = "1.1.5"
androidGradlePlugin = "7.2.1" androidGradlePlugin = "7.2.1"
androidxActivity = "1.4.0" androidxActivity = "1.4.0"
androidxAppCompat = "1.4.2" androidxAppCompat = "1.4.2"
androidxCompose = "1.2.0-rc02" androidxCompose = "1.2.0-rc03"
androidxComposeCompiler = "1.2.0" androidxComposeCompiler = "1.2.0"
androidxComposeMaterial3 = "1.0.0-alpha13" androidxComposeMaterial3 = "1.0.0-alpha13"
androidxCore = "1.8.0" androidxCore = "1.8.0"
@ -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-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