Incorporate code review changes: Move UserNewsResourceRepository to data

module; move UserNewsResource to model module. Implement unread dot for
bookmarked articles. Keep the flows cold in UserNewsResourceRepository.
pull/595/head
James Rose 2 years ago
parent ebfbb5bafd
commit 57c13d84bd

@ -86,11 +86,9 @@ dependencies {
implementation(project(":feature:settings"))
implementation(project(":core:common"))
implementation(project(":core:domain"))
implementation(project(":core:ui"))
implementation(project(":core:designsystem"))
implementation(project(":core:data"))
implementation(project(":core:domain"))
implementation(project(":core:model"))
implementation(project(":core:analytics"))

@ -25,16 +25,14 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.google.accompanist.testharness.TestHarness
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.domain.repository.CompositeUserNewsResourceRepository
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.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -69,7 +67,6 @@ class NavigationUiTest {
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
val userNewsResourceRepository = CompositeUserNewsResourceRepository(
coroutineScope = TestScope(UnconfinedTestDispatcher()),
newsRepository = TestNewsRepository(),
userDataRepository = TestUserDataRepository(),
)

@ -30,6 +30,9 @@ import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.compose.composable
import androidx.navigation.createGraph
import androidx.navigation.testing.TestNavHostController
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
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.util.TestNetworkMonitor
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@ -56,6 +59,9 @@ class NiaAppStateTest {
// Create the test dependencies.
private val networkMonitor = TestNetworkMonitor()
private val userNewsResourceRepository =
CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository())
// Subject under test.
private lateinit var state: NiaAppState
@ -67,10 +73,11 @@ class NiaAppStateTest {
val navController = rememberTestNavController()
state = remember(navController) {
NiaAppState(
windowSizeClass = getCompactWindowClass(),
navController = navController,
networkMonitor = networkMonitor,
coroutineScope = backgroundScope,
windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
}
@ -92,6 +99,7 @@ class NiaAppStateTest {
state = rememberNiaAppState(
windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
}
@ -105,10 +113,11 @@ class NiaAppStateTest {
fun niaAppState_showBottomBar_compact() = runTest {
composeTestRule.setContent {
state = NiaAppState(
windowSizeClass = getCompactWindowClass(),
navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor,
coroutineScope = backgroundScope,
windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
}
@ -120,10 +129,11 @@ class NiaAppStateTest {
fun niaAppState_showNavRail_medium() = runTest {
composeTestRule.setContent {
state = NiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor,
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
}
@ -135,10 +145,11 @@ class NiaAppStateTest {
fun niaAppState_showNavRail_large() = runTest {
composeTestRule.setContent {
state = NiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor,
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
}
@ -150,10 +161,11 @@ class NiaAppStateTest {
fun stateIsOfflineWhenNetworkMonitorIsOffline() = runTest(UnconfinedTestDispatcher()) {
composeTestRule.setContent {
state = NiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor,
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
}

@ -40,9 +40,9 @@ import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.ui.NiaApp

@ -57,6 +57,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
@ -70,7 +71,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVec
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.core.domain.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@ -85,11 +85,12 @@ import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
fun NiaApp(
windowSizeClass: WindowSizeClass,
networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
appState: NiaAppState = rememberNiaAppState(
networkMonitor = networkMonitor,
windowSizeClass = windowSizeClass,
userNewsResourceRepository = userNewsResourceRepository,
),
userNewsResourceRepository: UserNewsResourceRepository,
) {
val shouldShowGradientBackground =
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
@ -133,14 +134,7 @@ fun NiaApp(
snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = {
if (appState.shouldShowBottomBar) {
val forYouNewsResources by userNewsResourceRepository.getUserNewsResourcesForFollowedTopics()
.collectAsStateWithLifecycle(emptyList())
val unreadDestinations =
when {
forYouNewsResources.all { it.isViewed } -> emptySet()
else -> setOf(TopLevelDestination.FOR_YOU)
}
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources.collectAsStateWithLifecycle()
NiaBottomBar(
destinations = appState.topLevelDestinations,
destinationsWithUnreadResources = unreadDestinations,
@ -275,9 +269,16 @@ private fun NiaBottomBar(
}
},
label = { Text(stringResource(destination.iconTextId)) },
modifier = if (hasUnread) {
modifier = if (hasUnread) notificationDot() else Modifier,
)
}
}
}
@Composable
private fun notificationDot(): Modifier {
val tertiaryColor = MaterialTheme.colorScheme.tertiary
Modifier.drawWithContent {
return Modifier.drawWithContent {
drawContent()
drawCircle(
tertiaryColor,
@ -291,12 +292,6 @@ private fun NiaBottomBar(
),
)
}
} else {
Modifier
},
)
}
}
}
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =

@ -33,6 +33,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksRoute
@ -47,6 +48,8 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_Y
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@ -54,12 +57,25 @@ import kotlinx.coroutines.flow.stateIn
fun rememberNiaAppState(
windowSizeClass: WindowSizeClass,
networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
navController: NavHostController = rememberNavController(),
): NiaAppState {
NavigationTrackingSideEffect(navController)
return remember(navController, coroutineScope, windowSizeClass, networkMonitor) {
NiaAppState(navController, coroutineScope, windowSizeClass, networkMonitor)
return remember(
navController,
coroutineScope,
windowSizeClass,
networkMonitor,
userNewsResourceRepository,
) {
NiaAppState(
navController,
coroutineScope,
windowSizeClass,
networkMonitor,
userNewsResourceRepository,
)
}
}
@ -69,6 +85,7 @@ class NiaAppState(
val coroutineScope: CoroutineScope,
val windowSizeClass: WindowSizeClass,
networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
) {
val currentDestination: NavDestination?
@Composable get() = navController
@ -105,6 +122,22 @@ class NiaAppState(
*/
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.values().asList()
/**
* The top level destinations that have unread news resources.
*/
val topLevelDestinationsWithUnreadResources: StateFlow<Set<TopLevelDestination>> =
userNewsResourceRepository.getUserNewsResourcesForFollowedTopics()
.combine(userNewsResourceRepository.getBookmarkedUserNewsResources()) { forYouNewsResources, bookmarkedNewsResources ->
setOfNotNull(
FOR_YOU.takeIf { forYouNewsResources.any { !it.hasBeenViewed } },
BOOKMARKS.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } },
)
}.stateIn(
coroutineScope,
SharingStarted.WhileSubscribed(5_000),
initialValue = emptySet(),
)
/**
* UI logic for navigating to a top level destination in the app. Top level destinations have
* only one copy of the destination of the back stack, and save and restore state whenever you

@ -14,10 +14,10 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.domain.di
package com.google.samples.apps.nowinandroid.core.data.di
import com.google.samples.apps.nowinandroid.core.domain.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn

@ -0,0 +1,69 @@
/*
* Copyright 2023 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.data.repository
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResources
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import javax.inject.Inject
/**
* Implements a [UserNewsResourceRepository] by combining a [NewsRepository] with a
* [UserDataRepository].
*/
class CompositeUserNewsResourceRepository @Inject constructor(
val newsRepository: NewsRepository,
val userDataRepository: UserDataRepository,
) : UserNewsResourceRepository {
/**
* Returns available news resources (joined with user data) matching the given query.
*/
override fun getUserNewsResources(
query: NewsResourceQuery,
): Flow<List<UserNewsResource>> =
newsRepository.getNewsResources(query)
.combine(userDataRepository.userData) { newsResources, userData ->
newsResources.mapToUserNewsResources(userData)
}
/**
* Returns available news resources (joined with user data) for the followed topics.
*/
override fun getUserNewsResourcesForFollowedTopics(): Flow<List<UserNewsResource>> =
userDataRepository.userData.map { it.followedTopics }.distinctUntilChanged()
.flatMapLatest { followedTopics ->
when {
followedTopics.isEmpty() -> flowOf(emptyList())
else -> getUserNewsResources(NewsResourceQuery(filterTopicIds = followedTopics))
}
}
override fun getBookmarkedUserNewsResources(): Flow<List<UserNewsResource>> =
userDataRepository.userData.map { it.bookmarkedNewsResources }.distinctUntilChanged()
.flatMapLatest { bookmarkedNewsResources ->
when {
bookmarkedNewsResources.isEmpty() -> flowOf(emptyList())
else -> getUserNewsResources(NewsResourceQuery(filterNewsIds = bookmarkedNewsResources))
}
}
}

@ -50,8 +50,8 @@ class OfflineFirstUserDataRepository @Inject constructor(
)
}
override suspend fun updateNewsResourceViewed(newsResourceId: String, viewed: Boolean) =
niaPreferencesDataSource.toggleNewsResourceViewed(newsResourceId, viewed)
override suspend fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) =
niaPreferencesDataSource.setNewsResourceViewed(newsResourceId, viewed)
override suspend fun setThemeBrand(themeBrand: ThemeBrand) {
niaPreferencesDataSource.setThemeBrand(themeBrand)

@ -46,7 +46,7 @@ interface UserDataRepository {
/**
* Updates the viewed status for a news resource
*/
suspend fun updateNewsResourceViewed(newsResourceId: String, viewed: Boolean)
suspend fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean)
/**
* Sets the desired theme brand.

@ -14,10 +14,9 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.domain.repository
package com.google.samples.apps.nowinandroid.core.data.repository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import kotlinx.coroutines.flow.Flow
/**
@ -38,4 +37,9 @@ interface UserNewsResourceRepository {
* Returns available news resources for the user's followed topics as a stream.
*/
fun getUserNewsResourcesForFollowedTopics(): Flow<List<UserNewsResource>>
/**
*
*/
fun getBookmarkedUserNewsResources(): Flow<List<UserNewsResource>>
}

@ -47,8 +47,8 @@ class FakeUserDataRepository @Inject constructor(
niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked)
}
override suspend fun updateNewsResourceViewed(newsResourceId: String, viewed: Boolean) =
niaPreferencesDataSource.toggleNewsResourceViewed(newsResourceId, viewed)
override suspend fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) =
niaPreferencesDataSource.setNewsResourceViewed(newsResourceId, viewed)
override suspend fun setThemeBrand(themeBrand: ThemeBrand) {
niaPreferencesDataSource.setThemeBrand(themeBrand)

@ -14,20 +14,18 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.domain
package com.google.samples.apps.nowinandroid.core.data
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources
import com.google.samples.apps.nowinandroid.core.domain.repository.CompositeUserNewsResourceRepository
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.Topic
import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResources
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.emptyUserData
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.Test
@ -39,7 +37,6 @@ class CompositeUserNewsResourceRepositoryTest {
private val userDataRepository = TestUserDataRepository()
private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
coroutineScope = TestScope(UnconfinedTestDispatcher()),
newsRepository = newsRepository,
userDataRepository = userDataRepository,
)
@ -71,7 +68,13 @@ class CompositeUserNewsResourceRepositoryTest {
fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest {
// Obtain a stream of user news resources for the given topic id.
val userNewsResources =
userNewsResourceRepository.getUserNewsResources(NewsResourceQuery(filterTopicIds = setOf(sampleTopic1.id)))
userNewsResourceRepository.getUserNewsResources(
NewsResourceQuery(
filterTopicIds = setOf(
sampleTopic1.id,
),
),
)
// Send test data into the repositories.
newsRepository.sendNewsResources(sampleNewsResources)
@ -107,6 +110,29 @@ class CompositeUserNewsResourceRepositoryTest {
userNewsResources.first(),
)
}
@Test
fun whenFilteredByBookmarkedResources_matchingNewsResourcesAreReturned() = runTest {
// Obtain the bookmarked user news resources flow.
val userNewsResources = userNewsResourceRepository.getBookmarkedUserNewsResources()
// Send some news resources and user data into the data repositories.
newsRepository.sendNewsResources(sampleNewsResources)
// Construct the test user data with bookmarks and followed topics.
val userData = emptyUserData.copy(
bookmarkedNewsResources = setOf(sampleNewsResources[0].id, sampleNewsResources[2].id),
followedTopics = setOf(sampleTopic1.id),
)
userDataRepository.setUserData(userData)
// Check that the correct news resources are returned with their bookmarked state.
assertEquals(
listOf(sampleNewsResources[0], sampleNewsResources[2]).mapToUserNewsResources(userData),
userNewsResources.first(),
)
}
}
private val sampleTopic1 = Topic(

@ -14,16 +14,16 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.domain
package com.google.samples.apps.nowinandroid.core.data
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Article
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import kotlinx.datetime.Clock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue

@ -164,7 +164,7 @@ class OfflineFirstUserDataRepositoryTest {
@Test
fun offlineFirstUserDataRepository_update_viewed_news_resources_delegates_to_nia_preferences() =
runTest {
subject.updateNewsResourceViewed(newsResourceId = "0", viewed = true)
subject.setNewsResourceViewed(newsResourceId = "0", viewed = true)
assertEquals(
setOf("0"),
@ -173,7 +173,7 @@ class OfflineFirstUserDataRepositoryTest {
.first(),
)
subject.updateNewsResourceViewed(newsResourceId = "1", viewed = true)
subject.setNewsResourceViewed(newsResourceId = "1", viewed = true)
assertEquals(
setOf("0", "1"),

@ -138,7 +138,7 @@ class NiaPreferencesDataSource @Inject constructor(
}
}
suspend fun toggleNewsResourceViewed(newsResourceId: String, viewed: Boolean) {
suspend fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) {
userPreferences.updateData {
it.copy {
if (viewed) {

@ -20,7 +20,7 @@ import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepositor
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NAME
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NONE
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import javax.inject.Inject

@ -1,42 +0,0 @@
/*
* Copyright 2023 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.domain.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import javax.inject.Qualifier
import javax.inject.Singleton
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {
@Singleton
@ApplicationScope
@Provides
fun providesCoroutineScope(): CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.Default)
}

@ -1,74 +0,0 @@
/*
* Copyright 2023 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.domain.repository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.di.ApplicationScope
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import javax.inject.Inject
/**
* Implements a [UserNewsResourceRepository] by combining a [NewsRepository] with a
* [UserDataRepository].
*/
class CompositeUserNewsResourceRepository @Inject constructor(
@ApplicationScope private val coroutineScope: CoroutineScope,
val newsRepository: NewsRepository,
val userDataRepository: UserDataRepository,
) : UserNewsResourceRepository {
private val userNewsResources =
newsRepository.getNewsResources().mapToUserNewsResources(userDataRepository.userData)
.shareIn(coroutineScope, started = WhileSubscribed(5000), replay = 1)
override fun getUserNewsResources(
query: NewsResourceQuery,
): Flow<List<UserNewsResource>> =
userNewsResources.map { resources ->
resources.filter { resource ->
query.filterTopicIds?.let { topics -> resource.hasTopic(topics) } ?: true &&
query.filterNewsIds?.contains(resource.id) ?: true
}
}
override fun getUserNewsResourcesForFollowedTopics(): Flow<List<UserNewsResource>> =
userDataRepository.userData.flatMapLatest { getUserNewsResources(NewsResourceQuery(filterTopicIds = it.followedTopics)) }
private fun UserNewsResource.hasTopic(filterTopicIds: Set<String>) =
followableTopics.any { filterTopicIds.contains(it.topic.id) }
}
private fun Flow<List<NewsResource>>.mapToUserNewsResources(
userDataStream: Flow<UserData>,
): Flow<List<UserNewsResource>> =
filterNot { it.isEmpty() }
.combine(userDataStream) { newsResources, userData ->
newsResources.mapToUserNewsResources(userData)
}

@ -17,7 +17,7 @@
package com.google.samples.apps.nowinandroid.core.domain
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NAME
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository

@ -1,5 +1,5 @@
/*
* Copyright 2022 The Android Open Source Project
* Copyright 2023 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.
@ -14,9 +14,7 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.domain.model
import com.google.samples.apps.nowinandroid.core.model.data.Topic
package com.google.samples.apps.nowinandroid.core.model.data
/**
* A [topic] with the additional information for whether or not it is followed.

@ -1,5 +1,5 @@
/*
* Copyright 2022 The Android Open Source Project
* Copyright 2023 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.
@ -14,11 +14,8 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.domain.model
package com.google.samples.apps.nowinandroid.core.model.data
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.datetime.Instant
/**
@ -35,7 +32,7 @@ data class UserNewsResource internal constructor(
val type: NewsResourceType,
val followableTopics: List<FollowableTopic>,
val isSaved: Boolean,
val isViewed: Boolean,
val hasBeenViewed: Boolean,
) {
constructor(newsResource: NewsResource, userData: UserData) : this(
id = newsResource.id,
@ -52,7 +49,7 @@ data class UserNewsResource internal constructor(
)
},
isSaved = userData.bookmarkedNewsResources.contains(newsResource.id),
isViewed = userData.viewedNewsResources.contains(newsResource.id),
hasBeenViewed = userData.viewedNewsResources.contains(newsResource.id),
)
}

@ -16,7 +16,7 @@
package com.google.samples.apps.nowinandroid.core.testing.data
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic
/* ktlint-disable max-line-length */

@ -16,12 +16,14 @@
package com.google.samples.apps.nowinandroid.core.testing.data
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Codelab
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Unknown
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
@ -54,7 +56,7 @@ val userNewsResourcesTestData: List<UserNewsResource> = UserData(
second = 0,
nanosecond = 0,
).toInstant(TimeZone.UTC),
type = NewsResourceType.Codelab,
type = Codelab,
topics = listOf(topicsTestData[2]),
),
userData = userData,
@ -70,7 +72,7 @@ val userNewsResourcesTestData: List<UserNewsResource> = UserData(
url = "https://youtu.be/-fJ6poHQrjM",
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = NewsResourceType.Video,
type = Video,
topics = topicsTestData.take(2),
),
userData = userData,
@ -86,7 +88,7 @@ val userNewsResourcesTestData: List<UserNewsResource> = UserData(
url = "https://youtu.be/ZARz0pjm5YM",
headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = NewsResourceType.Video,
type = Video,
topics = listOf(topicsTestData[2]),
),
userData = userData,
@ -100,7 +102,7 @@ val userNewsResourcesTestData: List<UserNewsResource> = UserData(
url = "https://developer.android.com/jetpack/androidx/versions/all-channel",
headerImageUrl = "",
publishDate = Instant.parse("2022-10-01T00:00:00.000Z"),
type = NewsResourceType.Unknown,
type = Unknown,
topics = listOf(topicsTestData[2]),
),
userData = userData,

@ -73,7 +73,7 @@ class TestUserDataRepository : UserDataRepository {
}
}
override suspend fun updateNewsResourceViewed(newsResourceId: String, viewed: Boolean) {
override suspend fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) {
currentUserData.let { current ->
_userData.tryEmit(
current.copy(

@ -40,7 +40,7 @@ class NewsResourceCardTest {
NewsResourceCardExpanded(
userNewsResource = newsWithKnownResourceType,
isBookmarked = false,
isViewed = false,
hasBeenViewed = false,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},
@ -69,7 +69,7 @@ class NewsResourceCardTest {
NewsResourceCardExpanded(
userNewsResource = newsWithUnknownResourceType,
isBookmarked = false,
isViewed = false,
hasBeenViewed = false,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},
@ -113,7 +113,7 @@ class NewsResourceCardTest {
NewsResourceCardExpanded(
userNewsResource = unreadNews,
isBookmarked = false,
isViewed = false,
hasBeenViewed = false,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},
@ -137,7 +137,7 @@ class NewsResourceCardTest {
NewsResourceCardExpanded(
userNewsResource = readNews,
isBookmarked = false,
isViewed = true,
hasBeenViewed = true,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},

@ -17,7 +17,7 @@
package com.google.samples.apps.nowinandroid.core.ui
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic
/* ktlint-disable max-line-length */

@ -39,7 +39,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
/**
* An extension on [LazyListScope] defining a feed with news resources.
@ -48,7 +48,7 @@ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
fun LazyGridScope.newsFeed(
feedState: NewsFeedUiState,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onNewsResourceViewed: (String) -> Unit,
onTopicClick: (String) -> Unit,
) {
when (feedState) {
@ -71,9 +71,9 @@ fun LazyGridScope.newsFeed(
newsResourceTitle = userNewsResource.title,
)
launchCustomChromeTab(context, resourceUrl, backgroundColor)
onNewsResourcesViewedChanged(userNewsResource.id, true)
onNewsResourceViewed(userNewsResource.id)
},
isViewed = userNewsResource.isViewed,
hasBeenViewed = userNewsResource.hasBeenViewed,
onToggleBookmark = {
onNewsResourcesCheckedChanged(
userNewsResource.id,
@ -125,7 +125,7 @@ private fun NewsFeedLoadingPreview() {
newsFeed(
feedState = NewsFeedUiState.Loading,
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}
@ -144,7 +144,7 @@ private fun NewsFeedContentPreview(
newsFeed(
feedState = NewsFeedUiState.Success(userNewsResources),
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}

@ -61,10 +61,10 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconT
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import kotlinx.datetime.Instant
import kotlinx.datetime.toJavaInstant
import java.time.ZoneId
@ -81,7 +81,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR
fun NewsResourceCardExpanded(
userNewsResource: UserNewsResource,
isBookmarked: Boolean,
isViewed: Boolean,
hasBeenViewed: Boolean,
onToggleBookmark: () -> Unit,
onClick: () -> Unit,
onTopicClick: (String) -> Unit,
@ -119,8 +119,8 @@ fun NewsResourceCardExpanded(
}
Spacer(modifier = Modifier.height(12.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
if (!isViewed) {
Dot(
if (!hasBeenViewed) {
NotificationDot(
color = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(8.dp),
)
@ -196,7 +196,7 @@ fun BookmarkButton(
}
@Composable
fun Dot(
fun NotificationDot(
color: Color,
modifier: Modifier = Modifier,
) {
@ -333,7 +333,7 @@ private fun ExpandedNewsResourcePreview(
NewsResourceCardExpanded(
userNewsResource = userNewsResources[0],
isBookmarked = true,
isViewed = false,
hasBeenViewed = false,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},

@ -24,7 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
/**
* Extension function for displaying a [List] of [NewsResourceCardExpanded] backed by a list of
@ -37,7 +37,7 @@ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
fun LazyListScope.userNewsResourceCardItems(
items: List<UserNewsResource>,
onToggleBookmark: (item: UserNewsResource) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onNewsResourceViewed: (String) -> Unit,
onItemClick: ((item: UserNewsResource) -> Unit)? = null,
onTopicClick: (String) -> Unit,
itemModifier: Modifier = Modifier,
@ -53,7 +53,7 @@ fun LazyListScope.userNewsResourceCardItems(
NewsResourceCardExpanded(
userNewsResource = userNewsResource,
isBookmarked = userNewsResource.isSaved,
isViewed = userNewsResource.isViewed,
hasBeenViewed = userNewsResource.hasBeenViewed,
onToggleBookmark = { onToggleBookmark(userNewsResource) },
onClick = {
analyticsHelper.logNewsResourceOpened(
@ -61,12 +61,10 @@ fun LazyListScope.userNewsResourceCardItems(
newsResourceTitle = userNewsResource.title,
)
when (onItemClick) {
null -> {
launchCustomChromeTab(context, resourceUrl, backgroundColor)
onNewsResourcesViewedChanged(userNewsResource.id, true)
}
null -> launchCustomChromeTab(context, resourceUrl, backgroundColor)
else -> onItemClick(userNewsResource)
}
onNewsResourceViewed(userNewsResource.id)
},
onTopicClick = onTopicClick,
modifier = itemModifier,

@ -17,7 +17,6 @@
package com.google.samples.apps.nowinandroid.core.ui
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
@ -25,6 +24,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Vid
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone

@ -52,7 +52,7 @@ class BookmarksScreenTest {
feedState = NewsFeedUiState.Loading,
removeFromBookmarks = {},
onTopicClick = {},
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
@ -72,7 +72,7 @@ class BookmarksScreenTest {
),
removeFromBookmarks = {},
onTopicClick = {},
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
@ -115,7 +115,7 @@ class BookmarksScreenTest {
removeFromBookmarksCalled = true
},
onTopicClick = {},
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
@ -146,7 +146,7 @@ class BookmarksScreenTest {
feedState = NewsFeedUiState.Success(emptyList()),
removeFromBookmarks = {},
onTopicClick = {},
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}

@ -54,7 +54,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
@ -73,7 +73,7 @@ internal fun BookmarksRoute(
BookmarksScreen(
feedState = feedState,
removeFromBookmarks = viewModel::removeFromSavedResources,
onNewsResourcesViewedChanged = viewModel::updateNewsResourceViewed,
onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) },
onTopicClick = onTopicClick,
modifier = modifier,
)
@ -87,14 +87,14 @@ internal fun BookmarksRoute(
internal fun BookmarksScreen(
feedState: NewsFeedUiState,
removeFromBookmarks: (String) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onNewsResourceViewed: (String) -> Unit,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
when (feedState) {
Loading -> LoadingState(modifier)
is Success -> if (feedState.feed.isNotEmpty()) {
BookmarksGrid(feedState, removeFromBookmarks, onNewsResourcesViewedChanged, onTopicClick, modifier)
BookmarksGrid(feedState, removeFromBookmarks, onNewsResourceViewed, onTopicClick, modifier)
} else {
EmptyState(modifier)
}
@ -117,7 +117,7 @@ private fun LoadingState(modifier: Modifier = Modifier) {
private fun BookmarksGrid(
feedState: NewsFeedUiState,
removeFromBookmarks: (String) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onNewsResourceViewed: (String) -> Unit,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
@ -136,7 +136,7 @@ private fun BookmarksGrid(
newsFeed(
feedState = feedState,
onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) },
onNewsResourcesViewedChanged = onNewsResourcesViewedChanged,
onNewsResourceViewed = onNewsResourceViewed,
onTopicClick = onTopicClick,
)
item(span = { GridItemSpan(maxLineSpan) }) {
@ -202,7 +202,7 @@ private fun BookmarksGridPreview(
BookmarksGrid(
feedState = Success(userNewsResources),
removeFromBookmarks = {},
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}

@ -19,14 +19,13 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
@ -39,9 +38,8 @@ class BookmarksViewModel @Inject constructor(
userNewsResourceRepository: UserNewsResourceRepository,
) : ViewModel() {
val feedUiState: StateFlow<NewsFeedUiState> = userNewsResourceRepository.getUserNewsResources()
.filterNot { it.isEmpty() }
.map { newsResources -> newsResources.filter(UserNewsResource::isSaved) } // Only show bookmarked news resources.
val feedUiState: StateFlow<NewsFeedUiState> =
userNewsResourceRepository.getBookmarkedUserNewsResources()
.map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart { emit(Loading) }
.stateIn(
@ -56,9 +54,9 @@ class BookmarksViewModel @Inject constructor(
}
}
fun updateNewsResourceViewed(newsResourceId: String, isViewed: Boolean) {
fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) {
viewModelScope.launch {
userDataRepository.updateNewsResourceViewed(newsResourceId, isViewed)
userDataRepository.setNewsResourceViewed(newsResourceId, viewed)
}
}
}

@ -16,7 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.bookmarks
import com.google.samples.apps.nowinandroid.core.domain.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
@ -25,7 +25,6 @@ import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
@ -45,7 +44,6 @@ class BookmarksViewModelTest {
private val userDataRepository = TestUserDataRepository()
private val newsRepository = TestNewsRepository()
private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
coroutineScope = TestScope(UnconfinedTestDispatcher()),
newsRepository = newsRepository,
userDataRepository = userDataRepository,
)

@ -56,7 +56,7 @@ class ForYouScreenTest {
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
}
@ -80,7 +80,7 @@ class ForYouScreenTest {
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
}
@ -110,7 +110,7 @@ class ForYouScreenTest {
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
}
@ -155,7 +155,7 @@ class ForYouScreenTest {
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
}
@ -193,7 +193,7 @@ class ForYouScreenTest {
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
}
@ -217,7 +217,7 @@ class ForYouScreenTest {
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
}
@ -242,7 +242,7 @@ class ForYouScreenTest {
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}

@ -81,7 +81,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconT
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
@ -107,7 +107,7 @@ internal fun ForYouRoute(
onTopicClick = onTopicClick,
saveFollowedTopics = viewModel::dismissOnboarding,
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
onNewsResourcesViewedChanged = viewModel::updateNewsResourceViewed,
onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) },
modifier = modifier,
)
}
@ -121,7 +121,7 @@ internal fun ForYouScreen(
onTopicClick: (String) -> Unit,
saveFollowedTopics: () -> Unit,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onNewsResourceViewed: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val isOnboardingLoading = onboardingUiState is OnboardingUiState.Loading
@ -179,7 +179,7 @@ internal fun ForYouScreen(
newsFeed(
feedState = feedState,
onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged,
onNewsResourcesViewedChanged = onNewsResourcesViewedChanged,
onNewsResourceViewed = onNewsResourceViewed,
onTopicClick = onTopicClick,
)
@ -416,7 +416,7 @@ fun ForYouScreenPopulatedFeed(
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}
@ -440,7 +440,7 @@ fun ForYouScreenOfflinePopulatedFeed(
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}
@ -466,7 +466,7 @@ fun ForYouScreenTopicSelection(
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}
@ -485,7 +485,7 @@ fun ForYouScreenLoading() {
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}
@ -509,7 +509,7 @@ fun ForYouScreenPopulatedAndLoading(
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}

@ -18,22 +18,16 @@ package com.google.samples.apps.nowinandroid.feature.foryou
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -58,7 +52,7 @@ class ForYouViewModel @Inject constructor(
)
val feedState: StateFlow<NewsFeedUiState> =
userDataRepository.getFollowedUserNewsResources(userNewsResourceRepository)
userNewsResourceRepository.getUserNewsResourcesForFollowedTopics()
.map(NewsFeedUiState::Success)
.stateIn(
scope = viewModelScope,
@ -95,9 +89,9 @@ class ForYouViewModel @Inject constructor(
}
}
fun updateNewsResourceViewed(newsResourceId: String, isViewed: Boolean) {
fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) {
viewModelScope.launch {
userDataRepository.updateNewsResourceViewed(newsResourceId, isViewed)
userDataRepository.setNewsResourceViewed(newsResourceId, viewed)
}
}
@ -107,47 +101,3 @@ class ForYouViewModel @Inject constructor(
}
}
}
/**
* Obtain a flow of user news resources whose topics match those the user is following.
*
* getUserNewsResources: The `UseCase` used to obtain the flow of user news resources.
*/
private fun UserDataRepository.getFollowedUserNewsResources(
userNewsResourceRepository: UserNewsResourceRepository,
): Flow<List<UserNewsResource>> = userData
// Map the user data into a set of followed topic IDs or null if we should return an empty list.
.map { userData ->
if (userData.shouldShowEmptyFeed()) {
null
} else {
userData.followedTopics
}
}
// Only emit a set of followed topic IDs if it's changed. This avoids calling potentially
// expensive operations (like setting up a new flow) when nothing has changed.
.distinctUntilChanged()
// getUserNewsResources returns a flow, so we have a flow inside a flow. flatMapLatest moves
// the inner flow (the one we want to return) to the outer flow and cancels any previous flows
// created by getUserNewsResources.
.flatMapLatest { followedTopics ->
if (followedTopics == null) {
flowOf(emptyList())
} else {
userNewsResourceRepository.getUserNewsResources(
NewsResourceQuery(filterTopicIds = followedTopics),
)
}
}
/**
* If the user hasn't completed the onboarding and hasn't selected any interests
* show an empty news list to clearly demonstrate that their selections affect the
* news articles they will see.
*
* Note: It should not be possible for the user to get into a state where the onboarding
* is not displayed AND they haven't followed any topics, however, this method is to safeguard
* against that scenario in future.
*/
private fun UserData.shouldShowEmptyFeed() =
!shouldHideOnboarding && followedTopics.isEmpty()

@ -16,7 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
/**
* A sealed hierarchy describing the onboarding state for the for you screen.

@ -16,14 +16,14 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources
import com.google.samples.apps.nowinandroid.core.domain.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
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.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResources
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.TestUserDataRepository
@ -34,7 +34,6 @@ import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncStatusMoni
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@ -58,7 +57,6 @@ class ForYouViewModelTest {
private val topicsRepository = TestTopicsRepository()
private val newsRepository = TestNewsRepository()
private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
coroutineScope = TestScope(UnconfinedTestDispatcher()),
newsRepository = newsRepository,
userDataRepository = userDataRepository,
)

@ -29,7 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.FollowableTopicPreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent

@ -21,7 +21,7 @@ import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow

@ -27,7 +27,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
@Composable
fun TopicsTabContent(

@ -17,7 +17,7 @@
package com.google.samples.apps.nowinandroid.interests
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository

@ -59,7 +59,7 @@ class TopicScreenTest {
onFollowClick = {},
onTopicClick = {},
onBookmarkChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
@ -79,7 +79,7 @@ class TopicScreenTest {
onFollowClick = {},
onTopicClick = {},
onBookmarkChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
@ -104,7 +104,7 @@ class TopicScreenTest {
onFollowClick = {},
onTopicClick = {},
onBookmarkChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}
@ -127,7 +127,7 @@ class TopicScreenTest {
onFollowClick = {},
onTopicClick = {},
onBookmarkChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
)
}

@ -51,8 +51,8 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilte
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
@ -79,7 +79,7 @@ internal fun TopicRoute(
onBackClick = onBackClick,
onFollowClick = viewModel::followTopicToggle,
onBookmarkChanged = viewModel::bookmarkNews,
onNewsResourcesViewedChanged = viewModel::updateNewsResourceViewed,
onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) },
onTopicClick = onTopicClick,
)
}
@ -93,7 +93,7 @@ internal fun TopicScreen(
onFollowClick: (Boolean) -> Unit,
onTopicClick: (String) -> Unit,
onBookmarkChanged: (String, Boolean) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onNewsResourceViewed: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val state = rememberLazyListState()
@ -129,7 +129,7 @@ internal fun TopicScreen(
news = newsUiState,
imageUrl = topicUiState.followableTopic.topic.imageUrl,
onBookmarkChanged = onBookmarkChanged,
onNewsResourcesViewedChanged = onNewsResourcesViewedChanged,
onNewsResourceViewed = onNewsResourceViewed,
onTopicClick = onTopicClick,
)
}
@ -146,7 +146,7 @@ private fun LazyListScope.TopicBody(
news: NewsUiState,
imageUrl: String,
onBookmarkChanged: (String, Boolean) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onNewsResourceViewed: (String) -> Unit,
onTopicClick: (String) -> Unit,
) {
// TODO: Show icon if available
@ -154,7 +154,7 @@ private fun LazyListScope.TopicBody(
TopicHeader(name, description, imageUrl)
}
userNewsResourceCards(news, onBookmarkChanged, onNewsResourcesViewedChanged, onTopicClick)
userNewsResourceCards(news, onBookmarkChanged, onNewsResourceViewed, onTopicClick)
}
@Composable
@ -185,7 +185,7 @@ private fun TopicHeader(name: String, description: String, imageUrl: String) {
private fun LazyListScope.userNewsResourceCards(
news: NewsUiState,
onBookmarkChanged: (String, Boolean) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onNewsResourceViewed: (String) -> Unit,
onTopicClick: (String) -> Unit,
) {
when (news) {
@ -193,7 +193,7 @@ private fun LazyListScope.userNewsResourceCards(
userNewsResourceCardItems(
items = news.news,
onToggleBookmark = { onBookmarkChanged(it.id, !it.isSaved) },
onNewsResourcesViewedChanged = onNewsResourcesViewedChanged,
onNewsResourceViewed = onNewsResourceViewed,
onTopicClick = onTopicClick,
itemModifier = Modifier.padding(24.dp),
)
@ -220,7 +220,7 @@ private fun TopicBodyPreview() {
news = NewsUiState.Success(emptyList()),
imageUrl = "",
onBookmarkChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}
@ -278,7 +278,7 @@ fun TopicScreenPopulated(
onBackClick = {},
onFollowClick = {},
onBookmarkChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}
@ -296,7 +296,7 @@ fun TopicScreenLoading() {
onBackClick = {},
onFollowClick = {},
onBookmarkChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
)
}

@ -22,11 +22,11 @@ import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
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.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
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.TopicArgs
@ -87,9 +87,9 @@ class TopicViewModel @Inject constructor(
}
}
fun updateNewsResourceViewed(newsResourceId: String, isViewed: Boolean) {
fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) {
viewModelScope.launch {
userDataRepository.updateNewsResourceViewed(newsResourceId, isViewed)
userDataRepository.setNewsResourceViewed(newsResourceId, viewed)
}
}
}

@ -17,8 +17,8 @@
package com.google.samples.apps.nowinandroid.feature.topic
import androidx.lifecycle.SavedStateHandle
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
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.Topic
@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
@ -55,7 +54,6 @@ class TopicViewModelTest {
private val topicsRepository = TestTopicsRepository()
private val newsRepository = TestNewsRepository()
private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
coroutineScope = TestScope(UnconfinedTestDispatcher()),
newsRepository = newsRepository,
userDataRepository = userDataRepository,
)

Loading…
Cancel
Save