Refactor navigation

Change-Id: I3501446e73976f3872592038501fcef8c8324f74
pull/2/head
Simona Stojanovic 3 years ago committed by Don Turner
parent 52b771a8b5
commit febbd26261

@ -76,6 +76,7 @@ dependencies {
implementation(project(":feature-topic")) implementation(project(":feature-topic"))
implementation(project(":core-ui")) implementation(project(":core-ui"))
implementation(project(":core-navigation"))
implementation(project(":sync")) implementation(project(":sync"))
@ -88,8 +89,6 @@ dependencies {
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.compose.material3.windowSizeClass)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.window.manager) implementation(libs.androidx.window.manager)
implementation(libs.material3) implementation(libs.material3)
implementation(libs.androidx.profileinstaller) implementation(libs.androidx.profileinstaller)

@ -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()
}
}
}

@ -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
)
)

@ -16,7 +16,6 @@
package com.google.samples.apps.nowinandroid.ui package com.google.samples.apps.nowinandroid.ui
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets 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.safeDrawing
import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.windowInsetsPadding 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.material.ripple.LocalRippleTheme
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -56,25 +46,27 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
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.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController 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.ClearRippleTheme
import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground 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.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) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun NiaApp(windowSizeClass: WindowSizeClass) { fun NiaApp(windowSizeClass: WindowSizeClass) {
NiaTheme { NiaTheme {
val navController = rememberNavController() val navController = rememberNavController()
val navigationActions = remember(navController) { val niaTopLevelNavigation = remember(navController) {
NiaNavigationActions(navController) NiaTopLevelNavigation(navController)
} }
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
@ -88,7 +80,7 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
bottomBar = { bottomBar = {
if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) {
NiABottomBar( NiABottomBar(
navigationActions = navigationActions, onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo,
currentDestination = currentDestination currentDestination = currentDestination
) )
} }
@ -105,13 +97,13 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
) { ) {
if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact) { if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact) {
NiANavRail( NiANavRail(
navigationActions = navigationActions, onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo,
currentDestination = currentDestination, currentDestination = currentDestination,
modifier = Modifier.safeDrawingPadding() modifier = Modifier.safeDrawingPadding()
) )
} }
NiaNavGraph( NiaNavHost(
windowSizeClass = windowSizeClass, windowSizeClass = windowSizeClass,
navController = navController, navController = navController,
modifier = Modifier modifier = Modifier
@ -126,7 +118,7 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
@Composable @Composable
private fun NiANavRail( private fun NiANavRail(
navigationActions: NiaNavigationActions, onNavigateToTopLevelDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?, currentDestination: NavDestination?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -136,7 +128,7 @@ private fun NiANavRail(
currentDestination?.hierarchy?.any { it.route == destination.route } == true currentDestination?.hierarchy?.any { it.route == destination.route } == true
NavigationRailItem( NavigationRailItem(
selected = selected, selected = selected,
onClick = { navigationActions.navigateToTopLevelDestination(destination.route) }, onClick = { onNavigateToTopLevelDestination(destination) },
icon = { icon = {
Icon( Icon(
if (selected) destination.selectedIcon else destination.unselectedIcon, if (selected) destination.selectedIcon else destination.unselectedIcon,
@ -151,7 +143,7 @@ private fun NiANavRail(
@Composable @Composable
private fun NiABottomBar( private fun NiABottomBar(
navigationActions: NiaNavigationActions, onNavigateToTopLevelDestination: (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
@ -172,9 +164,7 @@ private fun NiABottomBar(
currentDestination?.hierarchy?.any { it.route == destination.route } == true currentDestination?.hierarchy?.any { it.route == destination.route } == true
NavigationBarItem( NavigationBarItem(
selected = selected, selected = selected,
onClick = { onClick = { onNavigateToTopLevelDestination(destination) },
navigationActions.navigateToTopLevelDestination(destination.route)
},
icon = { icon = {
Icon( Icon(
if (selected) { 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
)

@ -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()
}
}
}

@ -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
}
}
}

@ -0,0 +1 @@
/build

@ -13,20 +13,19 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
plugins {
package com.google.samples.apps.nowinandroid.feature.author id("nowinandroid.android.library")
id("nowinandroid.android.library.jacoco")
import com.google.samples.apps.nowinandroid.feature.author.AuthorDestinationsArgs.AUTHOR_ID_ARG kotlin("kapt")
import com.google.samples.apps.nowinandroid.feature.author.InterestsScreens.AUTHOR_SCREEN id("dagger.hilt.android.plugin")
alias(libs.plugins.ksp)
object AuthorDestinations { id("nowinandroid.spotless")
const val AUTHOR_ROUTE = "$AUTHOR_SCREEN/{$AUTHOR_ID_ARG}"
} }
object AuthorDestinationsArgs { dependencies {
const val AUTHOR_ID_ARG = "authorId" api(libs.androidx.hilt.navigation.compose)
} api(libs.androidx.navigation.compose)
object InterestsScreens { implementation(libs.hilt.android)
const val AUTHOR_SCREEN = "author" kapt(libs.hilt.compiler)
} }

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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
http://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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.samples.apps.nowinandroid.core.navigation">
</manifest>

@ -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
}

@ -33,6 +33,7 @@ dependencies {
implementation(project(":core-ui")) implementation(project(":core-ui"))
implementation(project(":core-data")) implementation(project(":core-data"))
implementation(project(":core-common")) implementation(project(":core-common"))
implementation(project(":core-navigation"))
testImplementation(project(":core-testing")) testImplementation(project(":core-testing"))
androidTestImplementation(project(":core-testing")) androidTestImplementation(project(":core-testing"))

@ -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.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.asResult 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 dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -43,7 +44,7 @@ class AuthorViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private val authorId: String = checkNotNull( private val authorId: String = checkNotNull(
savedStateHandle[AuthorDestinationsArgs.AUTHOR_ID_ARG] savedStateHandle[AuthorDestination.authorIdArg]
) )
// Observe the followed authors, as they could change over time. // Observe the followed authors, as they could change over time.

@ -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)
}
}

@ -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.TestAuthorsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository 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.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.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
@ -49,7 +49,7 @@ class AuthorViewModelTest {
viewModel = AuthorViewModel( viewModel = AuthorViewModel(
savedStateHandle = SavedStateHandle( savedStateHandle = SavedStateHandle(
mapOf( mapOf(
AUTHOR_ID_ARG to testInputAuthors[0].author.id AuthorDestination.authorIdArg to testInputAuthors[0].author.id
) )
), ),
authorsRepository = authorsRepository, authorsRepository = authorsRepository,

@ -33,6 +33,7 @@ dependencies {
implementation(project(":core-model")) implementation(project(":core-model"))
implementation(project(":core-ui")) implementation(project(":core-ui"))
implementation(project(":core-data")) implementation(project(":core-data"))
implementation(project(":core-navigation"))
testImplementation(project(":core-testing")) testImplementation(project(":core-testing"))
androidTestImplementation(project(":core-testing")) androidTestImplementation(project(":core-testing"))

@ -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)
}
}

@ -32,6 +32,7 @@ dependencies {
implementation(project(":core-model")) implementation(project(":core-model"))
implementation(project(":core-ui")) implementation(project(":core-ui"))
implementation(project(":core-data")) implementation(project(":core-data"))
implementation(project(":core-navigation"))
testImplementation(project(":core-testing")) testImplementation(project(":core-testing"))
androidTestImplementation(project(":core-testing")) androidTestImplementation(project(":core-testing"))

@ -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,
)
}
}

@ -33,6 +33,7 @@ dependencies {
implementation(project(":core-ui")) implementation(project(":core-ui"))
implementation(project(":core-data")) implementation(project(":core-data"))
implementation(project(":core-common")) implementation(project(":core-common"))
implementation(project(":core-navigation"))
testImplementation(project(":core-testing")) testImplementation(project(":core-testing"))
androidTestImplementation(project(":core-testing")) androidTestImplementation(project(":core-testing"))

@ -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"
}

@ -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.model.data.Topic
import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.asResult 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 dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -42,7 +43,7 @@ class TopicViewModel @Inject constructor(
newsRepository: NewsRepository newsRepository: NewsRepository
) : ViewModel() { ) : 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. // Observe the followed topics, as they could change over time.
private val followedTopicIdsStream: Flow<Result<Set<String>>> = private val followedTopicIdsStream: Flow<Result<Set<String>>> =

@ -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)
}
}

@ -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.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository 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.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.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
@ -47,7 +47,8 @@ class TopicViewModelTest {
@Before @Before
fun setup() { fun setup() {
viewModel = TopicViewModel( 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, topicsRepository = topicsRepository,
newsRepository = newsRepository newsRepository = newsRepository
) )

@ -50,6 +50,7 @@ include(":core-database")
include(":core-datastore") include(":core-datastore")
include(":core-datastore-test") include(":core-datastore-test")
include(":core-model") include(":core-model")
include(":core-navigation")
include(":core-network") include(":core-network")
include(":core-ui") include(":core-ui")
include(":core-testing") include(":core-testing")

Loading…
Cancel
Save