From 5ca7b6c60785217fce6d8fb75613d034f3b4a198 Mon Sep 17 00:00:00 2001 From: Simona Stojanovic Date: Tue, 10 May 2022 19:51:14 +0100 Subject: [PATCH] Refactor navigation Change-Id: I3501446e73976f3872592038501fcef8c8324f74 --- app/build.gradle.kts | 3 +- .../nowinandroid/navigation/NiaNavHost.kt | 89 +++++++++++++ .../navigation/NiaTopLevelNavigation.kt | 80 +++++++++++ .../samples/apps/nowinandroid/ui/NiaApp.kt | 77 ++--------- .../apps/nowinandroid/ui/NiaNavGraph.kt | 124 ------------------ .../apps/nowinandroid/ui/NiaNavigation.kt | 53 -------- core-navigation/.gitignore | 1 + .../build.gradle.kts | 27 ++-- core-navigation/src/main/AndroidManifest.xml | 20 +++ .../navigation/NiaNavigationDestination.kt | 38 ++++++ feature-author/build.gradle.kts | 1 + .../feature/author/AuthorViewModel.kt | 3 +- .../author/navigation/AuthorNavigation.kt | 45 +++++++ .../feature/author/AuthorViewModelTest.kt | 4 +- feature-foryou/build.gradle.kts | 1 + .../foryou/navigation/ForYouNavigation.kt | 36 +++++ feature-interests/build.gradle.kts | 1 + .../navigation/InterestsNavigation.kt | 39 ++++++ feature-topic/build.gradle.kts | 1 + .../nowinandroid/feature/topic/Navigation.kt | 33 ----- .../feature/topic/TopicViewModel.kt | 3 +- .../topic/navigation/TopicNavigation.kt | 45 +++++++ .../feature/topic/TopicViewModelTest.kt | 5 +- settings.gradle.kts | 1 + 24 files changed, 434 insertions(+), 296 deletions(-) create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt delete mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavGraph.kt delete mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavigation.kt create mode 100644 core-navigation/.gitignore rename feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/Navigation.kt => core-navigation/build.gradle.kts (56%) create mode 100644 core-navigation/src/main/AndroidManifest.xml create mode 100644 core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigationDestination.kt create mode 100644 feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/navigation/AuthorNavigation.kt create mode 100644 feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt create mode 100644 feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt delete mode 100644 feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/Navigation.kt create mode 100644 feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c360b1447..15405e118 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -76,6 +76,7 @@ dependencies { implementation(project(":feature-topic")) implementation(project(":core-ui")) + implementation(project(":core-navigation")) implementation(project(":sync")) @@ -88,8 +89,6 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.core.ktx) implementation(libs.androidx.compose.material3.windowSizeClass) - implementation(libs.androidx.hilt.navigation.compose) - implementation(libs.androidx.navigation.compose) implementation(libs.androidx.window.manager) implementation(libs.material3) implementation(libs.androidx.profileinstaller) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt new file mode 100644 index 000000000..74113a1d7 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt @@ -0,0 +1,89 @@ +/* + * 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 android.app.Activity +import android.view.View +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.core.view.doOnPreDraw +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +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 +import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouGraph +import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsGraph +import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination +import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicGraph + +/** + * Top-level navigation graph. Navigation is organized as explained at + * https://d.android.com/jetpack/compose/nav-adaptive + * + * The navigation graph defined in this file defines the different top level routes. Navigation + * within each route is handled using state and Back Handlers. + */ +@Composable +fun NiaNavHost( + windowSizeClass: WindowSizeClass, + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController(), + startDestination: String = ForYouDestination.route +) { + NavHost( + navController = navController, + startDestination = startDestination, + modifier = modifier, + ) { + forYouGraph( + windowSizeClass = windowSizeClass + ) + interestsGraph( + navigateToTopic = { navController.navigate("${TopicDestination.route}/$it") }, + navigateToAuthor = { navController.navigate("${AuthorDestination.route}/$it") } + ) + topicGraph( + onBackClick = { navController.popBackStack() } + ) + authorGraph( + onBackClick = { navController.popBackStack() } + ) + } +// Reporting the app fully drawn to get accurate TTFD readings for the baseline profile. +// https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD + ReportFullyDrawn(ForYouDestination.route) +} + +/** + * Calling [Activity#reportFullyDrawn] in compose UI. + */ +@Composable +private fun ReportFullyDrawn(destination: String) { + // Holding on to the local view and calling `reportFullyDrawn` in an `onPreDraw` listener. + // Compose currently doesn't offer a way to otherwise report fully drawn, + // so this is a viable approach. + val localView: View = LocalView.current + (localView.context as? Activity)?.run { + localView.doOnPreDraw { + reportFullyDrawn() + } + } +} diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt new file mode 100644 index 000000000..f2404e88c --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt @@ -0,0 +1,80 @@ +/* + * 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.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Grid3x3 +import androidx.compose.material.icons.filled.Upcoming +import androidx.compose.material.icons.outlined.Grid3x3 +import androidx.compose.material.icons.outlined.Upcoming +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +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) { + 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: ImageVector, + val unselectedIcon: ImageVector, + val iconTextId: Int +) + +val TOP_LEVEL_DESTINATIONS = listOf( + TopLevelDestination( + route = ForYouDestination.route, + selectedIcon = Icons.Filled.Upcoming, + unselectedIcon = Icons.Outlined.Upcoming, + iconTextId = for_you + ), + TopLevelDestination( + route = InterestsDestination.route, + selectedIcon = Icons.Filled.Grid3x3, + unselectedIcon = Icons.Outlined.Grid3x3, + iconTextId = interests + ) +) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 29a912c3e..3a3e25b66 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -16,7 +16,6 @@ package com.google.samples.apps.nowinandroid.ui -import androidx.annotation.StringRes import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets @@ -28,15 +27,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AutoStories -import androidx.compose.material.icons.filled.Bookmarks -import androidx.compose.material.icons.filled.Grid3x3 -import androidx.compose.material.icons.filled.Upcoming -import androidx.compose.material.icons.outlined.AutoStories -import androidx.compose.material.icons.outlined.Bookmarks -import androidx.compose.material.icons.outlined.Grid3x3 -import androidx.compose.material.icons.outlined.Upcoming import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -56,25 +46,27 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp 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.R import com.google.samples.apps.nowinandroid.core.ui.ClearRippleTheme import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme +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(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun NiaApp(windowSizeClass: WindowSizeClass) { NiaTheme { val navController = rememberNavController() - val navigationActions = remember(navController) { - NiaNavigationActions(navController) + val niaTopLevelNavigation = remember(navController) { + NiaTopLevelNavigation(navController) } val navBackStackEntry by navController.currentBackStackEntryAsState() @@ -88,7 +80,7 @@ fun NiaApp(windowSizeClass: WindowSizeClass) { bottomBar = { if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { NiABottomBar( - navigationActions = navigationActions, + onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo, currentDestination = currentDestination ) } @@ -105,13 +97,13 @@ fun NiaApp(windowSizeClass: WindowSizeClass) { ) { if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact) { NiANavRail( - navigationActions = navigationActions, + onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo, currentDestination = currentDestination, modifier = Modifier.safeDrawingPadding() ) } - NiaNavGraph( + NiaNavHost( windowSizeClass = windowSizeClass, navController = navController, modifier = Modifier @@ -126,7 +118,7 @@ fun NiaApp(windowSizeClass: WindowSizeClass) { @Composable private fun NiANavRail( - navigationActions: NiaNavigationActions, + onNavigateToTopLevelDestination: (TopLevelDestination) -> Unit, currentDestination: NavDestination?, modifier: Modifier = Modifier, ) { @@ -136,7 +128,7 @@ private fun NiANavRail( currentDestination?.hierarchy?.any { it.route == destination.route } == true NavigationRailItem( selected = selected, - onClick = { navigationActions.navigateToTopLevelDestination(destination.route) }, + onClick = { onNavigateToTopLevelDestination(destination) }, icon = { Icon( if (selected) destination.selectedIcon else destination.unselectedIcon, @@ -151,7 +143,7 @@ private fun NiANavRail( @Composable private fun NiABottomBar( - navigationActions: NiaNavigationActions, + onNavigateToTopLevelDestination: (TopLevelDestination) -> Unit, currentDestination: NavDestination? ) { // Wrap the navigation bar in a surface so the color behind the system @@ -172,9 +164,7 @@ private fun NiABottomBar( currentDestination?.hierarchy?.any { it.route == destination.route } == true NavigationBarItem( selected = selected, - onClick = { - navigationActions.navigateToTopLevelDestination(destination.route) - }, + onClick = { onNavigateToTopLevelDestination(destination) }, icon = { Icon( if (selected) { @@ -192,44 +182,3 @@ private fun NiABottomBar( } } } - -private sealed class Destination( - val route: String, - val selectedIcon: ImageVector, - val unselectedIcon: ImageVector, - @StringRes val iconTextId: Int -) { - object ForYou : Destination( - route = NiaDestinations.FOR_YOU_ROUTE, - selectedIcon = Icons.Filled.Upcoming, - unselectedIcon = Icons.Outlined.Upcoming, - iconTextId = R.string.for_you - ) - - object Episodes : Destination( - route = NiaDestinations.EPISODES_ROUTE, - selectedIcon = Icons.Filled.AutoStories, - unselectedIcon = Icons.Outlined.AutoStories, - iconTextId = R.string.episodes - ) - - object Saved : Destination( - route = NiaDestinations.SAVED_ROUTE, - selectedIcon = Icons.Filled.Bookmarks, - unselectedIcon = Icons.Outlined.Bookmarks, - iconTextId = R.string.saved - ) - - object Interests : Destination( - route = NiaDestinations.INTERESTS_ROUTE, - selectedIcon = Icons.Filled.Grid3x3, - unselectedIcon = Icons.Outlined.Grid3x3, - iconTextId = R.string.interests - ) -} - -private val TOP_LEVEL_DESTINATIONS = listOf( - Destination.ForYou, - // TODO: Add destinations here, see b/226359180. - Destination.Interests -) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavGraph.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavGraph.kt deleted file mode 100644 index 53029ea39..000000000 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavGraph.kt +++ /dev/null @@ -1,124 +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.ui - -import android.app.Activity -import android.view.View -import androidx.compose.material3.Text -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalView -import androidx.core.view.doOnPreDraw -import androidx.navigation.NavHostController -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.navigation -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import com.google.samples.apps.nowinandroid.feature.author.AuthorDestinations -import com.google.samples.apps.nowinandroid.feature.author.AuthorDestinationsArgs -import com.google.samples.apps.nowinandroid.feature.author.AuthorRoute -import com.google.samples.apps.nowinandroid.feature.author.InterestsScreens.AUTHOR_SCREEN -import com.google.samples.apps.nowinandroid.feature.foryou.ForYouRoute -import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute -import com.google.samples.apps.nowinandroid.feature.topic.InterestsDestinations -import com.google.samples.apps.nowinandroid.feature.topic.InterestsScreens.TOPIC_SCREEN -import com.google.samples.apps.nowinandroid.feature.topic.TopicDestinationsArgs -import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute - -/** - * Top-level navigation graph. Navigation is organized as explained at - * https://d.android.com/jetpack/compose/nav-adaptive - * - * The navigation graph defined in this file defines the different top level routes. Navigation - * within each route is handled using state and Back Handlers. - */ -@Composable -fun NiaNavGraph( - windowSizeClass: WindowSizeClass, - modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController(), - startDestination: String = NiaDestinations.FOR_YOU_ROUTE -) { - NavHost( - navController = navController, - startDestination = startDestination, - modifier = modifier, - ) { - composable(NiaDestinations.FOR_YOU_ROUTE) { - ForYouRoute(windowSizeClass) - } - composable(NiaDestinations.EPISODES_ROUTE) { - Text("EPISODES") - } - composable(NiaDestinations.SAVED_ROUTE) { - Text("SAVED") - } - navigation( - startDestination = InterestsDestinations.INTERESTS_DESTINATION, - route = NiaDestinations.INTERESTS_ROUTE - ) { - composable(InterestsDestinations.INTERESTS_DESTINATION) { - InterestsRoute( - navigateToTopic = { navController.navigate("$TOPIC_SCREEN/$it") }, - navigateToAuthor = { navController.navigate("$AUTHOR_SCREEN/$it") }, - ) - } - composable( - InterestsDestinations.TOPIC_ROUTE, - arguments = listOf( - navArgument(TopicDestinationsArgs.TOPIC_ID_ARG) { - type = NavType.StringType - } - ) - ) { - TopicRoute(onBackClick = { navController.popBackStack() }) - } - composable( - AuthorDestinations.AUTHOR_ROUTE, - arguments = listOf( - navArgument(AuthorDestinationsArgs.AUTHOR_ID_ARG) { - type = NavType.StringType - } - ) - ) { - AuthorRoute(onBackClick = { navController.popBackStack() }) - } - } - } - // Reporting the app fully drawn to get accurate TTFD readings for the baseline profile. - // https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD - ReportFullyDrawn(NiaDestinations.FOR_YOU_ROUTE) -} - -/** - * Calling [Activity#reportFullyDrawn] in compose UI. - */ -@Composable -private fun ReportFullyDrawn(destination: String) { - // Holding on to the local view and calling `reportFullyDrawn` in an `onPreDraw` listener. - // Compose currently doesn't offer a way to otherwise report fully drawn, - // so this is a viable approach. - val localView: View = LocalView.current - (localView.context as? Activity)?.run { - localView.doOnPreDraw { - reportFullyDrawn() - } - } -} diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavigation.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavigation.kt deleted file mode 100644 index e5d0c4e1b..000000000 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavigation.kt +++ /dev/null @@ -1,53 +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.ui - -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController - -/** - * Routes for the different 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 Compose, not using the Navigation component. - */ -object NiaDestinations { - const val FOR_YOU_ROUTE = "for_you" - const val EPISODES_ROUTE = "episodes" - const val SAVED_ROUTE = "saved" - const val INTERESTS_ROUTE = "interests" -} - -/** - * Models the navigation actions in the app. - */ -class NiaNavigationActions(private val navController: NavHostController) { - fun navigateToTopLevelDestination(route: String) { - navController.navigate(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 - } - } -} diff --git a/core-navigation/.gitignore b/core-navigation/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core-navigation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/Navigation.kt b/core-navigation/build.gradle.kts similarity index 56% rename from feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/Navigation.kt rename to core-navigation/build.gradle.kts index 52c41d832..c9606b6ce 100644 --- a/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/Navigation.kt +++ b/core-navigation/build.gradle.kts @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package com.google.samples.apps.nowinandroid.feature.author - -import com.google.samples.apps.nowinandroid.feature.author.AuthorDestinationsArgs.AUTHOR_ID_ARG -import com.google.samples.apps.nowinandroid.feature.author.InterestsScreens.AUTHOR_SCREEN - -object AuthorDestinations { - const val AUTHOR_ROUTE = "$AUTHOR_SCREEN/{$AUTHOR_ID_ARG}" +plugins { + id("nowinandroid.android.library") + id("nowinandroid.android.library.jacoco") + kotlin("kapt") + id("dagger.hilt.android.plugin") + alias(libs.plugins.ksp) + id("nowinandroid.spotless") } -object AuthorDestinationsArgs { - const val AUTHOR_ID_ARG = "authorId" -} +dependencies { + api(libs.androidx.hilt.navigation.compose) + api(libs.androidx.navigation.compose) -object InterestsScreens { - const val AUTHOR_SCREEN = "author" -} + implementation(libs.hilt.android) + kapt(libs.hilt.compiler) +} \ No newline at end of file diff --git a/core-navigation/src/main/AndroidManifest.xml b/core-navigation/src/main/AndroidManifest.xml new file mode 100644 index 000000000..915676f9b --- /dev/null +++ b/core-navigation/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigationDestination.kt b/core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigationDestination.kt new file mode 100644 index 000000000..af3303f18 --- /dev/null +++ b/core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigationDestination.kt @@ -0,0 +1,38 @@ +/* + * 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.core.navigation + +/** + * Interface for describing the Now in Android navigation destinations + */ + +interface NiaNavigationDestination { + /** + * Defines a specific route this destination belongs to. + * Route is a String that defines the path to your composable. + * You can think of it as an implicit deep link that leads to a specific destination. + * Each destination should have a unique route. + */ + val route: String + + /** + * Defines a specific destination ID. + * This is needed when using nested graphs via the navigation DLS, to differentiate a specific + * destination's route from the route of the entire nested graph it belongs to. + */ + val destination: String +} diff --git a/feature-author/build.gradle.kts b/feature-author/build.gradle.kts index 172e25654..d212fe597 100644 --- a/feature-author/build.gradle.kts +++ b/feature-author/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(project(":core-ui")) implementation(project(":core-data")) implementation(project(":core-common")) + implementation(project(":core-navigation")) testImplementation(project(":core-testing")) androidTestImplementation(project(":core-testing")) diff --git a/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt b/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt index 656015e5d..61329570c 100644 --- a/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt +++ b/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt @@ -26,6 +26,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.asResult +import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -43,7 +44,7 @@ class AuthorViewModel @Inject constructor( ) : ViewModel() { private val authorId: String = checkNotNull( - savedStateHandle[AuthorDestinationsArgs.AUTHOR_ID_ARG] + savedStateHandle[AuthorDestination.authorIdArg] ) // Observe the followed authors, as they could change over time. diff --git a/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/navigation/AuthorNavigation.kt b/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/navigation/AuthorNavigation.kt new file mode 100644 index 000000000..983ef69cd --- /dev/null +++ b/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/navigation/AuthorNavigation.kt @@ -0,0 +1,45 @@ +/* + * 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.feature.author.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination +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" +} + +fun NavGraphBuilder.authorGraph( + onBackClick: () -> Unit +) { + composable( + route = "${AuthorDestination.route}/{${AuthorDestination.authorIdArg}}", + arguments = listOf( + navArgument(AuthorDestination.authorIdArg) { + type = NavType.StringType + } + ) + ) { + AuthorRoute(onBackClick = onBackClick) + } +} diff --git a/feature-author/src/test/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModelTest.kt b/feature-author/src/test/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModelTest.kt index 2779b6e49..b25dd979b 100644 --- a/feature-author/src/test/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModelTest.kt +++ b/feature-author/src/test/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModelTest.kt @@ -25,7 +25,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Vid import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule -import com.google.samples.apps.nowinandroid.feature.author.AuthorDestinationsArgs.AUTHOR_ID_ARG +import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant @@ -49,7 +49,7 @@ class AuthorViewModelTest { viewModel = AuthorViewModel( savedStateHandle = SavedStateHandle( mapOf( - AUTHOR_ID_ARG to testInputAuthors[0].author.id + AuthorDestination.authorIdArg to testInputAuthors[0].author.id ) ), authorsRepository = authorsRepository, diff --git a/feature-foryou/build.gradle.kts b/feature-foryou/build.gradle.kts index 5fe5db4ab..eb8cea8f1 100644 --- a/feature-foryou/build.gradle.kts +++ b/feature-foryou/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(project(":core-model")) implementation(project(":core-ui")) implementation(project(":core-data")) + implementation(project(":core-navigation")) testImplementation(project(":core-testing")) androidTestImplementation(project(":core-testing")) diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt new file mode 100644 index 000000000..c19b8016d --- /dev/null +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt @@ -0,0 +1,36 @@ +/* + * 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.feature.foryou.navigation + +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination +import com.google.samples.apps.nowinandroid.feature.foryou.ForYouRoute + +object ForYouDestination : NiaNavigationDestination { + override val route = "for_you_route" + override val destination = "for_you_destination" +} + +fun NavGraphBuilder.forYouGraph( + windowSizeClass: WindowSizeClass +) { + composable(route = ForYouDestination.route) { + ForYouRoute(windowSizeClass) + } +} diff --git a/feature-interests/build.gradle.kts b/feature-interests/build.gradle.kts index 409e0fb90..396d6c85e 100644 --- a/feature-interests/build.gradle.kts +++ b/feature-interests/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(project(":core-model")) implementation(project(":core-ui")) implementation(project(":core-data")) + implementation(project(":core-navigation")) testImplementation(project(":core-testing")) androidTestImplementation(project(":core-testing")) diff --git a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt new file mode 100644 index 000000000..69ed7bba9 --- /dev/null +++ b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt @@ -0,0 +1,39 @@ +/* + * 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.feature.interests.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination +import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute + +object InterestsDestination : NiaNavigationDestination { + override val route = "interests_route" + override val destination = "interests_destination" +} + +fun NavGraphBuilder.interestsGraph( + navigateToTopic: (String) -> Unit, + navigateToAuthor: (String) -> Unit +) { + composable(route = InterestsDestination.route) { + InterestsRoute( + navigateToTopic = navigateToTopic, + navigateToAuthor = navigateToAuthor, + ) + } +} diff --git a/feature-topic/build.gradle.kts b/feature-topic/build.gradle.kts index cffa90040..de2c8085b 100644 --- a/feature-topic/build.gradle.kts +++ b/feature-topic/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(project(":core-ui")) implementation(project(":core-data")) implementation(project(":core-common")) + implementation(project(":core-navigation")) testImplementation(project(":core-testing")) androidTestImplementation(project(":core-testing")) diff --git a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/Navigation.kt b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/Navigation.kt deleted file mode 100644 index 2fca9f89f..000000000 --- a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/Navigation.kt +++ /dev/null @@ -1,33 +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.feature.topic - -import com.google.samples.apps.nowinandroid.feature.topic.InterestsScreens.TOPIC_SCREEN -import com.google.samples.apps.nowinandroid.feature.topic.TopicDestinationsArgs.TOPIC_ID_ARG - -object InterestsDestinations { - const val INTERESTS_DESTINATION = "interests_destination" - const val TOPIC_ROUTE = "$TOPIC_SCREEN/{$TOPIC_ID_ARG}" -} - -object TopicDestinationsArgs { - const val TOPIC_ID_ARG = "topicId" -} - -object InterestsScreens { - const val TOPIC_SCREEN = "topic" -} diff --git a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index f96b8d765..9e1bba01b 100644 --- a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -26,6 +26,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.asResult +import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -42,7 +43,7 @@ class TopicViewModel @Inject constructor( newsRepository: NewsRepository ) : ViewModel() { - private val topicId: String = checkNotNull(savedStateHandle[TopicDestinationsArgs.TOPIC_ID_ARG]) + private val topicId: String = checkNotNull(savedStateHandle[TopicDestination.topicIdArg]) // Observe the followed topics, as they could change over time. private val followedTopicIdsStream: Flow>> = diff --git a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt new file mode 100644 index 000000000..39d3b0875 --- /dev/null +++ b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt @@ -0,0 +1,45 @@ +/* + * 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.feature.topic.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination +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" +} + +fun NavGraphBuilder.topicGraph( + onBackClick: () -> Unit +) { + composable( + route = "${TopicDestination.route}/{${TopicDestination.topicIdArg}}", + arguments = listOf( + navArgument(TopicDestination.topicIdArg) { + type = NavType.StringType + } + ) + ) { + TopicRoute(onBackClick = onBackClick) + } +} diff --git a/feature-topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature-topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt index 891fbea0f..46d32ea80 100644 --- a/feature-topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt +++ b/feature-topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt @@ -25,7 +25,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule -import com.google.samples.apps.nowinandroid.feature.topic.TopicDestinationsArgs.TOPIC_ID_ARG +import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant @@ -47,7 +47,8 @@ class TopicViewModelTest { @Before fun setup() { viewModel = TopicViewModel( - savedStateHandle = SavedStateHandle(mapOf(TOPIC_ID_ARG to testInputTopics[0].topic.id)), + savedStateHandle = + SavedStateHandle(mapOf(TopicDestination.topicIdArg to testInputTopics[0].topic.id)), topicsRepository = topicsRepository, newsRepository = newsRepository ) diff --git a/settings.gradle.kts b/settings.gradle.kts index 7464b9b7f..489ed6f8d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -50,6 +50,7 @@ include(":core-database") include(":core-datastore") include(":core-datastore-test") include(":core-model") +include(":core-navigation") include(":core-network") include(":core-ui") include(":core-testing")