Merge "Updates Navigation approach with new guidance" into main

pull/347/head
Manuel Vicente Vivo 2 years ago committed by Gerrit Code Review
commit 0810686f48

@ -85,7 +85,6 @@ dependencies {
implementation(project(":core:ui")) implementation(project(":core:ui"))
implementation(project(":core:designsystem")) implementation(project(":core:designsystem"))
implementation(project(":core:navigation"))
implementation(project(":sync:work")) implementation(project(":sync:work"))
implementation(project(":sync:sync-test")) implementation(project(":sync:sync-test"))
@ -104,6 +103,8 @@ dependencies {
implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.runtime.tracing) implementation(libs.androidx.compose.runtime.tracing)
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.androidx.profileinstaller) implementation(libs.androidx.profileinstaller)

@ -82,9 +82,9 @@ class NiaAppStateTest {
} }
assertEquals(3, state.topLevelDestinations.size) assertEquals(3, state.topLevelDestinations.size)
assertTrue(state.topLevelDestinations[0].destination.contains("for_you")) assertTrue(state.topLevelDestinations[0].name.contains("for_you", true))
assertTrue(state.topLevelDestinations[1].destination.contains("bookmarks")) assertTrue(state.topLevelDestinations[1].name.contains("bookmarks", true))
assertTrue(state.topLevelDestinations[2].destination.contains("interests")) assertTrue(state.topLevelDestinations[2].name.contains("interests", true))
} }
@Test @Test

@ -20,15 +20,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination import com.google.samples.apps.nowinandroid.feature.author.navigation.authorScreen
import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination import com.google.samples.apps.nowinandroid.feature.author.navigation.navigateToAuthor
import com.google.samples.apps.nowinandroid.feature.author.navigation.authorGraph import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksGraph import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen
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.interests.navigation.interestsGraph
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicGraph import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
/** /**
* Top-level navigation graph. Navigation is organized as explained at * Top-level navigation graph. Navigation is organized as explained at
@ -40,32 +39,27 @@ import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicGraph
@Composable @Composable
fun NiaNavHost( fun NiaNavHost(
navController: NavHostController, navController: NavHostController,
onNavigateToDestination: (NiaNavigationDestination, String) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
startDestination: String = ForYouDestination.route startDestination: String = forYouNavigationRoute
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = startDestination, startDestination = startDestination,
modifier = modifier, modifier = modifier,
) { ) {
forYouGraph() forYouScreen()
bookmarksGraph() bookmarksScreen()
interestsGraph( interestsGraph(
navigateToTopic = { navigateToTopic = { topicId ->
onNavigateToDestination( navController.navigateToTopic(topicId)
TopicDestination, TopicDestination.createNavigationRoute(it)
)
}, },
navigateToAuthor = { navigateToAuthor = { authorId ->
onNavigateToDestination( navController.navigateToAuthor(authorId)
AuthorDestination, AuthorDestination.createNavigationRoute(it)
)
}, },
nestedGraphs = { nestedGraphs = {
topicGraph(onBackClick) topicScreen(onBackClick)
authorGraph(onBackClick) authorScreen(onBackClick)
} }
) )
} }

@ -17,17 +17,36 @@
package com.google.samples.apps.nowinandroid.navigation package com.google.samples.apps.nowinandroid.navigation
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
/** /**
* Type for the top level destinations in the application. Each of these destinations * Type for the top level destinations in the application. Each of these destinations
* can contain one or more screens (based on the window size). Navigation from one screen to the * 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. * next within a single destination will be handled directly in composables.
*/ */
data class TopLevelDestination( enum class TopLevelDestination(
override val route: String,
override val destination: String,
val selectedIcon: Icon, val selectedIcon: Icon,
val unselectedIcon: Icon, val unselectedIcon: Icon,
val iconTextId: Int val iconTextId: Int
) : NiaNavigationDestination ) {
FOR_YOU(
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = forYouR.string.for_you
),
BOOKMARKS(
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
iconTextId = bookmarksR.string.saved
),
INTERESTS(
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
iconTextId = interestsR.string.interests
)
}

