Skip to content

Commit a0b22d8

Browse files
author
Manuel Vivo
authored
Updates Navigation approach with new guidance (#347)
1 parent 02cf6fa commit a0b22d8

File tree

32 files changed

+296
-278
lines changed

32 files changed

+296
-278
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ dependencies {
8585

8686
implementation(project(":core:ui"))
8787
implementation(project(":core:designsystem"))
88-
implementation(project(":core:navigation"))
8988

9089
implementation(project(":sync:work"))
9190
implementation(project(":sync:sync-test"))
@@ -104,6 +103,8 @@ dependencies {
104103
implementation(libs.androidx.compose.runtime)
105104
implementation(libs.androidx.compose.runtime.tracing)
106105
implementation(libs.androidx.compose.material3.windowSizeClass)
106+
implementation(libs.androidx.hilt.navigation.compose)
107+
implementation(libs.androidx.navigation.compose)
107108
implementation(libs.androidx.window.manager)
108109
implementation(libs.androidx.profileinstaller)
109110

@@ -118,4 +119,4 @@ configurations.configureEach {
118119
// Temporary workaround for https://issuetracker.google.com/174733673
119120
force("org.objenesis:objenesis:2.6")
120121
}
121-
}
122+
}

app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ class NiaAppStateTest {
8282
}
8383

8484
assertEquals(3, state.topLevelDestinations.size)
85-
assertTrue(state.topLevelDestinations[0].destination.contains("for_you"))
86-
assertTrue(state.topLevelDestinations[1].destination.contains("bookmarks"))
87-
assertTrue(state.topLevelDestinations[2].destination.contains("interests"))
85+
assertTrue(state.topLevelDestinations[0].name.contains("for_you", true))
86+
assertTrue(state.topLevelDestinations[1].name.contains("bookmarks", true))
87+
assertTrue(state.topLevelDestinations[2].name.contains("interests", true))
8888
}
8989

9090
@Test

app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@ import androidx.compose.runtime.Composable
2020
import androidx.compose.ui.Modifier
2121
import androidx.navigation.NavHostController
2222
import androidx.navigation.compose.NavHost
23-
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
24-
import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination
25-
import com.google.samples.apps.nowinandroid.feature.author.navigation.authorGraph
26-
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksGraph
27-
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
28-
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouGraph
23+
import com.google.samples.apps.nowinandroid.feature.author.navigation.authorScreen
24+
import com.google.samples.apps.nowinandroid.feature.author.navigation.navigateToAuthor
25+
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen
26+
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute
27+
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen
2928
import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsGraph
30-
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination
31-
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicGraph
29+
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
30+
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
3231

3332
/**
3433
* Top-level navigation graph. Navigation is organized as explained at
@@ -40,32 +39,27 @@ import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicGraph
4039
@Composable
4140
fun NiaNavHost(
4241
navController: NavHostController,
43-
onNavigateToDestination: (NiaNavigationDestination, String) -> Unit,
4442
onBackClick: () -> Unit,
4543
modifier: Modifier = Modifier,
46-
startDestination: String = ForYouDestination.route
44+
startDestination: String = forYouNavigationRoute
4745
) {
4846
NavHost(
4947
navController = navController,
5048
startDestination = startDestination,
5149
modifier = modifier,
5250
) {
53-
forYouGraph()
54-
bookmarksGraph()
51+
forYouScreen()
52+
bookmarksScreen()
5553
interestsGraph(
56-
navigateToTopic = {
57-
onNavigateToDestination(
58-
TopicDestination, TopicDestination.createNavigationRoute(it)
59-
)
54+
navigateToTopic = { topicId ->
55+
navController.navigateToTopic(topicId)
6056
},
61-
navigateToAuthor = {
62-
onNavigateToDestination(
63-
AuthorDestination, AuthorDestination.createNavigationRoute(it)
64-
)
57+
navigateToAuthor = { authorId ->
58+
navController.navigateToAuthor(authorId)
6559
},
6660
nestedGraphs = {
67-
topicGraph(onBackClick)
68-
authorGraph(onBackClick)
61+
topicScreen(onBackClick)
62+
authorScreen(onBackClick)
6963
}
7064
)
7165
}

app/src/main/java/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,36 @@
1717
package com.google.samples.apps.nowinandroid.navigation
1818

1919
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon
20-
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
20+
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
21+
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
22+
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
23+
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR
24+
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
25+
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
2126

2227
/**
2328
* Type for the top level destinations in the application. Each of these destinations
2429
* can contain one or more screens (based on the window size). Navigation from one screen to the
2530
* next within a single destination will be handled directly in composables.
2631
*/
27-
data class TopLevelDestination(
28-
override val route: String,
29-
override val destination: String,
32+
enum class TopLevelDestination(
3033
val selectedIcon: Icon,
3134
val unselectedIcon: Icon,
3235
val iconTextId: Int
33-
) : NiaNavigationDestination
36+
) {
37+
FOR_YOU(
38+
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
39+
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
40+
iconTextId = forYouR.string.for_you
41+
),
42+
BOOKMARKS(
43+
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
44+
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
45+
iconTextId = bookmarksR.string.saved
46+
),
47+
INTERESTS(
48+
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
49+
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
50+
iconTextId = interestsR.string.interests
51+
)
52+
}

