diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index c2c74458d..5480f1e35 100644 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -31,6 +31,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepo import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -58,7 +59,11 @@ class NiaAppStateTest { private val timeZoneMonitor = TestTimeZoneMonitor() private val userNewsResourceRepository = - CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository()) + CompositeUserNewsResourceRepository( + newsRepository = TestNewsRepository(), + userDataRepository = TestUserDataRepository(), + defaultDispatcher = Dispatchers.Default, + ) // Subject under test. private lateinit var state: NiaAppState diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt index 64e02e7d9..5fcd0d698 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt @@ -18,11 +18,15 @@ 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 com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default +import kotlinx.coroutines.CoroutineDispatcher 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.flowOn import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -33,6 +37,7 @@ import javax.inject.Inject class CompositeUserNewsResourceRepository @Inject constructor( val newsRepository: NewsRepository, val userDataRepository: UserDataRepository, + @Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher, ) : UserNewsResourceRepository { /** @@ -44,7 +49,7 @@ class CompositeUserNewsResourceRepository @Inject constructor( newsRepository.getNewsResources(query) .combine(userDataRepository.userData) { newsResources, userData -> newsResources.mapToUserNewsResources(userData) - } + }.flowOn(defaultDispatcher) /** * Returns available news resources (joined with user data) for the followed topics. diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt index 3bacb8a14..c355e07c4 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt @@ -16,6 +16,7 @@ package com.google.samples.apps.nowinandroid.core.data.repository +import androidx.tracing.trace import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao @@ -25,6 +26,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import com.google.samples.apps.nowinandroid.core.database.model.asFtsEntity import com.google.samples.apps.nowinandroid.core.model.data.SearchResult 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 kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -32,6 +34,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext import javax.inject.Inject @@ -42,6 +45,7 @@ internal class DefaultSearchContentsRepository @Inject constructor( private val topicDao: TopicDao, private val topicFtsDao: TopicFtsDao, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, + @Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher, ) : SearchContentsRepository { override suspend fun populateFtsData() { @@ -75,11 +79,13 @@ internal class DefaultSearchContentsRepository @Inject constructor( .distinctUntilChanged() .flatMapLatest(topicDao::getTopicEntities) return combine(newsResourcesFlow, topicsFlow) { newsResources, topics -> - SearchResult( - topics = topics.map { it.asExternalModel() }, - newsResources = newsResources.map { it.asExternalModel() }, - ) - } + trace("DefaultSearchContentsRepository.searchContents") { + SearchResult( + topics = topics.map { it.asExternalModel() }, + newsResources = newsResources.map { it.asExternalModel() }, + ) + } + }.flowOn(defaultDispatcher) } override fun getSearchContentsCount(): Flow = @@ -87,6 +93,8 @@ internal class DefaultSearchContentsRepository @Inject constructor( newsResourceFtsDao.getCount(), topicFtsDao.getCount(), ) { newsResourceCount, topicsCount -> - newsResourceCount + topicsCount - } + trace("DefaultSearchContentsRepository.getSearchContentsCount") { + newsResourceCount + topicsCount + } + }.flowOn(defaultDispatcher) } diff --git a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt index 05811f4be..b1920db4f 100644 --- a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt +++ b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt @@ -24,20 +24,26 @@ import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResourc 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.runTest import kotlinx.datetime.Instant +import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals class CompositeUserNewsResourceRepositoryTest { + @get:Rule + val dispatcherRule = MainDispatcherRule() + private val newsRepository = TestNewsRepository() private val userDataRepository = TestUserDataRepository() private val userNewsResourceRepository = CompositeUserNewsResourceRepository( newsRepository = newsRepository, userDataRepository = userDataRepository, + defaultDispatcher = dispatcherRule.testDispatcher, ) @Test diff --git a/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt b/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt index 0167a3192..82057234f 100644 --- a/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt +++ b/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt @@ -16,13 +16,18 @@ package com.google.samples.apps.nowinandroid.core.domain +import androidx.tracing.trace 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.domain.TopicSortField.NAME import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NONE import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn import javax.inject.Inject /** @@ -31,6 +36,7 @@ import javax.inject.Inject class GetFollowableTopicsUseCase @Inject constructor( private val topicsRepository: TopicsRepository, private val userDataRepository: UserDataRepository, + @Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher, ) { /** * Returns a list of topics with their associated followed state. @@ -41,18 +47,20 @@ class GetFollowableTopicsUseCase @Inject constructor( userDataRepository.userData, topicsRepository.getTopics(), ) { userData, topics -> - val followedTopics = topics - .map { topic -> - FollowableTopic( - topic = topic, - isFollowed = topic.id in userData.followedTopics, - ) + trace("GetFollowableTopicsUseCase.invoke") { + val followedTopics = topics + .map { topic -> + FollowableTopic( + topic = topic, + isFollowed = topic.id in userData.followedTopics, + ) + } + when (sortBy) { + NAME -> followedTopics.sortedBy { it.topic.name } + else -> followedTopics } - when (sortBy) { - NAME -> followedTopics.sortedBy { it.topic.name } - else -> followedTopics } - } + }.flowOn(defaultDispatcher) } enum class TopicSortField { diff --git a/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetSearchContentsUseCase.kt b/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetSearchContentsUseCase.kt index d1065e87c..6634562ab 100644 --- a/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetSearchContentsUseCase.kt +++ b/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetSearchContentsUseCase.kt @@ -16,6 +16,7 @@ package com.google.samples.apps.nowinandroid.core.domain +import androidx.tracing.trace import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic @@ -23,8 +24,12 @@ import com.google.samples.apps.nowinandroid.core.model.data.SearchResult import com.google.samples.apps.nowinandroid.core.model.data.UserData import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.model.data.UserSearchResult +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn import javax.inject.Inject /** @@ -33,29 +38,35 @@ import javax.inject.Inject class GetSearchContentsUseCase @Inject constructor( private val searchContentsRepository: SearchContentsRepository, private val userDataRepository: UserDataRepository, + @Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher, ) { operator fun invoke( searchQuery: String, ): Flow = searchContentsRepository.searchContents(searchQuery) - .mapToUserSearchResult(userDataRepository.userData) + .mapToUserSearchResult(userDataRepository.userData, defaultDispatcher) } -private fun Flow.mapToUserSearchResult(userDataStream: Flow): Flow = +private fun Flow.mapToUserSearchResult( + userDataStream: Flow, + defaultDispatcher: CoroutineDispatcher, +): Flow = combine(userDataStream) { searchResult, userData -> - UserSearchResult( - topics = searchResult.topics.map { topic -> - FollowableTopic( - topic = topic, - isFollowed = topic.id in userData.followedTopics, - ) - }, - newsResources = searchResult.newsResources.map { news -> - UserNewsResource( - newsResource = news, - userData = userData, - ) - }, - ) - } + trace("Flow.mapToUserSearchResult") { + UserSearchResult( + topics = searchResult.topics.map { topic -> + FollowableTopic( + topic = topic, + isFollowed = topic.id in userData.followedTopics, + ) + }, + newsResources = searchResult.newsResources.map { news -> + UserNewsResource( + newsResource = news, + userData = userData, + ) + }, + ) + } + }.flowOn(defaultDispatcher) diff --git a/core/domain/src/test/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCaseTest.kt b/core/domain/src/test/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCaseTest.kt index 42a31f858..8da3a300a 100644 --- a/core/domain/src/test/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCaseTest.kt +++ b/core/domain/src/test/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCaseTest.kt @@ -37,8 +37,9 @@ class GetFollowableTopicsUseCaseTest { private val userDataRepository = TestUserDataRepository() val useCase = GetFollowableTopicsUseCase( - topicsRepository, - userDataRepository, + topicsRepository = topicsRepository, + userDataRepository = userDataRepository, + defaultDispatcher = mainDispatcherRule.testDispatcher, ) @Test diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/MainDispatcherRule.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/MainDispatcherRule.kt index 666c4edd4..5d68f6e2a 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/MainDispatcherRule.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/MainDispatcherRule.kt @@ -30,7 +30,11 @@ import org.junit.runner.Description * for the duration of the test. */ class MainDispatcherRule( - private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), + /** + * Expose testDispatcher to share the scheduler to the test. + * See more in [Documentation](https://developer.android.com/kotlin/coroutines/test#injecting-test-dispatchers) + */ + val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) = Dispatchers.setMain(testDispatcher) diff --git a/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt b/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt index aa42adae2..099c9dea3 100644 --- a/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt +++ b/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt @@ -48,6 +48,7 @@ class BookmarksViewModelTest { private val userNewsResourceRepository = CompositeUserNewsResourceRepository( newsRepository = newsRepository, userDataRepository = userDataRepository, + defaultDispatcher = dispatcherRule.testDispatcher, ) private lateinit var viewModel: BookmarksViewModel diff --git a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt index 812544c0c..e9208efe2 100644 --- a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt @@ -65,11 +65,13 @@ class ForYouViewModelTest { private val userNewsResourceRepository = CompositeUserNewsResourceRepository( newsRepository = newsRepository, userDataRepository = userDataRepository, + defaultDispatcher = mainDispatcherRule.testDispatcher, ) private val getFollowableTopicsUseCase = GetFollowableTopicsUseCase( topicsRepository = topicsRepository, userDataRepository = userDataRepository, + defaultDispatcher = mainDispatcherRule.testDispatcher, ) private val savedStateHandle = SavedStateHandle() diff --git a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt b/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt index cdf21f325..c1a5befdc 100644 --- a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt +++ b/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt @@ -59,6 +59,7 @@ class InterestsViewModelTest { private val getFollowableTopicsUseCase = GetFollowableTopicsUseCase( topicsRepository = topicsRepository, userDataRepository = userDataRepository, + defaultDispatcher = mainDispatcherRule.testDispatcher, ) private lateinit var viewModel: InterestsViewModel diff --git a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt b/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt index 1b866cec2..51b14824f 100644 --- a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt +++ b/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt @@ -57,6 +57,7 @@ class SearchViewModelTest { private val getSearchContentsUseCase = GetSearchContentsUseCase( searchContentsRepository = searchContentsRepository, userDataRepository = userDataRepository, + defaultDispatcher = dispatcherRule.testDispatcher, ) private val recentSearchRepository = TestRecentSearchRepository() private val getRecentQueryUseCase = GetRecentSearchQueriesUseCase(recentSearchRepository) diff --git a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt index 34f21a59a..06d0e9684 100644 --- a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt +++ b/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt @@ -64,6 +64,7 @@ class TopicViewModelTest { private val userNewsResourceRepository = CompositeUserNewsResourceRepository( newsRepository = newsRepository, userDataRepository = userDataRepository, + defaultDispatcher = dispatcherRule.testDispatcher, ) private lateinit var viewModel: TopicViewModel