@ -52,7 +52,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavig
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@ -69,7 +68,9 @@ fun NiaApp(
NiaTheme { NiaTheme {
val background: @Composable (@Composable () -> Unit) -> Unit = val background: @Composable (@Composable () -> Unit) -> Unit =
when (appState.currentDestination?.route) { when (appState.currentDestination?.route) {
ForYouDestination.route -> { content -> NiaGradientBackground(content = content) } TopLevelDestination.FOR_YOU.name -> { content ->
NiaGradientBackground(content = content)
}
else -> { content -> NiaBackground(content = content) } else -> { content -> NiaBackground(content = content) }
} }
@ -85,7 +86,7 @@ fun NiaApp(
if (appState.shouldShowBottomBar) { if (appState.shouldShowBottomBar) {
NiaBottomBar( NiaBottomBar(
destinations = appState.topLevelDestinations, destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigate, onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination currentDestination = appState.currentDestination
) )
} }
@ -103,7 +104,7 @@ fun NiaApp(
if (appState.shouldShowNavRail) { if (appState.shouldShowNavRail) {
NiaNavRail( NiaNavRail(
destinations = appState.topLevelDestinations, destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigate, onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination, currentDestination = appState.currentDestination,
modifier = Modifier.safeDrawingPadding() modifier = Modifier.safeDrawingPadding()
) )
@ -112,7 +113,6 @@ fun NiaApp(
NiaNavHost( NiaNavHost(
navController = appState.navController, navController = appState.navController,
onBackClick = appState::onBackClick, onBackClick = appState::onBackClick,
onNavigateToDestination = appState::navigate,
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumedWindowInsets(padding) .consumedWindowInsets(padding)
@ -132,8 +132,7 @@ private fun NiaNavRail(
) { ) {
NiaNavigationRail(modifier = modifier) { NiaNavigationRail(modifier = modifier) {
destinations.forEach { destination -> destinations.forEach { destination ->
val selected = val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
currentDestination?.hierarchy?.any { it.route == destination.route } == true
NiaNavigationRailItem( NiaNavigationRailItem(
selected = selected, selected = selected,
onClick = { onNavigateToDestination(destination) }, onClick = { onNavigateToDestination(destination) },
@ -168,8 +167,7 @@ private fun NiaBottomBar(
) { ) {
NiaNavigationBar { NiaNavigationBar {
destinations.forEach { destination -> destinations.forEach { destination ->
val selected = val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
currentDestination?.hierarchy?.any { it.route == destination.route } == true
NiaNavigationBarItem( NiaNavigationBarItem(
selected = selected, selected = selected,
onClick = { onNavigateToDestination(destination) }, onClick = { onNavigateToDestination(destination) },
@ -195,3 +193,8 @@ private fun NiaBottomBar(
} }
} }
} }
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
this?.hierarchy?.any {
it.route?.contains(destination.name, true) ?: false
} ?: false

@ -28,19 +28,16 @@ import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.tracing.trace import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksDestination import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterestsGraph
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsDestination
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.BOOKMARKS
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
@Composable @Composable
fun rememberNiaAppState( fun rememberNiaAppState(
@ -72,61 +69,35 @@ class NiaAppState(
/** /**
* Top level destinations to be used in the BottomBar and NavRail * Top level destinations to be used in the BottomBar and NavRail
*/ */
val topLevelDestinations: List<TopLevelDestination> = listOf( val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.values().asList()
TopLevelDestination(
route = ForYouDestination.route,
destination = ForYouDestination.destination,
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = forYouR.string.for_you
),
TopLevelDestination(
route = BookmarksDestination.route,
destination = BookmarksDestination.destination,
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
iconTextId = bookmarksR.string.saved
),
TopLevelDestination(
route = InterestsDestination.route,
destination = InterestsDestination.destination,
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
iconTextId = interestsR.string.interests
)
)
/** /**
* UI logic for navigating to a particular destination in the app. The NavigationOptions to * UI logic for navigating to a top level destination in the app. Top level destinations have
* navigate with are based on the type of destination, which could be a top level destination or * only one copy of the destination of the back stack, and save and restore state whenever you
* just a regular destination. * navigate to and from it.
* *
* Top level destinations have only one copy of the destination of the back stack, and save and * @param topLevelDestination: The destination the app needs to navigate to.
* restore state whenever you navigate to and from it.
* Regular destinations can have multiple copies in the back stack and state isn't saved nor
* restored.
*
* @param destination: The [NiaNavigationDestination] the app needs to navigate to.
* @param route: Optional route to navigate to in case the destination contains arguments.
*/ */
fun navigate(destination: NiaNavigationDestination, route: String? = null) { fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
trace("Navigation: ${destination.route}") { trace("Navigation: ${topLevelDestination.name}") {
if (destination is TopLevelDestination) { val topLevelNavOptions = navOptions {
navController.navigate(route ?: destination.route) { // Pop up to the start destination of the graph to
// Pop up to the start destination of the graph to // avoid building up a large stack of destinations
// avoid building up a large stack of destinations // on the back stack as users select items
// on the back stack as users select items popUpTo(navController.graph.findStartDestination().id) {
popUpTo(navController.graph.findStartDestination().id) { saveState = true
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
} }
} else { // Avoid multiple copies of the same destination when
navController.navigate(route ?: destination.route) // reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
when (topLevelDestination) {
FOR_YOU -> navController.navigateToForYou(topLevelNavOptions)
BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions)
INTERESTS -> navController.navigateToInterestsGraph(topLevelNavOptions)
} }
} }
} }

@ -44,7 +44,6 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
add("implementation", project(":core:designsystem")) add("implementation", project(":core:designsystem"))
add("implementation", project(":core:data")) add("implementation", project(":core:data"))
add("implementation", project(":core:common")) add("implementation", project(":core:common"))
add("implementation", project(":core:navigation"))
add("implementation", project(":core:domain")) add("implementation", project(":core:domain"))
add("testImplementation", project(":core:testing")) add("testImplementation", project(":core:testing"))

@ -13,19 +13,9 @@
* 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.
*/ */
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("nowinandroid.android.library")
id("nowinandroid.android.library.jacoco")
id("nowinandroid.android.hilt")
}
android { package com.google.samples.apps.nowinandroid.core.decoder
namespace = "com.google.samples.apps.nowinandroid.core.navigation"
}
dependencies { interface StringDecoder {
api(libs.androidx.hilt.navigation.compose) fun decodeString(encodedString: String): String
api(libs.androidx.navigation.compose)
} }

@ -0,0 +1,24 @@
/*
* 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.decoder
import android.net.Uri
import javax.inject.Inject
class UriDecoder @Inject constructor() : StringDecoder {
override fun decodeString(encodedString: String): String = Uri.decode(encodedString)
}

@ -0,0 +1,31 @@
/*
* 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.decoder.di
import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import com.google.samples.apps.nowinandroid.core.decoder.UriDecoder
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class StringDecoderModule {
@Binds
abstract fun bindStringDecoder(uriDecoder: UriDecoder): StringDecoder
}

@ -1 +0,0 @@
/build

@ -1,3 +0,0 @@
# :core:navigation module
![Dependency graph](../../docs/images/graphs/dep_graph_core_navigation.png)

@ -1,19 +0,0 @@
<?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">
</manifest>

@ -1,38 +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.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
}

@ -0,0 +1,24 @@
/*
* 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.testing.decoder
import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import javax.inject.Inject
class FakeStringDecoder @Inject constructor() : StringDecoder {
override fun decodeString(encodedString: String): String = encodedString
}

@ -0,0 +1,35 @@
/*
* 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.testing.di
import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import com.google.samples.apps.nowinandroid.core.decoder.di.StringDecoderModule
import com.google.samples.apps.nowinandroid.core.testing.decoder.FakeStringDecoder
import dagger.Binds
import dagger.Module
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [StringDecoderModule::class],
)
abstract class TestStringDecoderModule {
@Binds
abstract fun bindsStringDecoder(fakeStringDecoder: FakeStringDecoder): StringDecoder
}

@ -224,14 +224,6 @@ Using the above modularization strategy, the Now in Android app has the followin
<code>NewsResource</code> <code>NewsResource</code>
</td> </td>
</tr> </tr>
<tr>
<td><code>core:navigation</code>
</td>
<td>Navigation dependencies and shared navigation classes.
</td>
<td><code>NiaNavigationDestination</code>
</td>
</tr>
</table> </table>

@ -65,7 +65,7 @@ import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
fun AuthorRoute( internal fun AuthorRoute(
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: AuthorViewModel = hiltViewModel(), viewModel: AuthorViewModel = hiltViewModel(),

@ -21,13 +21,14 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
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 com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorArgs
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
@ -41,17 +42,16 @@ import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class AuthorViewModel @Inject constructor( class AuthorViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
stringDecoder: StringDecoder,
private val userDataRepository: UserDataRepository, private val userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository, authorsRepository: AuthorsRepository,
getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase
) : ViewModel() { ) : ViewModel() {
private val authorId: String = checkNotNull( private val authorArgs: AuthorArgs = AuthorArgs(savedStateHandle, stringDecoder)
savedStateHandle[AuthorDestination.authorIdArg]
)
val authorUiState: StateFlow<AuthorUiState> = authorUiStateStream( val authorUiState: StateFlow<AuthorUiState> = authorUiStateStream(
authorId = authorId, authorId = authorArgs.authorId,
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
authorsRepository = authorsRepository authorsRepository = authorsRepository
) )
@ -62,7 +62,7 @@ class AuthorViewModel @Inject constructor(
) )
val newsUiState: StateFlow<NewsUiState> = val newsUiState: StateFlow<NewsUiState> =
getSaveableNewsResourcesStream.newsUiStateStream(authorId = authorId) getSaveableNewsResourcesStream.newsUiStateStream(authorId = authorArgs.authorId)
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000), started = SharingStarted.WhileSubscribed(5_000),
@ -71,7 +71,7 @@ class AuthorViewModel @Inject constructor(
fun followAuthorToggle(followed: Boolean) { fun followAuthorToggle(followed: Boolean) {
viewModelScope.launch { viewModelScope.launch {
userDataRepository.toggleFollowedAuthorId(authorId, followed) userDataRepository.toggleFollowedAuthorId(authorArgs.authorId, followed)
} }
} }

@ -17,43 +17,36 @@
package com.google.samples.apps.nowinandroid.feature.author.navigation package com.google.samples.apps.nowinandroid.feature.author.navigation
import android.net.Uri import android.net.Uri
import androidx.navigation.NavBackStackEntry import androidx.annotation.VisibleForTesting
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import com.google.samples.apps.nowinandroid.feature.author.AuthorRoute import com.google.samples.apps.nowinandroid.feature.author.AuthorRoute
object AuthorDestination : NiaNavigationDestination { @VisibleForTesting
const val authorIdArg = "authorId" internal const val authorIdArg = "authorId"
override val route = "author_route/{$authorIdArg}"
override val destination = "author_destination"
/** internal class AuthorArgs(val authorId: String) {
* Creates destination route for an authorId that could include special characters constructor(savedStateHandle: SavedStateHandle, stringDecoder: StringDecoder) :
*/ this(stringDecoder.decodeString(checkNotNull(savedStateHandle[authorIdArg])))
fun createNavigationRoute(authorIdArg: String): String { }
val encodedId = Uri.encode(authorIdArg)
return "author_route/$encodedId"
}
/** fun NavController.navigateToAuthor(authorId: String) {
* Returns the authorId from a [NavBackStackEntry] after an author destination navigation call val encodedString = Uri.encode(authorId)
*/ this.navigate("author_route/$encodedString")
fun fromNavArgs(entry: NavBackStackEntry): String {
val encodedId = entry.arguments?.getString(authorIdArg)!!
return Uri.decode(encodedId)
}
} }
fun NavGraphBuilder.authorGraph( fun NavGraphBuilder.authorScreen(
onBackClick: () -> Unit onBackClick: () -> Unit
) { ) {
composable( composable(
route = AuthorDestination.route, route = "author_route/{$authorIdArg}",
arguments = listOf( arguments = listOf(
navArgument(AuthorDestination.authorIdArg) { type = NavType.StringType } navArgument(authorIdArg) { type = NavType.StringType }
) )
) { ) {
AuthorRoute(onBackClick = onBackClick) AuthorRoute(onBackClick = onBackClick)

@ -22,11 +22,12 @@ import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
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.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.testing.decoder.FakeStringDecoder
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.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination import com.google.samples.apps.nowinandroid.feature.author.navigation.authorIdArg
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -63,9 +64,10 @@ class AuthorViewModelTest {
viewModel = AuthorViewModel( viewModel = AuthorViewModel(
savedStateHandle = SavedStateHandle( savedStateHandle = SavedStateHandle(
mapOf( mapOf(
AuthorDestination.authorIdArg to testInputAuthors[0].author.id authorIdArg to testInputAuthors[0].author.id
) )
), ),
stringDecoder = FakeStringDecoder(),
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
authorsRepository = authorsRepository, authorsRepository = authorsRepository,
getSaveableNewsResourcesStream = getSaveableNewsResourcesStreamUseCase getSaveableNewsResourcesStream = getSaveableNewsResourcesStreamUseCase

@ -54,7 +54,7 @@ import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
fun BookmarksRoute( internal fun BookmarksRoute(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: BookmarksViewModel = hiltViewModel() viewModel: BookmarksViewModel = hiltViewModel()
) { ) {

@ -16,18 +16,20 @@
package com.google.samples.apps.nowinandroid.feature.bookmarks.navigation package com.google.samples.apps.nowinandroid.feature.bookmarks.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute
object BookmarksDestination : NiaNavigationDestination { private const val bookmarksRoute = "bookmarks_route"
override val route = "bookmarks_route"
override val destination = "bookmarks_destination" fun NavController.navigateToBookmarks(navOptions: NavOptions? = null) {
this.navigate(bookmarksRoute, navOptions)
} }
fun NavGraphBuilder.bookmarksGraph() { fun NavGraphBuilder.bookmarksScreen() {
composable(route = BookmarksDestination.route) { composable(route = bookmarksRoute) {
BookmarksRoute() BookmarksRoute()
} }
} }

@ -100,7 +100,7 @@ import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
fun ForYouRoute( internal fun ForYouRoute(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ForYouViewModel = hiltViewModel() viewModel: ForYouViewModel = hiltViewModel()
) { ) {
@ -124,7 +124,7 @@ fun ForYouRoute(
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun ForYouScreen( internal fun ForYouScreen(
isOffline: Boolean, isOffline: Boolean,
isSyncing: Boolean, isSyncing: Boolean,
interestsSelectionState: ForYouInterestsSelectionUiState, interestsSelectionState: ForYouInterestsSelectionUiState,

@ -16,18 +16,20 @@
package com.google.samples.apps.nowinandroid.feature.foryou.navigation package com.google.samples.apps.nowinandroid.feature.foryou.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
import com.google.samples.apps.nowinandroid.feature.foryou.ForYouRoute import com.google.samples.apps.nowinandroid.feature.foryou.ForYouRoute
object ForYouDestination : NiaNavigationDestination { const val forYouNavigationRoute = "for_you_route"
override val route = "for_you_route"
override val destination = "for_you_destination" fun NavController.navigateToForYou(navOptions: NavOptions? = null) {
this.navigate(forYouNavigationRoute, navOptions)
} }
fun NavGraphBuilder.forYouGraph() { fun NavGraphBuilder.forYouScreen() {
composable(route = ForYouDestination.route) { composable(route = forYouNavigationRoute) {
ForYouRoute() ForYouRoute()
} }
} }

@ -45,7 +45,7 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
fun InterestsRoute( internal fun InterestsRoute(
navigateToAuthor: (String) -> Unit, navigateToAuthor: (String) -> Unit,
navigateToTopic: (String) -> Unit, navigateToTopic: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -76,7 +76,7 @@ fun InterestsRoute(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun InterestsScreen( internal fun InterestsScreen(
uiState: InterestsUiState, uiState: InterestsUiState,
tabState: InterestsTabState, tabState: InterestsTabState,
followAuthor: (String, Boolean) -> Unit, followAuthor: (String, Boolean) -> Unit,

@ -16,28 +16,30 @@
package com.google.samples.apps.nowinandroid.feature.interests.navigation package com.google.samples.apps.nowinandroid.feature.interests.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navigation import androidx.navigation.navigation
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination
import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
object InterestsDestination : NiaNavigationDestination { private const val interestsGraphRoutePattern = "interests_graph"
override val route = "interests_route" private const val interestsRoute = "interests_route"
override val destination = "interests_destination"
fun NavController.navigateToInterestsGraph(navOptions: NavOptions? = null) {
this.navigate(interestsGraphRoutePattern, navOptions)
} }
fun NavGraphBuilder.interestsGraph( fun NavGraphBuilder.interestsGraph(
navigateToTopic: (String) -> Unit, navigateToTopic: (String) -> Unit,
navigateToAuthor: (String) -> Unit, navigateToAuthor: (String) -> Unit,
nestedGraphs: NavGraphBuilder.() -> Unit nestedGraphs: NavGraphBuilder.() -> Unit
) { ) {
navigation( navigation(
route = InterestsDestination.route, route = interestsGraphRoutePattern,
startDestination = InterestsDestination.destination startDestination = interestsRoute
) { ) {
composable(route = InterestsDestination.destination) { composable(route = interestsRoute) {
InterestsRoute( InterestsRoute(
navigateToTopic = navigateToTopic, navigateToTopic = navigateToTopic,
navigateToAuthor = navigateToAuthor, navigateToAuthor = navigateToAuthor,

@ -64,7 +64,7 @@ import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
fun TopicRoute( internal fun TopicRoute(
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: TopicViewModel = hiltViewModel(), viewModel: TopicViewModel = hiltViewModel(),

@ -21,13 +21,14 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
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 com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicArgs
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
@ -41,16 +42,17 @@ import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class TopicViewModel @Inject constructor( class TopicViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
stringDecoder: StringDecoder,
private val userDataRepository: UserDataRepository, private val userDataRepository: UserDataRepository,
topicsRepository: TopicsRepository, topicsRepository: TopicsRepository,
// newsRepository: NewsRepository, // newsRepository: NewsRepository,
getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase
) : ViewModel() { ) : ViewModel() {
private val topicId: String = checkNotNull(savedStateHandle[TopicDestination.topicIdArg]) private val topicArgs: TopicArgs = TopicArgs(savedStateHandle, stringDecoder)
val topicUiState: StateFlow<TopicUiState> = topicUiStateStream( val topicUiState: StateFlow<TopicUiState> = topicUiStateStream(
topicId = topicId, topicId = topicArgs.topicId,
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
topicsRepository = topicsRepository topicsRepository = topicsRepository
) )
@ -61,7 +63,7 @@ class TopicViewModel @Inject constructor(
) )
val newUiState: StateFlow<NewsUiState> = newsUiStateStream( val newUiState: StateFlow<NewsUiState> = newsUiStateStream(
topicId = topicId, topicId = topicArgs.topicId,
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
getSaveableNewsResourcesStream = getSaveableNewsResourcesStream getSaveableNewsResourcesStream = getSaveableNewsResourcesStream
) )
@ -73,7 +75,7 @@ class TopicViewModel @Inject constructor(
fun followTopicToggle(followed: Boolean) { fun followTopicToggle(followed: Boolean) {
viewModelScope.launch { viewModelScope.launch {
userDataRepository.toggleFollowedTopicId(topicId, followed) userDataRepository.toggleFollowedTopicId(topicArgs.topicId, followed)
} }
} }

@ -17,43 +17,36 @@
package com.google.samples.apps.nowinandroid.feature.topic.navigation package com.google.samples.apps.nowinandroid.feature.topic.navigation
import android.net.Uri import android.net.Uri
import androidx.navigation.NavBackStackEntry import androidx.annotation.VisibleForTesting
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigationDestination import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute
object TopicDestination : NiaNavigationDestination { @VisibleForTesting
const val topicIdArg = "topicId" internal const val topicIdArg = "topicId"
override val route = "topic_route/{$topicIdArg}"
override val destination = "topic_destination"
/** internal class TopicArgs(val topicId: String) {
* Creates destination route for a topicId that could include special characters constructor(savedStateHandle: SavedStateHandle, stringDecoder: StringDecoder) :
*/ this(stringDecoder.decodeString(checkNotNull(savedStateHandle[topicIdArg])))
fun createNavigationRoute(topicIdArg: String): String { }
val encodedId = Uri.encode(topicIdArg)
return "topic_route/$encodedId"
}
/** fun NavController.navigateToTopic(topicId: String) {
* Returns the topicId from a [NavBackStackEntry] after a topic destination navigation call val encodedId = Uri.encode(topicId)
*/ this.navigate("topic_route/$encodedId")
fun fromNavArgs(entry: NavBackStackEntry): String {
val encodedId = entry.arguments?.getString(topicIdArg)!!
return Uri.decode(encodedId)
}
} }
fun NavGraphBuilder.topicGraph( fun NavGraphBuilder.topicScreen(
onBackClick: () -> Unit onBackClick: () -> Unit
) { ) {
composable( composable(
route = TopicDestination.route, route = "topic_route/{$topicIdArg}",
arguments = listOf( arguments = listOf(
navArgument(TopicDestination.topicIdArg) { type = NavType.StringType } navArgument(topicIdArg) { type = NavType.StringType }
) )
) { ) {
TopicRoute(onBackClick = onBackClick) TopicRoute(onBackClick = onBackClick)

@ -22,11 +22,12 @@ import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
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.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
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.testing.decoder.FakeStringDecoder
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.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicIdArg
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -61,8 +62,8 @@ class TopicViewModelTest {
@Before @Before
fun setup() { fun setup() {
viewModel = TopicViewModel( viewModel = TopicViewModel(
savedStateHandle = savedStateHandle = SavedStateHandle(mapOf(topicIdArg to testInputTopics[0].topic.id)),
SavedStateHandle(mapOf(TopicDestination.topicIdArg to testInputTopics[0].topic.id)), stringDecoder = FakeStringDecoder(),
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
topicsRepository = topicsRepository, topicsRepository = topicsRepository,
getSaveableNewsResourcesStream = getSaveableNewsResourcesStreamUseCase getSaveableNewsResourcesStream = getSaveableNewsResourcesStreamUseCase

@ -43,7 +43,6 @@ include(":core:datastore-test")
include(":core:designsystem") include(":core:designsystem")
include(":core:domain") include(":core:domain")
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