From 0e19a2746ce2ec07eba2e9d2b2c69ba3f710b4e7 Mon Sep 17 00:00:00 2001 From: James Rose Date: Fri, 10 Feb 2023 11:02:06 -0800 Subject: [PATCH] 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, ) }