app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavig
5252
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
5353
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
5454
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
55-
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
5655
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
5756
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
5857

@@ -69,7 +68,9 @@ fun NiaApp(
6968
NiaTheme {
7069
val background: @Composable (@Composable () -> Unit) -> Unit =
7170
when (appState.currentDestination?.route) {
72-
ForYouDestination.route -> { content -> NiaGradientBackground(content = content) }
71+
TopLevelDestination.FOR_YOU.name -> { content ->
72+
NiaGradientBackground(content = content)
73+
}
7374
else -> { content -> NiaBackground(content = content) }
7475
}
7576

@@ -85,7 +86,7 @@ fun NiaApp(
8586
if (appState.shouldShowBottomBar) {
8687
NiaBottomBar(
8788
destinations = appState.topLevelDestinations,
88-
onNavigateToDestination = appState::navigate,
89+
onNavigateToDestination = appState::navigateToTopLevelDestination,
8990
currentDestination = appState.currentDestination
9091
)
9192
}
@@ -103,7 +104,7 @@ fun NiaApp(
103104
if (appState.shouldShowNavRail) {
104105
NiaNavRail(
105106
destinations = appState.topLevelDestinations,
106-
onNavigateToDestination = appState::navigate,
107+
onNavigateToDestination = appState::navigateToTopLevelDestination,
107108
currentDestination = appState.currentDestination,
108109
modifier = Modifier.safeDrawingPadding()
109110
)
@@ -112,7 +113,6 @@ fun NiaApp(
112113
NiaNavHost(
113114
navController = appState.navController,
114115
onBackClick = appState::onBackClick,
115-
onNavigateToDestination = appState::navigate,
116116
modifier = Modifier
117117
.padding(padding)
118118
.consumedWindowInsets(padding)
@@ -132,8 +132,7 @@ private fun NiaNavRail(
132132
) {
133133
NiaNavigationRail(modifier = modifier) {
134134
destinations.forEach { destination ->
135-
val selected =
136-
currentDestination?.hierarchy?.any { it.route == destination.route } == true
135+
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
137136
NiaNavigationRailItem(
138137
selected = selected,
139138
onClick = { onNavigateToDestination(destination) },
@@ -168,8 +167,7 @@ private fun NiaBottomBar(
168167
) {
169168
NiaNavigationBar {
170169
destinations.forEach { destination ->
171-
val selected =
172-
currentDestination?.hierarchy?.any { it.route == destination.route } == true
170+
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
173171
NiaNavigationBarItem(
174172
selected = selected,
175173
onClick = { onNavigateToDestination(destination) },
@@ -195,3 +193,8 @@ private fun NiaBottomBar(
195193
}
196194
}
197195
}
196+
197+
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
198+
this?.hierarchy?.any {
199+
it.route?.contains(destination.name, true) ?: false
200+
} ?: false

app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt

Lines changed: 31 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,16 @@ import androidx.navigation.NavGraph.Companion.findStartDestination
2828
import androidx.navigation.NavHostController
2929
import androidx.navigation.compose.currentBackStackEntryAsState
3030
import androidx.navigation.compose.rememberNavController
31+
import androidx.navigation.navOptions
3132
import androidx.tracing.trace
32-
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
33-
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
34-
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
35-
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
3633
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
37-
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR
38-
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksDestination
39-
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
40-
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
41-
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
42-
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsDestination
34+
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks
35+
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou
36+
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterestsGraph
4337
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
38+
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.BOOKMARKS
39+
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU
40+
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
4441

4542
@Composable
4643
fun rememberNiaAppState(
@@ -72,61 +69,35 @@ class NiaAppState(
7269
/**
7370
* Top level destinations to be used in the BottomBar and NavRail
7471
*/
75-
val topLevelDestinations: List<TopLevelDestination> = listOf(
76-
TopLevelDestination(
77-
route = ForYouDestination.route,
78-
destination = ForYouDestination.destination,
79-
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
80-
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
81-
iconTextId = forYouR.string.for_you
82-
),
83-
TopLevelDestination(
84-
route = BookmarksDestination.route,
85-
destination = BookmarksDestination.destination,
86-
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
87-
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
88-
iconTextId = bookmarksR.string.saved
89-
),
90-
TopLevelDestination(
91-
route = InterestsDestination.route,
92-
destination = InterestsDestination.destination,
93-
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
94-
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
95-
iconTextId = interestsR.string.interests
96-
)
97-
)
72+
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.values().asList()
9873

9974
/**
100-
* UI logic for navigating to a particular destination in the app. The NavigationOptions to
101-
* navigate with are based on the type of destination, which could be a top level destination or
102-
* just a regular destination.
75+
* UI logic for navigating to a top level destination in the app. Top level destinations have
76+
* only one copy of the destination of the back stack, and save and restore state whenever you
77+
* navigate to and from it.
10378
*
104-
* Top level destinations have only one copy of the destination of the back stack, and save and
105-
* restore state whenever you navigate to and from it.
106-
* Regular destinations can have multiple copies in the back stack and state isn't saved nor
107-
* restored.
108-
*
109-
* @param destination: The [NiaNavigationDestination] the app needs to navigate to.
110-
* @param route: Optional route to navigate to in case the destination contains arguments.
79+
* @param topLevelDestination: The destination the app needs to navigate to.
11180
*/
112-
fun navigate(destination: NiaNavigationDestination, route: String? = null) {
113-
trace("Navigation: ${destination.route}") {
114-
if (destination is TopLevelDestination) {
115-
navController.navigate(route ?: destination.route) {
116-
// Pop up to the start destination of the graph to
117-
// avoid building up a large stack of destinations
118-
// on the back stack as users select items
119-
popUpTo(navController.graph.findStartDestination().id) {
120-
saveState = true
121-
}
122-
// Avoid multiple copies of the same destination when
123-
// reselecting the same item
124-
launchSingleTop = true
125-
// Restore state when reselecting a previously selected item
126-
restoreState = true
81+
fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
82+
trace("Navigation: ${topLevelDestination.name}") {
83+
val topLevelNavOptions = navOptions {
84+
// Pop up to the start destination of the graph to
85+
// avoid building up a large stack of destinations
86+
// on the back stack as users select items
87+
popUpTo(navController.graph.findStartDestination().id) {
88+
saveState = true
12789
}
128-
} else {
129-
navController.navigate(route ?: destination.route)
90+
// Avoid multiple copies of the same destination when
91+
// reselecting the same item
92+
launchSingleTop = true
93+
// Restore state when reselecting a previously selected item
94+
restoreState = true
95+
}
96+
97+
when (topLevelDestination) {
98+
FOR_YOU -> navController.navigateToForYou(topLevelNavOptions)
99+
BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions)
100+
INTERESTS -> navController.navigateToInterestsGraph(topLevelNavOptions)
130101
}
131102
}
132103
}

build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
4444
add("implementation", project(":core:designsystem"))
4545
add("implementation", project(":core:data"))
4646
add("implementation", project(":core:common"))
47-
add("implementation", project(":core:navigation"))
4847
add("implementation", project(":core:domain"))
4948

5049
add("testImplementation", project(":core:testing"))
@@ -68,4 +67,4 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
6867
}
6968
}
7069
}
71-
}
70+
}

core/navigation/build.gradle.kts renamed to core/common/src/main/java/com/google/samples/apps/nowinandroid/core/decoder/StringDecoder.kt

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,9 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
17-
@Suppress("DSL_SCOPE_VIOLATION")
18-
plugins {
19-
id("nowinandroid.android.library")
20-
id("nowinandroid.android.library.jacoco")
21-
id("nowinandroid.android.hilt")
22-
}
2316

24-
android {
25-
namespace = "com.google.samples.apps.nowinandroid.core.navigation"
26-
}
17+
package com.google.samples.apps.nowinandroid.core.decoder
2718

28-
dependencies {
29-
api(libs.androidx.hilt.navigation.compose)
30-
api(libs.androidx.navigation.compose)
31-
}
19+
interface StringDecoder {
20+
fun decodeString(encodedString: String): String
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2022 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.samples.apps.nowinandroid.core.decoder
18+
19+
import android.net.Uri
20+
import javax.inject.Inject
21+
22+
class UriDecoder @Inject constructor() : StringDecoder {
23+
override fun decodeString(encodedString: String): String = Uri.decode(encodedString)
24+
}

0 commit comments

Comments
 (0)