diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index 06ace74ca..6d6f2c890 100644 --- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -17,7 +17,6 @@ package com.google.samples.apps.nowinandroid.ui import androidx.compose.ui.test.assertIsOn -import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText @@ -84,8 +83,6 @@ class NavigationTest { @Test fun firstScreen_isForYou() { composeTestRule.apply { - // WAIT for initial content to be shown - waitUntil { onAllNodes(hasText("HEADLINES")).fetchSemanticsNodes().isNotEmpty() } // VERIFY first topic is displayed onNodeWithText("HEADLINES").assertExists() } @@ -101,8 +98,6 @@ class NavigationTest { @Test fun navigationBar_navigateToPreviouslySelectedTab_restoresContent() { composeTestRule.apply { - // WAIT for initial content to be shown - waitUntil { onAllNodes(hasText("HEADLINES")).fetchSemanticsNodes().isNotEmpty() } // GIVEN the user follows a topic onNodeWithText("HEADLINES").performClick() // WHEN the user navigates to the Topics destination @@ -120,8 +115,6 @@ class NavigationTest { @Test fun navigationBar_reselectTab_keepsState() { composeTestRule.apply { - // WAIT for initial content to be shown - waitUntil { onAllNodes(hasText("HEADLINES")).fetchSemanticsNodes().isNotEmpty() } // GIVEN the user follows a topic onNodeWithText("HEADLINES").performClick() // WHEN the user taps the For You navigation bar item @@ -179,8 +172,6 @@ class NavigationTest { @Test fun navigationBar_backFromAnyDestination_returnsToForYou() { composeTestRule.apply { - // WAIT for initial content to be shown - waitUntil { onAllNodes(hasText("HEADLINES")).fetchSemanticsNodes().isNotEmpty() } // GIVEN the user navigated to the Episodes destination onNodeWithText(episodes).performClick() // and then navigated to the Topics destination diff --git a/core-common/build.gradle b/core-common/build.gradle index 1dff19e67..f82419d66 100644 --- a/core-common/build.gradle +++ b/core-common/build.gradle @@ -16,6 +16,7 @@ plugins { id 'com.android.library' id 'kotlin-android' + id 'kotlin-kapt' } android { @@ -32,4 +33,10 @@ android { kotlinOptions { jvmTarget = '1.8' } +} + +dependencies { + implementation libs.kotlinx.coroutines.android + implementation libs.hilt.android + kapt libs.hilt.compiler } \ No newline at end of file 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 new file mode 100644 index 000000000..5895568a7 --- /dev/null +++ b/core-common/src/main/java/com/google/samples/apps/nowinandroid/core/network/NiaDispatchers.kt @@ -0,0 +1,28 @@ +/* + * 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.network + +import javax.inject.Qualifier +import kotlin.annotation.AnnotationRetention.RUNTIME + +@Qualifier +@Retention(RUNTIME) +annotation class Dispatcher(val niaDispatcher: NiaDispatchers) + +enum class NiaDispatchers { + IO +} diff --git a/core-network/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/di/DispatchersModule.kt similarity index 52% rename from core-network/src/main/java/com/google/samples/apps/nowinandroid/core/network/NiaDispatchers.kt rename to core-common/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/DispatchersModule.kt index b6501ed70..1b8409eff 100644 --- a/core-network/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/di/DispatchersModule.kt @@ -14,26 +14,21 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.network +package com.google.samples.apps.nowinandroid.core.network.di -import javax.inject.Inject +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.MainCoroutineDispatcher -interface NiaDispatchers { - val IO: CoroutineDispatcher - - val Default: CoroutineDispatcher - - val Main: MainCoroutineDispatcher - - val Unconfined: CoroutineDispatcher -} - -class DefaultNiaDispatchers @Inject constructor() : NiaDispatchers { - override val IO: CoroutineDispatcher = Dispatchers.IO - override val Default: CoroutineDispatcher = Dispatchers.Default - override val Main: MainCoroutineDispatcher = Dispatchers.Main - override val Unconfined: CoroutineDispatcher = Dispatchers.Unconfined +@Module +@InstallIn(SingletonComponent::class) +object DispatchersModule { + @Provides + @Dispatcher(IO) + fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO } diff --git a/core-datastore/build.gradle b/core-datastore/build.gradle index 1024cedba..86041ab07 100644 --- a/core-datastore/build.gradle +++ b/core-datastore/build.gradle @@ -57,6 +57,8 @@ protobuf { } dependencies { + implementation project(':core-common') + testImplementation project(':core-testing') implementation libs.kotlinx.coroutines.android diff --git a/core-datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt b/core-datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt index bf4ca947a..4978da0dc 100644 --- a/core-datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt +++ b/core-datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt @@ -22,12 +22,17 @@ import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob @Module @InstallIn(SingletonComponent::class) @@ -37,10 +42,12 @@ object DataStoreModule { @Singleton fun providesUserPreferencesDataStore( @ApplicationContext context: Context, + @Dispatcher(IO) ioDispatcher: CoroutineDispatcher, userPreferencesSerializer: UserPreferencesSerializer ): DataStore = DataStoreFactory.create( - serializer = userPreferencesSerializer + serializer = userPreferencesSerializer, + scope = CoroutineScope(ioDispatcher + SupervisorJob()) ) { context.dataStoreFile("user_preferences.pb") } diff --git a/core-domain/build.gradle b/core-domain/build.gradle index 36e7b6f52..80edd7603 100644 --- a/core-domain/build.gradle +++ b/core-domain/build.gradle @@ -38,6 +38,7 @@ android { } dependencies { + implementation project(':core-common') implementation project(':core-model') implementation project(':core-database') implementation project(':core-datastore') diff --git a/core-domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/fake/FakeNewsRepository.kt b/core-domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/fake/FakeNewsRepository.kt index c4f420f53..946436086 100644 --- a/core-domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/fake/FakeNewsRepository.kt +++ b/core-domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/fake/FakeNewsRepository.kt @@ -18,8 +18,10 @@ package com.google.samples.apps.nowinandroid.core.domain.repository.fake import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.model.data.NewsResource -import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.serialization.json.Json @@ -31,7 +33,7 @@ import kotlinx.serialization.json.Json * backend. */ class FakeNewsRepository @Inject constructor( - private val dispatchers: NiaDispatchers, + @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, private val networkJson: Json ) : NewsRepository { diff --git a/core-domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/fake/FakeTopicsRepository.kt b/core-domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/fake/FakeTopicsRepository.kt index cb2902404..f999e8ea9 100644 --- a/core-domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/fake/FakeTopicsRepository.kt +++ b/core-domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/repository/fake/FakeTopicsRepository.kt @@ -19,10 +19,12 @@ package com.google.samples.apps.nowinandroid.core.domain.repository.fake import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.model.data.Topic -import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.fake.FakeDataSource import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -37,7 +39,7 @@ import kotlinx.serialization.json.Json * backend. */ class FakeTopicsRepository @Inject constructor( - private val dispatchers: NiaDispatchers, + @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, private val networkJson: Json, private val niaPreferences: NiaPreferences ) : TopicsRepository { @@ -52,7 +54,7 @@ class FakeTopicsRepository @Inject constructor( } ) } - .flowOn(dispatchers.IO) + .flowOn(ioDispatcher) override suspend fun setFollowedTopicIds(followedTopicIds: Set) = niaPreferences.setFollowedTopicIds(followedTopicIds) diff --git a/core-domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/repository/FakeNewsRepositoryTest.kt b/core-domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/repository/FakeNewsRepositoryTest.kt index 5a0a9275b..024ac6c55 100644 --- a/core-domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/repository/FakeNewsRepositoryTest.kt +++ b/core-domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/repository/FakeNewsRepositoryTest.kt @@ -17,7 +17,7 @@ package com.google.samples.apps.nowinandroid.core.domain.repository import com.google.samples.apps.nowinandroid.core.domain.repository.fake.FakeNewsRepository -import com.google.samples.apps.nowinandroid.core.network.DefaultNiaDispatchers +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.serialization.json.Json import org.junit.Before @@ -25,11 +25,12 @@ class FakeNewsRepositoryTest { private lateinit var subject: FakeNewsRepository + private val testDispatcher = StandardTestDispatcher() + @Before fun setup() { subject = FakeNewsRepository( - // TODO: Create test-specific NiaDispatchers - dispatchers = DefaultNiaDispatchers(), + ioDispatcher = testDispatcher, networkJson = Json { ignoreUnknownKeys = true } ) } diff --git a/core-network/build.gradle b/core-network/build.gradle index a2cf1f148..b0e9697ad 100644 --- a/core-network/build.gradle +++ b/core-network/build.gradle @@ -38,6 +38,7 @@ android { } dependencies { + implementation project(':core-common') implementation project(':core-model') testImplementation project(':core-testing') diff --git a/core-network/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt b/core-network/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt index 3bae6b0a2..521c6109e 100644 --- a/core-network/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt +++ b/core-network/src/main/java/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt @@ -16,9 +16,7 @@ package com.google.samples.apps.nowinandroid.core.network.di -import com.google.samples.apps.nowinandroid.core.network.DefaultNiaDispatchers import com.google.samples.apps.nowinandroid.core.network.NiANetwork -import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiANetwork import dagger.Binds import dagger.Module @@ -37,9 +35,6 @@ interface NetworkModule { fakeNiANetwork: FakeNiANetwork ): NiANetwork - @Binds - fun bindsNiaDispatchers(defaultNiaDispatchers: DefaultNiaDispatchers): NiaDispatchers - companion object { @Provides @Singleton diff --git a/core-network/src/main/java/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiANetwork.kt b/core-network/src/main/java/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiANetwork.kt index b0c36f478..436b5684c 100644 --- a/core-network/src/main/java/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiANetwork.kt +++ b/core-network/src/main/java/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiANetwork.kt @@ -16,11 +16,13 @@ package com.google.samples.apps.nowinandroid.core.network.fake +import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.NiANetwork -import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString @@ -30,16 +32,16 @@ import kotlinx.serialization.json.Json * [NiANetwork] implementation that provides static news resources to aid development */ class FakeNiANetwork @Inject constructor( - private val dispatchers: NiaDispatchers, + @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, private val networkJson: Json ) : NiANetwork { override suspend fun getTopics(): List = - withContext(dispatchers.IO) { + withContext(ioDispatcher) { networkJson.decodeFromString(FakeDataSource.topicsData) } override suspend fun getNewsResources(): List = - withContext(dispatchers.IO) { + withContext(ioDispatcher) { networkJson.decodeFromString(FakeDataSource.data).resources } } diff --git a/core-network/src/test/java/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiANetworkTest.kt b/core-network/src/test/java/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiANetworkTest.kt index 37a6a8f52..c7fedbd6f 100644 --- a/core-network/src/test/java/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiANetworkTest.kt +++ b/core-network/src/test/java/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiANetworkTest.kt @@ -16,7 +16,7 @@ package com.google.samples.apps.nowinandroid.core.network.fake -import com.google.samples.apps.nowinandroid.core.network.DefaultNiaDispatchers +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals @@ -27,17 +27,18 @@ class FakeNiANetworkTest { private lateinit var subject: FakeNiANetwork + private val testDispatcher = StandardTestDispatcher() + @Before fun setUp() { subject = com.google.samples.apps.nowinandroid.core.network.fake.FakeNiANetwork( - // TODO: Create test-specific NiaDispatchers - dispatchers = DefaultNiaDispatchers(), + ioDispatcher = testDispatcher, networkJson = Json { ignoreUnknownKeys = true } ) } @Test - fun testDeserializationOfTopics() = runTest { + fun testDeserializationOfTopics() = runTest(testDispatcher) { assertEquals( FakeDataSource.sampleTopic, subject.getTopics().first() @@ -45,7 +46,7 @@ class FakeNiANetworkTest { } @Test - fun testDeserializationOfNewsResources() = runTest { + fun testDeserializationOfNewsResources() = runTest(testDispatcher) { assertEquals( FakeDataSource.sampleResource, subject.getNewsResources().first() diff --git a/core-testing/build.gradle b/core-testing/build.gradle index f5a6da468..acbc1da5e 100644 --- a/core-testing/build.gradle +++ b/core-testing/build.gradle @@ -16,6 +16,7 @@ plugins { id 'com.android.library' id 'kotlin-android' + id 'kotlin-kapt' } android { @@ -35,9 +36,13 @@ android { } dependencies { + implementation project(':core-common') implementation project(':core-domain') implementation project(':core-model') + implementation libs.hilt.android + kapt libs.hilt.compiler + api libs.junit4 api libs.mockk api libs.androidx.test.core diff --git a/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatcherModule.kt b/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatcherModule.kt new file mode 100644 index 000000000..d0af32893 --- /dev/null +++ b/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatcherModule.kt @@ -0,0 +1,33 @@ +/* + * 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.testing.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher + +@Module +@InstallIn(SingletonComponent::class) +object TestDispatcherModule { + @Provides + @Singleton + fun providesTestDispatcher(): TestDispatcher = UnconfinedTestDispatcher() +} diff --git a/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt b/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt new file mode 100644 index 000000000..a5eb506ae --- /dev/null +++ b/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt @@ -0,0 +1,38 @@ +/* + * 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.testing.di + +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO +import com.google.samples.apps.nowinandroid.core.network.di.DispatchersModule +import dagger.Module +import dagger.Provides +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.test.TestDispatcher + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [DispatchersModule::class], +) +object TestDispatchersModule { + @Provides + @Dispatcher(IO) + fun providesIODispatcher(testDispatcher: TestDispatcher): CoroutineDispatcher = testDispatcher +}