From 99d02f79e49237e6ed8973ef00aac05ad62f5d10 Mon Sep 17 00:00:00 2001 From: Jaehwa Noh Date: Sun, 30 Nov 2025 21:53:14 +0900 Subject: [PATCH 1/2] Add flowOn DefaultDispatcher. Change-Id: I04d005004d4fd6b813e625a1865edf5b65f2a5c8 --- .../CompositeUserNewsResourceRepository.kt | 8 ++++-- .../DefaultSearchContentsRepository.kt | 22 ++++++++++----- .../core/domain/GetFollowableTopicsUseCase.kt | 28 ++++++++++++------- .../core/domain/GetSearchContentsUseCase.kt | 14 ++++++++-- 4 files changed, 50 insertions(+), 22 deletions(-) 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..776548e79 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,8 +37,8 @@ import javax.inject.Inject class CompositeUserNewsResourceRepository @Inject constructor( val newsRepository: NewsRepository, val userDataRepository: UserDataRepository, + @Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher, ) : UserNewsResourceRepository { - /** * Returns available news resources (joined with user data) matching the given query. */ @@ -44,7 +48,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/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..2e8db5f0e 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 @@ -23,8 +23,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,16 +37,20 @@ 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 -> @@ -58,4 +66,4 @@ private fun Flow.mapToUserSearchResult(userDataStream: Flow Date: Sun, 30 Nov 2025 22:05:32 +0900 Subject: [PATCH 2/2] Add new defaultDispatcher argument. Change-Id: I81c5e020fe2632f1c1ad6ca411df59fb0e867ce9 --- .../samples/apps/nowinandroid/ui/NiaAppStateTest.kt | 8 ++++++-- .../core/data/CompositeUserNewsResourceRepositoryTest.kt | 6 ++++++ .../core/domain/GetFollowableTopicsUseCaseTest.kt | 1 + .../nowinandroid/core/testing/util/MainDispatcherRule.kt | 6 +++++- .../feature/bookmarks/BookmarksViewModelTest.kt | 1 + .../nowinandroid/feature/foryou/ForYouViewModelTest.kt | 2 ++ .../apps/nowinandroid/interests/InterestsViewModelTest.kt | 1 + .../nowinandroid/feature/search/SearchViewModelTest.kt | 1 + .../apps/nowinandroid/feature/topic/TopicViewModelTest.kt | 1 + 9 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index c6ddb54fb..e6d067b46 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -29,6 +29,7 @@ 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.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor import dagger.hilt.android.testing.HiltAndroidTest @@ -54,16 +55,19 @@ import kotlin.test.assertTrue @HiltAndroidTest class NiaAppStateTest { - @get:Rule + @get:Rule(0) val composeTestRule = createComposeRule() + @get:Rule(1) + val mainDispatcherRule = MainDispatcherRule() + // Create the test dependencies. private val networkMonitor = TestNetworkMonitor() private val timeZoneMonitor = TestTimeZoneMonitor() private val userNewsResourceRepository = - CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository()) + CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository(), mainDispatcherRule.testDispatcher) // Subject under test. private lateinit var state: NiaAppState 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/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..cd3890104 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 @@ -39,6 +39,7 @@ class GetFollowableTopicsUseCaseTest { val useCase = GetFollowableTopicsUseCase( topicsRepository, userDataRepository, + 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..057996fa8 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, + 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..9e5b5b179 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, + 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 58242110d..f1e1e9d1c 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 @@ -52,6 +52,7 @@ class TopicViewModelTest { private val userNewsResourceRepository = CompositeUserNewsResourceRepository( newsRepository = newsRepository, userDataRepository = userDataRepository, + defaultDispatcher = dispatcherRule.testDispatcher, ) private lateinit var viewModel: TopicViewModel