From b8fc2c9302c16862e2dce6a597d6b7d3fd7434f6 Mon Sep 17 00:00:00 2001 From: James Rose Date: Fri, 13 Jan 2023 15:35:11 -0800 Subject: [PATCH 1/6] Add viewed status for news resources to data layer --- .../OfflineFirstUserDataRepository.kt | 3 ++ .../data/repository/UserDataRepository.kt | 5 +++ .../repository/fake/FakeUserDataRepository.kt | 3 ++ .../OfflineFirstUserDataRepositoryTest.kt | 32 +++++++++++++++++++ .../datastore/NiaPreferencesDataSource.kt | 13 ++++++++ .../nowinandroid/data/user_preferences.proto | 3 ++ .../core/domain/model/UserNewsResource.kt | 2 ++ .../core/domain/UserNewsResourceTest.kt | 1 + .../nowinandroid/core/model/data/UserData.kt | 1 + .../testing/data/UserNewsResourcesTestData.kt | 1 + .../repository/TestUserDataRepository.kt | 16 ++++++++++ ...serNewsResourcePreviewParameterProvider.kt | 1 + 12 files changed, 81 insertions(+) diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt index 334209538..f10046f73 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt @@ -50,6 +50,9 @@ class OfflineFirstUserDataRepository @Inject constructor( ) } + override suspend fun updateNewsResourceViewed(newsResourceId: String, viewed: Boolean) = + niaPreferencesDataSource.toggleNewsResourceViewed(newsResourceId, viewed) + override suspend fun setThemeBrand(themeBrand: ThemeBrand) { niaPreferencesDataSource.setThemeBrand(themeBrand) analyticsHelper.logThemeChanged(themeBrand.name) diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt index ea093852f..2ce84a963 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt @@ -43,6 +43,11 @@ interface UserDataRepository { */ suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) + /** + * Updates the viewed status for a news resource + */ + suspend fun updateNewsResourceViewed(newsResourceId: String, viewed: Boolean) + /** * Sets the desired theme brand. */ diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt index af206e5c7..8b8a1f7f8 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt @@ -47,6 +47,9 @@ class FakeUserDataRepository @Inject constructor( niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked) } + override suspend fun updateNewsResourceViewed(newsResourceId: String, viewed: Boolean) = + niaPreferencesDataSource.toggleNewsResourceViewed(newsResourceId, viewed) + override suspend fun setThemeBrand(themeBrand: ThemeBrand) { niaPreferencesDataSource.setThemeBrand(themeBrand) } diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt index daf1a6564..994ae71b5 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt @@ -66,6 +66,7 @@ class OfflineFirstUserDataRepositoryTest { assertEquals( UserData( bookmarkedNewsResources = emptySet(), + viewedNewsResources = emptySet(), followedTopics = emptySet(), themeBrand = ThemeBrand.DEFAULT, darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, @@ -160,6 +161,37 @@ class OfflineFirstUserDataRepositoryTest { ) } + @Test + fun offlineFirstUserDataRepository_update_viewed_news_resources_delegates_to_nia_preferences() = + runTest { + subject.updateNewsResourceViewed(newsResourceId = "0", viewed = true) + + assertEquals( + setOf("0"), + subject.userData + .map { it.viewedNewsResources } + .first(), + ) + + subject.updateNewsResourceViewed(newsResourceId = "1", viewed = true) + + assertEquals( + setOf("0", "1"), + subject.userData + .map { it.viewedNewsResources } + .first(), + ) + + assertEquals( + niaPreferencesDataSource.userData + .map { it.viewedNewsResources } + .first(), + subject.userData + .map { it.viewedNewsResources } + .first(), + ) + } + @Test fun offlineFirstUserDataRepository_set_theme_brand_delegates_to_nia_preferences() = testScope.runTest { diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt index f5751193a..91f8a3df2 100644 --- a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt @@ -33,6 +33,7 @@ class NiaPreferencesDataSource @Inject constructor( .map { UserData( bookmarkedNewsResources = it.bookmarkedNewsResourceIdsMap.keys, + viewedNewsResources = it.viewedNewsResourceIdsMap.keys, followedTopics = it.followedTopicIdsMap.keys, themeBrand = when (it.themeBrand) { null, @@ -137,6 +138,18 @@ class NiaPreferencesDataSource @Inject constructor( } } + suspend fun toggleNewsResourceViewed(newsResourceId: String, viewed: Boolean) { + userPreferences.updateData { + it.copy { + if (viewed) { + viewedNewsResourceIds.put(newsResourceId, true) + } else { + viewedNewsResourceIds.remove(newsResourceId) + } + } + } + } + suspend fun getChangeListVersions() = userPreferences.data .map { ChangeListVersions( diff --git a/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto b/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto index 5288c04ea..11386613c 100644 --- a/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto +++ b/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto @@ -40,6 +40,7 @@ message UserPreferences { map followed_topic_ids = 13; map followed_author_ids = 14; map bookmarked_news_resource_ids = 15; + map viewed_news_resource_ids = 20; ThemeBrandProto theme_brand = 16; DarkThemeConfigProto dark_theme_config = 17; @@ -47,4 +48,6 @@ message UserPreferences { bool should_hide_onboarding = 18; bool use_dynamic_color = 19; + + // NEXT AVAILABLE ID: 21 } diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt index 4e12ec95b..1d0051918 100644 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt @@ -35,6 +35,7 @@ data class UserNewsResource internal constructor( val type: NewsResourceType, val followableTopics: List, val isSaved: Boolean, + val isViewed: Boolean, ) { constructor(newsResource: NewsResource, userData: UserData) : this( id = newsResource.id, @@ -51,6 +52,7 @@ data class UserNewsResource internal constructor( ) }, isSaved = userData.bookmarkedNewsResources.contains(newsResource.id), + isViewed = userData.viewedNewsResources.contains(newsResource.id), ) } diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt index 8350c5178..7931d3f80 100644 --- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt @@ -68,6 +68,7 @@ class UserNewsResourceTest { val userData = UserData( bookmarkedNewsResources = setOf("N1"), + viewedNewsResources = setOf("N1"), followedTopics = setOf("T1"), themeBrand = DEFAULT, darkThemeConfig = FOLLOW_SYSTEM, diff --git a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt index 638b90d36..6a22e4ff5 100644 --- a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt +++ b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserData.kt @@ -21,6 +21,7 @@ package com.google.samples.apps.nowinandroid.core.model.data */ data class UserData( val bookmarkedNewsResources: Set, + val viewedNewsResources: Set, val followedTopics: Set, val themeBrand: ThemeBrand, val darkThemeConfig: DarkThemeConfig, diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/UserNewsResourcesTestData.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/UserNewsResourcesTestData.kt index 381160006..f4085d11e 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/UserNewsResourcesTestData.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/UserNewsResourcesTestData.kt @@ -30,6 +30,7 @@ import kotlinx.datetime.toInstant /* ktlint-disable max-line-length */ val userNewsResourcesTestData: List = UserData( bookmarkedNewsResources = setOf("1", "4"), + viewedNewsResources = setOf("1", "2", "4"), followedTopics = emptySet(), themeBrand = ThemeBrand.ANDROID, darkThemeConfig = DarkThemeConfig.DARK, diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt index e1b86cd63..1b8483d1a 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.filterNotNull val emptyUserData = UserData( bookmarkedNewsResources = emptySet(), + viewedNewsResources = emptySet(), followedTopics = emptySet(), themeBrand = ThemeBrand.DEFAULT, darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, @@ -72,6 +73,21 @@ class TestUserDataRepository : UserDataRepository { } } + override suspend fun updateNewsResourceViewed(newsResourceId: String, viewed: Boolean) { + currentUserData.let { current -> + _userData.tryEmit( + current.copy( + viewedNewsResources = + if (viewed) { + current.viewedNewsResources + newsResourceId + } else { + current.viewedNewsResources - newsResourceId + }, + ), + ) + } + } + override suspend fun setThemeBrand(themeBrand: ThemeBrand) { currentUserData.let { current -> _userData.tryEmit(current.copy(themeBrand = themeBrand)) diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/UserNewsResourcePreviewParameterProvider.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/UserNewsResourcePreviewParameterProvider.kt index e32aa1a57..8a1c108c6 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/UserNewsResourcePreviewParameterProvider.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/UserNewsResourcePreviewParameterProvider.kt @@ -40,6 +40,7 @@ class UserNewsResourcePreviewParameterProvider : PreviewParameterProvider Date: Fri, 10 Feb 2023 11:02:06 -0800 Subject: [PATCH 2/6] Replace GetUserNewsResourcesUseCase with UserNewsResourceRepository This moves the responsibility for joining the UserData and the NewsResources to UserNewsResourceRepository. This way, the work can be done once and shared with all consumers in a SharedFlow, rather than having each consumer perform the join itself by invoking the UseCase. --- .../core/network/NiaDispatchers.kt | 1 + .../core/network/di/DispatchersModule.kt | 5 ++ .../domain/GetUserNewsResourcesUseCase.kt | 58 --------------- .../core/domain/di/CoroutineScopesModule.kt | 42 +++++++++++ .../di/UserNewsResourceRepositoryModule.kt | 33 +++++++++ .../CompositeUserNewsResourceRepository.kt | 74 +++++++++++++++++++ .../repository/UserNewsResourceRepository.kt | 41 ++++++++++ ...ompositeUserNewsResourceRepositoryTest.kt} | 51 +++++++++---- .../feature/bookmarks/BookmarksViewModel.kt | 6 +- .../bookmarks/BookmarksViewModelTest.kt | 8 +- .../feature/foryou/ForYouViewModel.kt | 14 ++-- .../feature/foryou/ForYouViewModelTest.kt | 8 +- .../feature/topic/TopicViewModel.kt | 14 ++-- .../feature/topic/TopicViewModelTest.kt | 8 +- 14 files changed, 262 insertions(+), 101 deletions(-) delete mode 100644 core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCase.kt create mode 100644 core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/CoroutineScopesModule.kt create mode 100644 core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/UserNewsResourceRepositoryModule.kt create mode 100644 core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/CompositeUserNewsResourceRepository.kt create mode 100644 core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/UserNewsResourceRepository.kt rename core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/{GetUserNewsResourcesUseCaseTest.kt => CompositeUserNewsResourceRepositoryTest.kt} (75%) diff --git a/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/NiaDispatchers.kt b/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/NiaDispatchers.kt index 277b68717..9c21dd69a 100644 --- a/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/NiaDispatchers.kt +++ b/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/NiaDispatchers.kt @@ -24,5 +24,6 @@ import kotlin.annotation.AnnotationRetention.RUNTIME annotation class Dispatcher(val niaDispatcher: NiaDispatchers) enum class NiaDispatchers { + Default, IO, } diff --git a/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/DispatchersModule.kt b/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/DispatchersModule.kt index 1b8409eff..95ec07049 100644 --- a/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/DispatchersModule.kt +++ b/core/common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/DispatchersModule.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.core.network.di import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import dagger.Module import dagger.Provides @@ -31,4 +32,8 @@ object DispatchersModule { @Provides @Dispatcher(IO) fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO + + @Provides + @Dispatcher(Default) + fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default } diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCase.kt deleted file mode 100644 index 393b7b08b..000000000 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCase.kt +++ /dev/null @@ -1,58 +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.domain - -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.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.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNot -import javax.inject.Inject - -/** - * A use case responsible for obtaining news resources with their associated bookmarked (also known - * as "saved") state. - */ -class GetUserNewsResourcesUseCase @Inject constructor( - private val newsRepository: NewsRepository, - private val userDataRepository: UserDataRepository, -) { - /** - * Returns a list of UserNewsResources which match the supplied set of topic ids. - * - * @param query - Summary of query parameters for news resources. - */ - operator fun invoke( - query: NewsResourceQuery = NewsResourceQuery(), - ): Flow> = - newsRepository.getNewsResources( - query = query, - ).mapToUserNewsResources(userDataRepository.userData) -} - -private fun Flow>.mapToUserNewsResources( - userDataStream: Flow, -): Flow> = - filterNot { it.isEmpty() } - .combine(userDataStream) { newsResources, userData -> - newsResources.mapToUserNewsResources(userData) - } diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/CoroutineScopesModule.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/CoroutineScopesModule.kt new file mode 100644 index 000000000..cfd07e565 --- /dev/null +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/CoroutineScopesModule.kt @@ -0,0 +1,42 @@ +/* + * 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) +} diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/UserNewsResourceRepositoryModule.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/UserNewsResourceRepositoryModule.kt new file mode 100644 index 000000000..0dd83a852 --- /dev/null +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/UserNewsResourceRepositoryModule.kt @@ -0,0 +1,33 @@ +/* + * 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 com.google.samples.apps.nowinandroid.core.domain.repository.CompositeUserNewsResourceRepository +import com.google.samples.apps.nowinandroid.core.domain.repository.UserNewsResourceRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface UserNewsResourceRepositoryModule { + @Binds + fun bindsUserNewsResourceRepository( + userDataRepository: CompositeUserNewsResourceRepository, + ): UserNewsResourceRepository +} diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/CompositeUserNewsResourceRepository.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/CompositeUserNewsResourceRepository.kt new file mode 100644 index 000000000..43c2ddf8f --- /dev/null +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/CompositeUserNewsResourceRepository.kt @@ -0,0 +1,74 @@ +/* + * 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> = + userNewsResources.map { resources -> + resources.filter { resource -> + query.filterTopicIds?.let { topics -> resource.hasTopic(topics) } ?: true && + query.filterNewsIds?.contains(resource.id) ?: true + } + } + + override fun getUserNewsResourcesForFollowedTopics(): Flow> = + userDataRepository.userData.flatMapLatest { getUserNewsResources(NewsResourceQuery(filterTopicIds = it.followedTopics)) } + + private fun UserNewsResource.hasTopic(filterTopicIds: Set) = + followableTopics.any { filterTopicIds.contains(it.topic.id) } +} + +private fun Flow>.mapToUserNewsResources( + userDataStream: Flow, +): Flow> = + filterNot { it.isEmpty() } + .combine(userDataStream) { newsResources, userData -> + newsResources.mapToUserNewsResources(userData) + } diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/UserNewsResourceRepository.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/UserNewsResourceRepository.kt new file mode 100644 index 000000000..d81a3d1e0 --- /dev/null +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/UserNewsResourceRepository.kt @@ -0,0 +1,41 @@ +/* + * 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.NewsResourceQuery +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource +import kotlinx.coroutines.flow.Flow + +/** + * Data layer implementation for [UserNewsResource] + */ +interface UserNewsResourceRepository { + /** + * Returns available news resources as a stream. + */ + fun getUserNewsResources( + query: NewsResourceQuery = NewsResourceQuery( + filterTopicIds = null, + filterNewsIds = null, + ), + ): Flow> + + /** + * Returns available news resources for the user's followed topics as a stream. + */ + fun getUserNewsResourcesForFollowedTopics(): Flow> +} diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/CompositeUserNewsResourceRepositoryTest.kt similarity index 75% rename from core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCaseTest.kt rename to core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/CompositeUserNewsResourceRepositoryTest.kt index 0ff863d7c..9462cf89e 100644 --- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCaseTest.kt +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/CompositeUserNewsResourceRepositoryTest.kt @@ -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. @@ -18,34 +18,36 @@ package com.google.samples.apps.nowinandroid.core.domain 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.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData -import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule 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.Rule import org.junit.Test import kotlin.test.assertEquals -class GetUserNewsResourcesUseCaseTest { - - @get:Rule - val mainDispatcherRule = MainDispatcherRule() +class CompositeUserNewsResourceRepositoryTest { private val newsRepository = TestNewsRepository() private val userDataRepository = TestUserDataRepository() - val useCase = GetUserNewsResourcesUseCase(newsRepository, userDataRepository) + private val userNewsResourceRepository = CompositeUserNewsResourceRepository( + coroutineScope = TestScope(UnconfinedTestDispatcher()), + newsRepository = newsRepository, + userDataRepository = userDataRepository, + ) @Test fun whenNoFilters_allNewsResourcesAreReturned() = runTest { - // Obtain the user news resources stream. - val userNewsResources = useCase() + // Obtain the user news resources flow. + val userNewsResources = userNewsResourceRepository.getUserNewsResources() // Send some news resources and user data into the data repositories. newsRepository.sendNewsResources(sampleNewsResources) @@ -68,11 +70,8 @@ class GetUserNewsResourcesUseCaseTest { @Test fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest { // Obtain a stream of user news resources for the given topic id. - val userNewsResources = useCase( - NewsResourceQuery( - filterTopicIds = setOf(sampleTopic1.id), - ), - ) + val userNewsResources = + userNewsResourceRepository.getUserNewsResources(NewsResourceQuery(filterTopicIds = setOf(sampleTopic1.id))) // Send test data into the repositories. newsRepository.sendNewsResources(sampleNewsResources) @@ -86,6 +85,28 @@ class GetUserNewsResourcesUseCaseTest { userNewsResources.first(), ) } + + @Test + fun whenFilteredByFollowedTopics_matchingNewsResourcesAreReturned() = runTest { + // Obtain a stream of user news resources for the given topic id. + val userNewsResources = + userNewsResourceRepository.getUserNewsResourcesForFollowedTopics() + + // Send test data into the repositories. + val userData = emptyUserData.copy( + followedTopics = setOf(sampleTopic1.id), + ) + newsRepository.sendNewsResources(sampleNewsResources) + userDataRepository.setUserData(userData) + + // Check that only news resources with the given topic id are returned. + assertEquals( + sampleNewsResources + .filter { it.topics.contains(sampleTopic1) } + .mapToUserNewsResources(userData), + userNewsResources.first(), + ) + } } private val sampleTopic1 = Topic( diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index fe631c287..91d9355ae 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -19,8 +19,8 @@ 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.GetUserNewsResourcesUseCase 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.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import dagger.hilt.android.lifecycle.HiltViewModel @@ -36,10 +36,10 @@ import javax.inject.Inject @HiltViewModel class BookmarksViewModel @Inject constructor( private val userDataRepository: UserDataRepository, - getSaveableNewsResources: GetUserNewsResourcesUseCase, + userNewsResourceRepository: UserNewsResourceRepository, ) : ViewModel() { - val feedUiState: StateFlow = getSaveableNewsResources() + val feedUiState: StateFlow = userNewsResourceRepository.getUserNewsResources() .filterNot { it.isEmpty() } .map { newsResources -> newsResources.filter(UserNewsResource::isSaved) } // Only show bookmarked news resources. .map, NewsFeedUiState>(NewsFeedUiState::Success) diff --git a/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt b/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt index ae4445197..d97f71095 100644 --- a/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt +++ b/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt @@ -16,7 +16,7 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks -import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase +import com.google.samples.apps.nowinandroid.core.domain.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,6 +25,7 @@ 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 @@ -43,7 +44,8 @@ class BookmarksViewModelTest { private val userDataRepository = TestUserDataRepository() private val newsRepository = TestNewsRepository() - private val getUserNewsResourcesUseCase = GetUserNewsResourcesUseCase( + private val userNewsResourceRepository = CompositeUserNewsResourceRepository( + coroutineScope = TestScope(UnconfinedTestDispatcher()), newsRepository = newsRepository, userDataRepository = userDataRepository, ) @@ -53,7 +55,7 @@ class BookmarksViewModelTest { fun setup() { viewModel = BookmarksViewModel( userDataRepository = userDataRepository, - getSaveableNewsResources = getUserNewsResourcesUseCase, + userNewsResourceRepository = userNewsResourceRepository, ) } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index 085593932..cece3a6c3 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -22,8 +22,8 @@ import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQue import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository 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.GetUserNewsResourcesUseCase 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 @@ -43,7 +43,7 @@ import javax.inject.Inject class ForYouViewModel @Inject constructor( syncStatusMonitor: SyncStatusMonitor, private val userDataRepository: UserDataRepository, - getUserNewsResources: GetUserNewsResourcesUseCase, + userNewsResourceRepository: UserNewsResourceRepository, getFollowableTopics: GetFollowableTopicsUseCase, ) : ViewModel() { @@ -58,7 +58,7 @@ class ForYouViewModel @Inject constructor( ) val feedState: StateFlow = - userDataRepository.getFollowedUserNewsResources(getUserNewsResources) + userDataRepository.getFollowedUserNewsResources(userNewsResourceRepository) .map(NewsFeedUiState::Success) .stateIn( scope = viewModelScope, @@ -108,7 +108,7 @@ class ForYouViewModel @Inject constructor( * getUserNewsResources: The `UseCase` used to obtain the flow of user news resources. */ private fun UserDataRepository.getFollowedUserNewsResources( - getUserNewsResources: GetUserNewsResourcesUseCase, + userNewsResourceRepository: UserNewsResourceRepository, ): Flow> = userData // Map the user data into a set of followed topic IDs or null if we should return an empty list. .map { userData -> @@ -128,10 +128,8 @@ private fun UserDataRepository.getFollowedUserNewsResources( if (followedTopics == null) { flowOf(emptyList()) } else { - getUserNewsResources( - NewsResourceQuery( - filterTopicIds = followedTopics, - ), + userNewsResourceRepository.getUserNewsResources( + NewsResourceQuery(filterTopicIds = followedTopics), ) } } diff --git a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt index 9e51758f0..9bac2549c 100644 --- a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt @@ -17,10 +17,10 @@ package com.google.samples.apps.nowinandroid.feature.foryou import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase -import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase 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.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.Topic @@ -34,6 +34,7 @@ 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 @@ -56,7 +57,8 @@ class ForYouViewModelTest { private val userDataRepository = TestUserDataRepository() private val topicsRepository = TestTopicsRepository() private val newsRepository = TestNewsRepository() - private val getUserNewsResourcesUseCase = GetUserNewsResourcesUseCase( + private val userNewsResourceRepository = CompositeUserNewsResourceRepository( + coroutineScope = TestScope(UnconfinedTestDispatcher()), newsRepository = newsRepository, userDataRepository = userDataRepository, ) @@ -72,7 +74,7 @@ class ForYouViewModelTest { viewModel = ForYouViewModel( syncStatusMonitor = syncStatusMonitor, userDataRepository = userDataRepository, - getUserNewsResources = getUserNewsResourcesUseCase, + userNewsResourceRepository = userNewsResourceRepository, getFollowableTopics = getFollowableTopicsUseCase, ) } diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index fcabff16b..bb03f9ae6 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -23,9 +23,9 @@ import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQue 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.decoder.StringDecoder -import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase 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.Topic import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.asResult @@ -46,7 +46,7 @@ class TopicViewModel @Inject constructor( stringDecoder: StringDecoder, private val userDataRepository: UserDataRepository, topicsRepository: TopicsRepository, - getSaveableNewsResources: GetUserNewsResourcesUseCase, + userNewsResourceRepository: UserNewsResourceRepository, ) : ViewModel() { private val topicArgs: TopicArgs = TopicArgs(savedStateHandle, stringDecoder) @@ -67,7 +67,7 @@ class TopicViewModel @Inject constructor( val newUiState: StateFlow = newsUiState( topicId = topicArgs.topicId, userDataRepository = userDataRepository, - getSaveableNewsResources = getSaveableNewsResources, + userNewsResourceRepository = userNewsResourceRepository, ) .stateIn( scope = viewModelScope, @@ -135,14 +135,12 @@ private fun topicUiState( private fun newsUiState( topicId: String, - getSaveableNewsResources: GetUserNewsResourcesUseCase, + userNewsResourceRepository: UserNewsResourceRepository, userDataRepository: UserDataRepository, ): Flow { // Observe news - val newsStream: Flow> = getSaveableNewsResources( - NewsResourceQuery( - filterTopicIds = setOf(element = topicId), - ), + val newsStream: Flow> = userNewsResourceRepository.getUserNewsResources( + NewsResourceQuery(filterTopicIds = setOf(element = topicId)), ) // Observe bookmarks diff --git a/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt index dfed60385..3580a960b 100644 --- a/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt +++ b/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt @@ -17,8 +17,8 @@ package com.google.samples.apps.nowinandroid.feature.topic import androidx.lifecycle.SavedStateHandle -import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase 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.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,6 +32,7 @@ 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 @@ -53,7 +54,8 @@ class TopicViewModelTest { private val userDataRepository = TestUserDataRepository() private val topicsRepository = TestTopicsRepository() private val newsRepository = TestNewsRepository() - private val getUserNewsResourcesUseCase = GetUserNewsResourcesUseCase( + private val userNewsResourceRepository = CompositeUserNewsResourceRepository( + coroutineScope = TestScope(UnconfinedTestDispatcher()), newsRepository = newsRepository, userDataRepository = userDataRepository, ) @@ -66,7 +68,7 @@ class TopicViewModelTest { stringDecoder = FakeStringDecoder(), userDataRepository = userDataRepository, topicsRepository = topicsRepository, - getSaveableNewsResources = getUserNewsResourcesUseCase, + userNewsResourceRepository = userNewsResourceRepository, ) } From b1286ca1888502e0f49fc8da83947ebcf537d384 Mon Sep 17 00:00:00 2001 From: James Rose Date: Fri, 20 Jan 2023 13:57:39 -0800 Subject: [PATCH 3/6] Display unread state on the news feed and bottom nav bar When a news resource is unread, display a dot on its card in the news feed. When the For You section has unread resources, display a dot on its icon in the navigation bar. Update the read status when a resource is opened. --- app/build.gradle.kts | 2 + .../apps/nowinandroid/ui/NavigationUiTest.kt | 20 ++++++++ .../samples/apps/nowinandroid/MainActivity.kt | 5 ++ .../samples/apps/nowinandroid/ui/NiaApp.kt | 37 ++++++++++++++ .../core/ui/NewsResourceCardTest.kt | 51 +++++++++++++++++++ .../apps/nowinandroid/core/ui/NewsFeed.kt | 5 ++ .../nowinandroid/core/ui/NewsResourceCard.kt | 35 ++++++++++++- .../core/ui/NewsResourceCardList.kt | 7 ++- core/ui/src/main/res/values/strings.xml | 2 + .../feature/bookmarks/BookmarksScreenTest.kt | 4 ++ .../feature/bookmarks/BookmarksScreen.kt | 7 ++- .../feature/bookmarks/BookmarksViewModel.kt | 6 +++ .../feature/foryou/ForYouScreenTest.kt | 7 +++ .../feature/foryou/ForYouScreen.kt | 8 +++ .../feature/foryou/ForYouViewModel.kt | 6 +++ .../feature/topic/TopicScreenTest.kt | 4 ++ .../nowinandroid/feature/topic/TopicScreen.kt | 11 +++- .../feature/topic/TopicViewModel.kt | 6 +++ 18 files changed, 219 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 81c128b91..8197ad57b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -86,9 +86,11 @@ 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")) diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt index c498c03dd..a0a737237 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt @@ -26,10 +26,15 @@ 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.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 @@ -63,6 +68,12 @@ class NavigationUiTest { @get:Rule(order = 2) val composeTestRule = createAndroidComposeRule() + val userNewsResourceRepository = CompositeUserNewsResourceRepository( + coroutineScope = TestScope(UnconfinedTestDispatcher()), + newsRepository = TestNewsRepository(), + userDataRepository = TestUserDataRepository(), + ) + @Inject lateinit var networkMonitor: NetworkMonitor @@ -81,6 +92,7 @@ class NavigationUiTest { DpSize(maxWidth, maxHeight), ), networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, ) } } @@ -100,6 +112,7 @@ class NavigationUiTest { DpSize(maxWidth, maxHeight), ), networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, ) } } @@ -119,6 +132,7 @@ class NavigationUiTest { DpSize(maxWidth, maxHeight), ), networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, ) } } @@ -138,6 +152,7 @@ class NavigationUiTest { DpSize(maxWidth, maxHeight), ), networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, ) } } @@ -157,6 +172,7 @@ class NavigationUiTest { DpSize(maxWidth, maxHeight), ), networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, ) } } @@ -176,6 +192,7 @@ class NavigationUiTest { DpSize(maxWidth, maxHeight), ), networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, ) } } @@ -195,6 +212,7 @@ class NavigationUiTest { DpSize(maxWidth, maxHeight), ), networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, ) } } @@ -214,6 +232,7 @@ class NavigationUiTest { DpSize(maxWidth, maxHeight), ), networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, ) } } @@ -233,6 +252,7 @@ class NavigationUiTest { DpSize(maxWidth, maxHeight), ), networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, ) } } diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt index 5fc9d0525..200c963b7 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -42,6 +42,7 @@ 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.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 @@ -67,6 +68,9 @@ class MainActivity : ComponentActivity() { @Inject lateinit var analyticsHelper: AnalyticsHelper + @Inject + lateinit var userNewsResourceRepository: UserNewsResourceRepository + val viewModel: MainActivityViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -119,6 +123,7 @@ class MainActivity : ComponentActivity() { NiaApp( networkMonitor = networkMonitor, windowSizeClass = calculateWindowSizeClass(this), + userNewsResourceRepository = userNewsResourceRepository, ) } } diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 9565af2f8..c8648b666 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -44,12 +44,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy @@ -67,6 +70,7 @@ 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,6 +89,7 @@ fun NiaApp( networkMonitor = networkMonitor, windowSizeClass = windowSizeClass, ), + userNewsResourceRepository: UserNewsResourceRepository, ) { val shouldShowGradientBackground = appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU @@ -128,8 +133,17 @@ 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) + } + NiaBottomBar( destinations = appState.topLevelDestinations, + destinationsWithUnreadResources = unreadDestinations, onNavigateToDestination = appState::navigateToTopLevelDestination, currentDestination = appState.currentDestination, modifier = Modifier.testTag("NiaBottomBar"), @@ -211,6 +225,7 @@ private fun NiaNavRail( imageVector = icon.imageVector, contentDescription = null, ) + is DrawableResourceIcon -> Icon( painter = painterResource(id = icon.id), contentDescription = null, @@ -218,6 +233,7 @@ private fun NiaNavRail( } }, label = { Text(stringResource(destination.iconTextId)) }, + ) } } @@ -226,6 +242,7 @@ private fun NiaNavRail( @Composable private fun NiaBottomBar( destinations: List, + destinationsWithUnreadResources: Set, onNavigateToDestination: (TopLevelDestination) -> Unit, currentDestination: NavDestination?, modifier: Modifier = Modifier, @@ -234,6 +251,7 @@ private fun NiaBottomBar( modifier = modifier, ) { destinations.forEach { destination -> + val hasUnread = destinationsWithUnreadResources.contains(destination) val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) NiaNavigationBarItem( selected = selected, @@ -257,6 +275,25 @@ private fun NiaBottomBar( } }, label = { Text(stringResource(destination.iconTextId)) }, + modifier = if (hasUnread) { + val tertiaryColor = MaterialTheme.colorScheme.tertiary + Modifier.drawWithContent { + drawContent() + drawCircle( + tertiaryColor, + radius = 5.dp.toPx(), + // This is based on the dimensions of the NavigationBar's "indicator pill"; + // however, its parameters are private, so we must depend on them implicitly + // (NavigationBarTokens.ActiveIndicatorWidth = 64.dp) + center = center + Offset( + 64.dp.toPx() * .45f, + 32.dp.toPx() * -.45f - 6.dp.toPx(), + ), + ) + } + } else { + Modifier + }, ) } } diff --git a/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt b/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt index 712771422..8e2e8fb4a 100644 --- a/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt +++ b/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt @@ -20,6 +20,7 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.assertContentDescriptionEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData @@ -39,6 +40,7 @@ class NewsResourceCardTest { NewsResourceCardExpanded( userNewsResource = newsWithKnownResourceType, isBookmarked = false, + isViewed = false, onToggleBookmark = {}, onClick = {}, onTopicClick = {}, @@ -67,6 +69,7 @@ class NewsResourceCardTest { NewsResourceCardExpanded( userNewsResource = newsWithUnknownResourceType, isBookmarked = false, + isViewed = false, onToggleBookmark = {}, onClick = {}, onTopicClick = {}, @@ -101,4 +104,52 @@ class NewsResourceCardTest { .assertContentDescriptionEquals(expectedContentDescription) } } + + @Test + fun testUnreadDot_displayedWhenUnread() { + val unreadNews = userNewsResourcesTestData[2] + + composeTestRule.setContent { + NewsResourceCardExpanded( + userNewsResource = unreadNews, + isBookmarked = false, + isViewed = false, + onToggleBookmark = {}, + onClick = {}, + onTopicClick = {}, + ) + } + + composeTestRule + .onNodeWithContentDescription( + composeTestRule.activity.getString( + R.string.unread_resource_dot_content_description, + ), + ) + .assertIsDisplayed() + } + + @Test + fun testUnreadDot_notDisplayedWhenRead() { + val readNews = userNewsResourcesTestData[0] + + composeTestRule.setContent { + NewsResourceCardExpanded( + userNewsResource = readNews, + isBookmarked = false, + isViewed = true, + onToggleBookmark = {}, + onClick = {}, + onTopicClick = {}, + ) + } + + composeTestRule + .onNodeWithContentDescription( + composeTestRule.activity.getString( + R.string.unread_resource_dot_content_description, + ), + ) + .assertDoesNotExist() + } } diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt index 3b0015bab..fb1fb56b7 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt @@ -48,6 +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, onTopicClick: (String) -> Unit, ) { when (feedState) { @@ -70,7 +71,9 @@ fun LazyGridScope.newsFeed( newsResourceTitle = userNewsResource.title, ) launchCustomChromeTab(context, resourceUrl, backgroundColor) + onNewsResourcesViewedChanged(userNewsResource.id, true) }, + isViewed = userNewsResource.isViewed, onToggleBookmark = { onNewsResourcesCheckedChanged( userNewsResource.id, @@ -122,6 +125,7 @@ private fun NewsFeedLoadingPreview() { newsFeed( feedState = NewsFeedUiState.Loading, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } @@ -140,6 +144,7 @@ private fun NewsFeedContentPreview( newsFeed( feedState = NewsFeedUiState.Success(userNewsResources), onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index cffa59436..67a41fece 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -16,6 +16,7 @@ package com.google.samples.apps.nowinandroid.core.ui +import androidx.compose.foundation.Canvas import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -25,6 +26,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -40,7 +42,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode @@ -77,6 +81,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR fun NewsResourceCardExpanded( userNewsResource: UserNewsResource, isBookmarked: Boolean, + isViewed: Boolean, onToggleBookmark: () -> Unit, onClick: () -> Unit, onTopicClick: (String) -> Unit, @@ -113,7 +118,16 @@ fun NewsResourceCardExpanded( BookmarkButton(isBookmarked, onToggleBookmark) } Spacer(modifier = Modifier.height(12.dp)) - NewsResourceMetaData(userNewsResource.publishDate, userNewsResource.type) + Row(verticalAlignment = Alignment.CenterVertically) { + if (!isViewed) { + Dot( + color = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(8.dp), + ) + Spacer(modifier = Modifier.size(6.dp)) + } + NewsResourceMetaData(userNewsResource.publishDate, userNewsResource.type) + } Spacer(modifier = Modifier.height(12.dp)) NewsResourceShortDescription(userNewsResource.content) Spacer(modifier = Modifier.height(12.dp)) @@ -181,6 +195,24 @@ fun BookmarkButton( ) } +@Composable +fun Dot( + color: Color, + modifier: Modifier = Modifier, +) { + val description = stringResource(R.string.unread_resource_dot_content_description) + Canvas( + modifier = modifier + .semantics { contentDescription = description }, + onDraw = { + drawCircle( + color, + radius = size.minDimension / 2, + ) + }, + ) +} + @Composable fun dateFormatted(publishDate: Instant): String { var zoneId by remember { mutableStateOf(ZoneId.systemDefault()) } @@ -301,6 +333,7 @@ private fun ExpandedNewsResourcePreview( NewsResourceCardExpanded( userNewsResource = userNewsResources[0], isBookmarked = true, + isViewed = false, onToggleBookmark = {}, onClick = {}, onTopicClick = {}, diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt index 0f6861fbc..6c971e7a2 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt @@ -37,6 +37,7 @@ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource fun LazyListScope.userNewsResourceCardItems( items: List, onToggleBookmark: (item: UserNewsResource) -> Unit, + onNewsResourcesViewedChanged: (String, Boolean) -> Unit, onItemClick: ((item: UserNewsResource) -> Unit)? = null, onTopicClick: (String) -> Unit, itemModifier: Modifier = Modifier, @@ -52,6 +53,7 @@ fun LazyListScope.userNewsResourceCardItems( NewsResourceCardExpanded( userNewsResource = userNewsResource, isBookmarked = userNewsResource.isSaved, + isViewed = userNewsResource.isViewed, onToggleBookmark = { onToggleBookmark(userNewsResource) }, onClick = { analyticsHelper.logNewsResourceOpened( @@ -59,7 +61,10 @@ fun LazyListScope.userNewsResourceCardItems( newsResourceTitle = userNewsResource.title, ) when (onItemClick) { - null -> launchCustomChromeTab(context, resourceUrl, backgroundColor) + null -> { + launchCustomChromeTab(context, resourceUrl, backgroundColor) + onNewsResourcesViewedChanged(userNewsResource.id, true) + } else -> onItemClick(userNewsResource) } }, diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index bfb1d38de..d21a5ea36 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -19,6 +19,8 @@ Unbookmark Back + Unread + Open Resource Link %1$s • %2$s diff --git a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt index 3662bd47f..c5ddd5c10 100644 --- a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt +++ b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt @@ -52,6 +52,7 @@ class BookmarksScreenTest { feedState = NewsFeedUiState.Loading, removeFromBookmarks = {}, onTopicClick = {}, + onNewsResourcesViewedChanged = { _, _ -> }, ) } @@ -71,6 +72,7 @@ class BookmarksScreenTest { ), removeFromBookmarks = {}, onTopicClick = {}, + onNewsResourcesViewedChanged = { _, _ -> }, ) } @@ -113,6 +115,7 @@ class BookmarksScreenTest { removeFromBookmarksCalled = true }, onTopicClick = {}, + onNewsResourcesViewedChanged = { _, _ -> }, ) } @@ -143,6 +146,7 @@ class BookmarksScreenTest { feedState = NewsFeedUiState.Success(emptyList()), removeFromBookmarks = {}, onTopicClick = {}, + onNewsResourcesViewedChanged = { _, _ -> }, ) } diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index 3e0bb5784..b39f189d1 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -73,6 +73,7 @@ internal fun BookmarksRoute( BookmarksScreen( feedState = feedState, removeFromBookmarks = viewModel::removeFromSavedResources, + onNewsResourcesViewedChanged = viewModel::updateNewsResourceViewed, onTopicClick = onTopicClick, modifier = modifier, ) @@ -86,13 +87,14 @@ internal fun BookmarksRoute( internal fun BookmarksScreen( feedState: NewsFeedUiState, removeFromBookmarks: (String) -> Unit, + onNewsResourcesViewedChanged: (String, Boolean) -> Unit, onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, ) { when (feedState) { Loading -> LoadingState(modifier) is Success -> if (feedState.feed.isNotEmpty()) { - BookmarksGrid(feedState, removeFromBookmarks, onTopicClick, modifier) + BookmarksGrid(feedState, removeFromBookmarks, onNewsResourcesViewedChanged, onTopicClick, modifier) } else { EmptyState(modifier) } @@ -115,6 +117,7 @@ private fun LoadingState(modifier: Modifier = Modifier) { private fun BookmarksGrid( feedState: NewsFeedUiState, removeFromBookmarks: (String) -> Unit, + onNewsResourcesViewedChanged: (String, Boolean) -> Unit, onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -133,6 +136,7 @@ private fun BookmarksGrid( newsFeed( feedState = feedState, onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) }, + onNewsResourcesViewedChanged = onNewsResourcesViewedChanged, onTopicClick = onTopicClick, ) item(span = { GridItemSpan(maxLineSpan) }) { @@ -198,6 +202,7 @@ private fun BookmarksGridPreview( BookmarksGrid( feedState = Success(userNewsResources), removeFromBookmarks = {}, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index 91d9355ae..7d0003aed 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -55,4 +55,10 @@ class BookmarksViewModel @Inject constructor( userDataRepository.updateNewsResourceBookmark(newsResourceId, false) } } + + fun updateNewsResourceViewed(newsResourceId: String, isViewed: Boolean) { + viewModelScope.launch { + userDataRepository.updateNewsResourceViewed(newsResourceId, isViewed) + } + } } diff --git a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt index ab712cbb5..a3566fc31 100644 --- a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt +++ b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt @@ -56,6 +56,7 @@ class ForYouScreenTest { onTopicClick = {}, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } } @@ -79,6 +80,7 @@ class ForYouScreenTest { onTopicClick = {}, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } } @@ -108,6 +110,7 @@ class ForYouScreenTest { onTopicClick = {}, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } } @@ -152,6 +155,7 @@ class ForYouScreenTest { onTopicClick = {}, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } } @@ -189,6 +193,7 @@ class ForYouScreenTest { onTopicClick = {}, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } } @@ -212,6 +217,7 @@ class ForYouScreenTest { onTopicClick = {}, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } } @@ -236,6 +242,7 @@ class ForYouScreenTest { onTopicClick = {}, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index aa4dc5f26..44a323868 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -107,6 +107,7 @@ internal fun ForYouRoute( onTopicClick = onTopicClick, saveFollowedTopics = viewModel::dismissOnboarding, onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved, + onNewsResourcesViewedChanged = viewModel::updateNewsResourceViewed, modifier = modifier, ) } @@ -120,6 +121,7 @@ internal fun ForYouScreen( onTopicClick: (String) -> Unit, saveFollowedTopics: () -> Unit, onNewsResourcesCheckedChanged: (String, Boolean) -> Unit, + onNewsResourcesViewedChanged: (String, Boolean) -> Unit, modifier: Modifier = Modifier, ) { val isOnboardingLoading = onboardingUiState is OnboardingUiState.Loading @@ -177,6 +179,7 @@ internal fun ForYouScreen( newsFeed( feedState = feedState, onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged, + onNewsResourcesViewedChanged = onNewsResourcesViewedChanged, onTopicClick = onTopicClick, ) @@ -413,6 +416,7 @@ fun ForYouScreenPopulatedFeed( onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } @@ -436,6 +440,7 @@ fun ForYouScreenOfflinePopulatedFeed( onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } @@ -461,6 +466,7 @@ fun ForYouScreenTopicSelection( onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } @@ -479,6 +485,7 @@ fun ForYouScreenLoading() { onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } @@ -502,6 +509,7 @@ fun ForYouScreenPopulatedAndLoading( onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, onNewsResourcesCheckedChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index cece3a6c3..363356aef 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -95,6 +95,12 @@ class ForYouViewModel @Inject constructor( } } + fun updateNewsResourceViewed(newsResourceId: String, isViewed: Boolean) { + viewModelScope.launch { + userDataRepository.updateNewsResourceViewed(newsResourceId, isViewed) + } + } + fun dismissOnboarding() { viewModelScope.launch { userDataRepository.setShouldHideOnboarding(true) diff --git a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt index 3a267d7e7..65d923442 100644 --- a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt +++ b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt @@ -59,6 +59,7 @@ class TopicScreenTest { onFollowClick = {}, onTopicClick = {}, onBookmarkChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } @@ -78,6 +79,7 @@ class TopicScreenTest { onFollowClick = {}, onTopicClick = {}, onBookmarkChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } @@ -102,6 +104,7 @@ class TopicScreenTest { onFollowClick = {}, onTopicClick = {}, onBookmarkChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } @@ -124,6 +127,7 @@ class TopicScreenTest { onFollowClick = {}, onTopicClick = {}, onBookmarkChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, ) } diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index da6981010..4fc9faaca 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -79,6 +79,7 @@ internal fun TopicRoute( onBackClick = onBackClick, onFollowClick = viewModel::followTopicToggle, onBookmarkChanged = viewModel::bookmarkNews, + onNewsResourcesViewedChanged = viewModel::updateNewsResourceViewed, onTopicClick = onTopicClick, ) } @@ -92,6 +93,7 @@ internal fun TopicScreen( onFollowClick: (Boolean) -> Unit, onTopicClick: (String) -> Unit, onBookmarkChanged: (String, Boolean) -> Unit, + onNewsResourcesViewedChanged: (String, Boolean) -> Unit, modifier: Modifier = Modifier, ) { val state = rememberLazyListState() @@ -127,6 +129,7 @@ internal fun TopicScreen( news = newsUiState, imageUrl = topicUiState.followableTopic.topic.imageUrl, onBookmarkChanged = onBookmarkChanged, + onNewsResourcesViewedChanged = onNewsResourcesViewedChanged, onTopicClick = onTopicClick, ) } @@ -143,6 +146,7 @@ private fun LazyListScope.TopicBody( news: NewsUiState, imageUrl: String, onBookmarkChanged: (String, Boolean) -> Unit, + onNewsResourcesViewedChanged: (String, Boolean) -> Unit, onTopicClick: (String) -> Unit, ) { // TODO: Show icon if available @@ -150,7 +154,7 @@ private fun LazyListScope.TopicBody( TopicHeader(name, description, imageUrl) } - userNewsResourceCards(news, onBookmarkChanged, onTopicClick) + userNewsResourceCards(news, onBookmarkChanged, onNewsResourcesViewedChanged, onTopicClick) } @Composable @@ -181,6 +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, onTopicClick: (String) -> Unit, ) { when (news) { @@ -188,6 +193,7 @@ private fun LazyListScope.userNewsResourceCards( userNewsResourceCardItems( items = news.news, onToggleBookmark = { onBookmarkChanged(it.id, !it.isSaved) }, + onNewsResourcesViewedChanged = onNewsResourcesViewedChanged, onTopicClick = onTopicClick, itemModifier = Modifier.padding(24.dp), ) @@ -214,6 +220,7 @@ private fun TopicBodyPreview() { news = NewsUiState.Success(emptyList()), imageUrl = "", onBookmarkChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } @@ -271,6 +278,7 @@ fun TopicScreenPopulated( onBackClick = {}, onFollowClick = {}, onBookmarkChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } @@ -288,6 +296,7 @@ fun TopicScreenLoading() { onBackClick = {}, onFollowClick = {}, onBookmarkChanged = { _, _ -> }, + onNewsResourcesViewedChanged = { _, _ -> }, onTopicClick = {}, ) } diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index bb03f9ae6..4dac25983 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -86,6 +86,12 @@ class TopicViewModel @Inject constructor( userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked) } } + + fun updateNewsResourceViewed(newsResourceId: String, isViewed: Boolean) { + viewModelScope.launch { + userDataRepository.updateNewsResourceViewed(newsResourceId, isViewed) + } + } } private fun topicUiState( From 317f47c3cf847de96e80ed1e75aaf156cf676278 Mon Sep 17 00:00:00 2001 From: James Rose Date: Fri, 3 Mar 2023 10:50:34 -0800 Subject: [PATCH 4/6] 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. --- app/build.gradle.kts | 2 - .../apps/nowinandroid/ui/NavigationUiTest.kt | 5 +- .../apps/nowinandroid/ui/NiaAppStateTest.kt | 32 +++++--- .../samples/apps/nowinandroid/MainActivity.kt | 2 +- .../samples/apps/nowinandroid/ui/NiaApp.kt | 53 ++++++------- .../apps/nowinandroid/ui/NiaAppState.kt | 37 +++++++++- .../di/UserNewsResourceRepositoryModule.kt | 6 +- .../CompositeUserNewsResourceRepository.kt | 69 +++++++++++++++++ .../OfflineFirstUserDataRepository.kt | 4 +- .../data/repository/UserDataRepository.kt | 2 +- .../repository/UserNewsResourceRepository.kt | 10 ++- .../repository/fake/FakeUserDataRepository.kt | 4 +- ...CompositeUserNewsResourceRepositoryTest.kt | 40 ++++++++-- .../core/data}/UserNewsResourceTest.kt | 6 +- .../OfflineFirstUserDataRepositoryTest.kt | 4 +- .../datastore/NiaPreferencesDataSource.kt | 2 +- .../core/domain/GetFollowableTopicsUseCase.kt | 2 +- .../core/domain/di/CoroutineScopesModule.kt | 42 ----------- .../CompositeUserNewsResourceRepository.kt | 74 ------------------- .../domain/GetFollowableTopicsUseCaseTest.kt | 2 +- .../core/model/data}/FollowableTopic.kt | 6 +- .../core/model/data}/UserNewsResource.kt | 11 +-- .../testing/data/FollowableTopicTestData.kt | 2 +- .../testing/data/UserNewsResourcesTestData.kt | 14 ++-- .../repository/TestUserDataRepository.kt | 2 +- .../core/ui/NewsResourceCardTest.kt | 8 +- ...FollowableTopicPreviewParameterProvider.kt | 2 +- .../apps/nowinandroid/core/ui/NewsFeed.kt | 12 +-- .../nowinandroid/core/ui/NewsResourceCard.kt | 14 ++-- .../core/ui/NewsResourceCardList.kt | 12 ++- ...serNewsResourcePreviewParameterProvider.kt | 2 +- .../feature/bookmarks/BookmarksScreenTest.kt | 8 +- .../feature/bookmarks/BookmarksScreen.kt | 14 ++-- .../feature/bookmarks/BookmarksViewModel.kt | 28 ++++--- .../bookmarks/BookmarksViewModelTest.kt | 4 +- .../feature/foryou/ForYouScreenTest.kt | 14 ++-- .../feature/foryou/ForYouScreen.kt | 18 ++--- .../feature/foryou/ForYouViewModel.kt | 58 +-------------- .../feature/foryou/OnboardingUiState.kt | 2 +- .../feature/foryou/ForYouViewModelTest.kt | 10 +-- .../feature/interests/InterestsScreen.kt | 2 +- .../feature/interests/InterestsViewModel.kt | 2 +- .../feature/interests/TabContent.kt | 2 +- .../interests/InterestsViewModelTest.kt | 2 +- .../feature/topic/TopicScreenTest.kt | 8 +- .../nowinandroid/feature/topic/TopicScreen.kt | 24 +++--- .../feature/topic/TopicViewModel.kt | 10 +-- .../feature/topic/TopicViewModelTest.kt | 6 +- 48 files changed, 325 insertions(+), 370 deletions(-) rename core/{domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain => data/src/main/java/com/google/samples/apps/nowinandroid/core/data}/di/UserNewsResourceRepositoryModule.kt (79%) create mode 100644 core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt rename core/{domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain => data/src/main/java/com/google/samples/apps/nowinandroid/core/data}/repository/UserNewsResourceRepository.kt (83%) rename core/{domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain => data/src/test/java/com/google/samples/apps/nowinandroid/core/data}/CompositeUserNewsResourceRepositoryTest.kt (81%) rename core/{domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain => data/src/test/java/com/google/samples/apps/nowinandroid/core/data}/UserNewsResourceTest.kt (95%) delete mode 100644 core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/CoroutineScopesModule.kt delete mode 100644 core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/CompositeUserNewsResourceRepository.kt rename core/{domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model => model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data}/FollowableTopic.kt (81%) rename core/{domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model => model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data}/UserNewsResource.kt (81%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8197ad57b..81c128b91 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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")) diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt index a0a737237..cd4b40a50 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt @@ -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() val userNewsResourceRepository = CompositeUserNewsResourceRepository( - coroutineScope = TestScope(UnconfinedTestDispatcher()), newsRepository = TestNewsRepository(), userDataRepository = TestUserDataRepository(), ) diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index 64896a544..2457af900 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -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, ) } diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt index 200c963b7..79d556f73 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -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 diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index c8648b666..780849cf2 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -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,30 +269,31 @@ private fun NiaBottomBar( } }, label = { Text(stringResource(destination.iconTextId)) }, - modifier = if (hasUnread) { - val tertiaryColor = MaterialTheme.colorScheme.tertiary - Modifier.drawWithContent { - drawContent() - drawCircle( - tertiaryColor, - radius = 5.dp.toPx(), - // This is based on the dimensions of the NavigationBar's "indicator pill"; - // however, its parameters are private, so we must depend on them implicitly - // (NavigationBarTokens.ActiveIndicatorWidth = 64.dp) - center = center + Offset( - 64.dp.toPx() * .45f, - 32.dp.toPx() * -.45f - 6.dp.toPx(), - ), - ) - } - } else { - Modifier - }, + modifier = if (hasUnread) notificationDot() else Modifier, ) } } } +@Composable +private fun notificationDot(): Modifier { + val tertiaryColor = MaterialTheme.colorScheme.tertiary + return Modifier.drawWithContent { + drawContent() + drawCircle( + tertiaryColor, + radius = 5.dp.toPx(), + // This is based on the dimensions of the NavigationBar's "indicator pill"; + // however, its parameters are private, so we must depend on them implicitly + // (NavigationBarTokens.ActiveIndicatorWidth = 64.dp) + center = center + Offset( + 64.dp.toPx() * .45f, + 32.dp.toPx() * -.45f - 6.dp.toPx(), + ), + ) + } +} + private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) = this?.hierarchy?.any { it.route?.contains(destination.name, true) ?: false diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt index 7f655af21..e472ee2af 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt @@ -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.values().asList() + /** + * The top level destinations that have unread news resources. + */ + val topLevelDestinationsWithUnreadResources: StateFlow> = + 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 diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/UserNewsResourceRepositoryModule.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/di/UserNewsResourceRepositoryModule.kt similarity index 79% rename from core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/UserNewsResourceRepositoryModule.kt rename to core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/di/UserNewsResourceRepositoryModule.kt index 0dd83a852..1a7a80fff 100644 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/UserNewsResourceRepositoryModule.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/di/UserNewsResourceRepositoryModule.kt @@ -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 diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt new file mode 100644 index 000000000..dc9ad299f --- /dev/null +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt @@ -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> = + 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> = + userDataRepository.userData.map { it.followedTopics }.distinctUntilChanged() + .flatMapLatest { followedTopics -> + when { + followedTopics.isEmpty() -> flowOf(emptyList()) + else -> getUserNewsResources(NewsResourceQuery(filterTopicIds = followedTopics)) + } + } + + override fun getBookmarkedUserNewsResources(): Flow> = + userDataRepository.userData.map { it.bookmarkedNewsResources }.distinctUntilChanged() + .flatMapLatest { bookmarkedNewsResources -> + when { + bookmarkedNewsResources.isEmpty() -> flowOf(emptyList()) + else -> getUserNewsResources(NewsResourceQuery(filterNewsIds = bookmarkedNewsResources)) + } + } +} diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt index f10046f73..2559362ba 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt @@ -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) diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt index 2ce84a963..5e0e7ebfc 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt @@ -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. diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/UserNewsResourceRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt similarity index 83% rename from core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/UserNewsResourceRepository.kt rename to core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt index d81a3d1e0..9f7540da2 100644 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/UserNewsResourceRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt @@ -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> + + /** + * + */ + fun getBookmarkedUserNewsResources(): Flow> } diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt index 8b8a1f7f8..74813389e 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt @@ -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) diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/CompositeUserNewsResourceRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt similarity index 81% rename from core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/CompositeUserNewsResourceRepositoryTest.kt rename to core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt index 9462cf89e..78271b809 100644 --- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/CompositeUserNewsResourceRepositoryTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt @@ -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( diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/UserNewsResourceTest.kt similarity index 95% rename from core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt rename to core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/UserNewsResourceTest.kt index 7931d3f80..004966ec9 100644 --- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/UserNewsResourceTest.kt @@ -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 diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt index 994ae71b5..952f667f7 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt @@ -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"), diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt index 91f8a3df2..33c04b70d 100644 --- a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt @@ -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) { diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt index ccc7e4ee1..c3c045d44 100644 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt @@ -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 diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/CoroutineScopesModule.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/CoroutineScopesModule.kt deleted file mode 100644 index cfd07e565..000000000 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/di/CoroutineScopesModule.kt +++ /dev/null @@ -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) -} diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/CompositeUserNewsResourceRepository.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/CompositeUserNewsResourceRepository.kt deleted file mode 100644 index 43c2ddf8f..000000000 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/CompositeUserNewsResourceRepository.kt +++ /dev/null @@ -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> = - userNewsResources.map { resources -> - resources.filter { resource -> - query.filterTopicIds?.let { topics -> resource.hasTopic(topics) } ?: true && - query.filterNewsIds?.contains(resource.id) ?: true - } - } - - override fun getUserNewsResourcesForFollowedTopics(): Flow> = - userDataRepository.userData.flatMapLatest { getUserNewsResources(NewsResourceQuery(filterTopicIds = it.followedTopics)) } - - private fun UserNewsResource.hasTopic(filterTopicIds: Set) = - followableTopics.any { filterTopicIds.contains(it.topic.id) } -} - -private fun Flow>.mapToUserNewsResources( - userDataStream: Flow, -): Flow> = - filterNot { it.isEmpty() } - .combine(userDataStream) { newsResources, userData -> - newsResources.mapToUserNewsResources(userData) - } diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCaseTest.kt index 8bf63aea4..42a31f858 100644 --- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCaseTest.kt +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCaseTest.kt @@ -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 diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/FollowableTopic.kt similarity index 81% rename from core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt rename to core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/FollowableTopic.kt index 7b59df412..cef319c5f 100644 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt +++ b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/FollowableTopic.kt @@ -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. diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserNewsResource.kt similarity index 81% rename from core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt rename to core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserNewsResource.kt index 1d0051918..251911930 100644 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt +++ b/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/UserNewsResource.kt @@ -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, 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), ) } diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/FollowableTopicTestData.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/FollowableTopicTestData.kt index 40e9327d3..32a0cd127 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/FollowableTopicTestData.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/FollowableTopicTestData.kt @@ -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 */ diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/UserNewsResourcesTestData.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/UserNewsResourcesTestData.kt index f4085d11e..987b48b57 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/UserNewsResourcesTestData.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/data/UserNewsResourcesTestData.kt @@ -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 = 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 = 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 = 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 = 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, diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt index 1b8483d1a..66ac80868 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt @@ -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( diff --git a/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt b/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt index 8e2e8fb4a..a495a6266 100644 --- a/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt +++ b/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt @@ -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 = {}, diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/FollowableTopicPreviewParameterProvider.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/FollowableTopicPreviewParameterProvider.kt index 3c83b973c..0dd9501b4 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/FollowableTopicPreviewParameterProvider.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/FollowableTopicPreviewParameterProvider.kt @@ -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 */ diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt index fb1fb56b7..412266034 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt @@ -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 = {}, ) } diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index 67a41fece..f74fb48ca 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -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 = {}, diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt index 6c971e7a2..884da93b5 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt @@ -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, 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, diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/UserNewsResourcePreviewParameterProvider.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/UserNewsResourcePreviewParameterProvider.kt index 8a1c108c6..3f3f9bddd 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/UserNewsResourcePreviewParameterProvider.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/UserNewsResourcePreviewParameterProvider.kt @@ -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 diff --git a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt index c5ddd5c10..680c6dcf7 100644 --- a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt +++ b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt @@ -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 = {}, ) } diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index b39f189d1..a9ef26f64 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -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 = {}, ) } diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index 7d0003aed..82d2c0e19 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -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,16 +38,15 @@ class BookmarksViewModel @Inject constructor( userNewsResourceRepository: UserNewsResourceRepository, ) : ViewModel() { - val feedUiState: StateFlow = userNewsResourceRepository.getUserNewsResources() - .filterNot { it.isEmpty() } - .map { newsResources -> newsResources.filter(UserNewsResource::isSaved) } // Only show bookmarked news resources. - .map, NewsFeedUiState>(NewsFeedUiState::Success) - .onStart { emit(Loading) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = Loading, - ) + val feedUiState: StateFlow = + userNewsResourceRepository.getBookmarkedUserNewsResources() + .map, NewsFeedUiState>(NewsFeedUiState::Success) + .onStart { emit(Loading) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = Loading, + ) fun removeFromSavedResources(newsResourceId: String) { viewModelScope.launch { @@ -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) } } } diff --git a/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt b/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt index d97f71095..6469a684b 100644 --- a/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt +++ b/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt @@ -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, ) diff --git a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt index a3566fc31..fde215aa1 100644 --- a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt +++ b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt @@ -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 = {}, ) } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 44a323868..961046538 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -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 = {}, ) } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index 363356aef..84638c55e 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -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 = - 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> = 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() diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt index faf368b1e..58f4f1683 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt @@ -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. diff --git a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt index 9bac2549c..16c593aa0 100644 --- a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt @@ -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, ) diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index 8f863ba5a..e618c1c9f 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -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 diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt index d6ef94521..debc49bcd 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt @@ -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 diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt index dcca35795..457014cc2 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt @@ -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( diff --git a/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt b/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt index e47b25021..c46cb7780 100644 --- a/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt +++ b/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt @@ -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 diff --git a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt index 65d923442..94f86a8e4 100644 --- a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt +++ b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt @@ -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 = {}, ) } diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 4fc9faaca..fd408f9cf 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -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 = {}, ) } diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index 4dac25983..425d66c73 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -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) } } } diff --git a/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt index 3580a960b..ff7a88160 100644 --- a/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt +++ b/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt @@ -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, ) From 08d87ecb1a0955d8895c25d01f3cd081c2d09124 Mon Sep 17 00:00:00 2001 From: James Rose Date: Mon, 17 Apr 2023 13:42:26 -0700 Subject: [PATCH 5/6] Rename getUserNewsResources to observeAll --- .../google/samples/apps/nowinandroid/ui/NiaAppState.kt | 4 ++-- .../repository/CompositeUserNewsResourceRepository.kt | 10 +++++----- .../core/data/repository/UserNewsResourceRepository.kt | 6 +++--- .../data/CompositeUserNewsResourceRepositoryTest.kt | 8 ++++---- .../feature/bookmarks/BookmarksViewModel.kt | 2 +- .../apps/nowinandroid/feature/topic/TopicViewModel.kt | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt index e472ee2af..df6fe1da2 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt @@ -126,8 +126,8 @@ class NiaAppState( * The top level destinations that have unread news resources. */ val topLevelDestinationsWithUnreadResources: StateFlow> = - userNewsResourceRepository.getUserNewsResourcesForFollowedTopics() - .combine(userNewsResourceRepository.getBookmarkedUserNewsResources()) { forYouNewsResources, bookmarkedNewsResources -> + userNewsResourceRepository.observeAllForFollowedTopics() + .combine(userNewsResourceRepository.observeAllBookmarked()) { forYouNewsResources, bookmarkedNewsResources -> setOfNotNull( FOR_YOU.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, BOOKMARKS.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt index dc9ad299f..64e02e7d9 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt @@ -38,7 +38,7 @@ class CompositeUserNewsResourceRepository @Inject constructor( /** * Returns available news resources (joined with user data) matching the given query. */ - override fun getUserNewsResources( + override fun observeAll( query: NewsResourceQuery, ): Flow> = newsRepository.getNewsResources(query) @@ -49,21 +49,21 @@ class CompositeUserNewsResourceRepository @Inject constructor( /** * Returns available news resources (joined with user data) for the followed topics. */ - override fun getUserNewsResourcesForFollowedTopics(): Flow> = + override fun observeAllForFollowedTopics(): Flow> = userDataRepository.userData.map { it.followedTopics }.distinctUntilChanged() .flatMapLatest { followedTopics -> when { followedTopics.isEmpty() -> flowOf(emptyList()) - else -> getUserNewsResources(NewsResourceQuery(filterTopicIds = followedTopics)) + else -> observeAll(NewsResourceQuery(filterTopicIds = followedTopics)) } } - override fun getBookmarkedUserNewsResources(): Flow> = + override fun observeAllBookmarked(): Flow> = userDataRepository.userData.map { it.bookmarkedNewsResources }.distinctUntilChanged() .flatMapLatest { bookmarkedNewsResources -> when { bookmarkedNewsResources.isEmpty() -> flowOf(emptyList()) - else -> getUserNewsResources(NewsResourceQuery(filterNewsIds = bookmarkedNewsResources)) + else -> observeAll(NewsResourceQuery(filterNewsIds = bookmarkedNewsResources)) } } } diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt index 9f7540da2..4e3e214bc 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt @@ -26,7 +26,7 @@ interface UserNewsResourceRepository { /** * Returns available news resources as a stream. */ - fun getUserNewsResources( + fun observeAll( query: NewsResourceQuery = NewsResourceQuery( filterTopicIds = null, filterNewsIds = null, @@ -36,10 +36,10 @@ interface UserNewsResourceRepository { /** * Returns available news resources for the user's followed topics as a stream. */ - fun getUserNewsResourcesForFollowedTopics(): Flow> + fun observeAllForFollowedTopics(): Flow> /** * */ - fun getBookmarkedUserNewsResources(): Flow> + fun observeAllBookmarked(): Flow> } diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt index 78271b809..eb4241295 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt @@ -44,7 +44,7 @@ class CompositeUserNewsResourceRepositoryTest { @Test fun whenNoFilters_allNewsResourcesAreReturned() = runTest { // Obtain the user news resources flow. - val userNewsResources = userNewsResourceRepository.getUserNewsResources() + val userNewsResources = userNewsResourceRepository.observeAll() // Send some news resources and user data into the data repositories. newsRepository.sendNewsResources(sampleNewsResources) @@ -68,7 +68,7 @@ class CompositeUserNewsResourceRepositoryTest { fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest { // Obtain a stream of user news resources for the given topic id. val userNewsResources = - userNewsResourceRepository.getUserNewsResources( + userNewsResourceRepository.observeAll( NewsResourceQuery( filterTopicIds = setOf( sampleTopic1.id, @@ -93,7 +93,7 @@ class CompositeUserNewsResourceRepositoryTest { fun whenFilteredByFollowedTopics_matchingNewsResourcesAreReturned() = runTest { // Obtain a stream of user news resources for the given topic id. val userNewsResources = - userNewsResourceRepository.getUserNewsResourcesForFollowedTopics() + userNewsResourceRepository.observeAllForFollowedTopics() // Send test data into the repositories. val userData = emptyUserData.copy( @@ -114,7 +114,7 @@ class CompositeUserNewsResourceRepositoryTest { @Test fun whenFilteredByBookmarkedResources_matchingNewsResourcesAreReturned() = runTest { // Obtain the bookmarked user news resources flow. - val userNewsResources = userNewsResourceRepository.getBookmarkedUserNewsResources() + val userNewsResources = userNewsResourceRepository.observeAllBookmarked() // Send some news resources and user data into the data repositories. newsRepository.sendNewsResources(sampleNewsResources) diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index 82d2c0e19..8a1869322 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -39,7 +39,7 @@ class BookmarksViewModel @Inject constructor( ) : ViewModel() { val feedUiState: StateFlow = - userNewsResourceRepository.getBookmarkedUserNewsResources() + userNewsResourceRepository.observeAllBookmarked() .map, NewsFeedUiState>(NewsFeedUiState::Success) .onStart { emit(Loading) } .stateIn( diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index 425d66c73..2b2565f9e 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -145,7 +145,7 @@ private fun newsUiState( userDataRepository: UserDataRepository, ): Flow { // Observe news - val newsStream: Flow> = userNewsResourceRepository.getUserNewsResources( + val newsStream: Flow> = userNewsResourceRepository.observeAll( NewsResourceQuery(filterTopicIds = setOf(element = topicId)), ) From f2680a8973a7d8ef02eaff268a40cad808e7c198 Mon Sep 17 00:00:00 2001 From: James Rose Date: Mon, 17 Apr 2023 13:52:58 -0700 Subject: [PATCH 6/6] Add missing method doc --- .../core/data/repository/UserNewsResourceRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt index 4e3e214bc..c0f4c013a 100644 --- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt +++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt @@ -39,7 +39,7 @@ interface UserNewsResourceRepository { fun observeAllForFollowedTopics(): Flow> /** - * + * Returns the user's bookmarked news resources as a stream. */ fun observeAllBookmarked(): Flow> }