diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index ec0b0b223..e6ad9c031 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -45,6 +45,7 @@ class AndroidFeatureConventionPlugin : Plugin { add("implementation", project(":core:data")) add("implementation", project(":core:common")) add("implementation", project(":core:navigation")) + add("implementation", project(":core:domain")) add("testImplementation", project(":core:testing")) add("androidTestImplementation", project(":core:testing")) diff --git a/core/domain/.gitignore b/core/domain/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts new file mode 100644 index 000000000..47d07d65e --- /dev/null +++ b/core/domain/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * 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. + */ +plugins { + id("nowinandroid.android.library") + id("nowinandroid.android.library.jacoco") + kotlin("kapt") +} + +dependencies { + + implementation(project(":core:data")) + implementation(project(":core:model")) + + testImplementation(project(":core:testing")) + + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.datetime) + + implementation(libs.hilt.android) + kapt(libs.hilt.compiler) +} \ No newline at end of file diff --git a/core/domain/src/main/AndroidManifest.xml b/core/domain/src/main/AndroidManifest.xml new file mode 100644 index 000000000..888821aae --- /dev/null +++ b/core/domain/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCase.kt new file mode 100644 index 000000000..1d34150b9 --- /dev/null +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCase.kt @@ -0,0 +1,76 @@ +/* + * 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.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.domain.model.FollowableTopic +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +/** + * A use case which obtains a list of topics with their followed state. + */ +class GetFollowableTopicsStreamUseCase @Inject constructor( + private val topicsRepository: TopicsRepository, + private val userDataRepository: UserDataRepository +) { + /** + * Returns a list of topics with their associated followed state. + * + * @param followedTopicIdsStream - the set of topic ids which are currently being followed. By + * default the followed topic ids are supplied from the user data repository, but in certain + * scenarios, such as when creating a temporary set of followed topics, you may wish to override + * this parameter to supply your own list of topic ids. @see ForYouViewModel for an example of + * this. + * @param sortBy - the field used to sort the topics. Default NONE = no sorting. + */ + operator fun invoke( + followedTopicIdsStream: Flow> = + userDataRepository.userDataStream.map { userdata -> + userdata.followedTopics + }, + sortBy: TopicSortField = NONE + ): Flow> { + return combine( + followedTopicIdsStream, + topicsRepository.getTopicsStream() + ) { followedIds, topics -> + val followedTopics = topics + .map { topic -> + FollowableTopic( + topic = topic, + isFollowed = topic.id in followedIds + ) + } + if (sortBy == NAME) { + followedTopics.sortedBy { it.topic.name } + } else { + followedTopics + } + } + } +} + +enum class TopicSortField { + NONE, + NAME, +} diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCase.kt new file mode 100644 index 000000000..fda0b4728 --- /dev/null +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCase.kt @@ -0,0 +1,48 @@ +/* + * 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.AuthorsRepository +import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map + +/** + * A use case which obtains a sorted list of authors with their followed state obtained from user + * data. + */ +class GetPersistentSortedFollowableAuthorsStreamUseCase @Inject constructor( + authorsRepository: AuthorsRepository, + private val userDataRepository: UserDataRepository +) { + private val getSortedFollowableAuthorsStream = + GetSortedFollowableAuthorsStreamUseCase(authorsRepository) + + /** + * Returns a list of authors with their associated followed state sorted alphabetically by name. + */ + operator fun invoke(): Flow> { + return userDataRepository.userDataStream.map { userdata -> + userdata.followedAuthors + }.flatMapLatest { + getSortedFollowableAuthorsStream(it) + } + } +} diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesStreamUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesStreamUseCase.kt new file mode 100644 index 000000000..a5cce3a65 --- /dev/null +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesStreamUseCase.kt @@ -0,0 +1,77 @@ +/* + * 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.UserDataRepository +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.map + +/** + * A use case responsible for obtaining news resources with their associated bookmarked (also known + * as "saved") state. + */ +class GetSaveableNewsResourcesStreamUseCase @Inject constructor( + private val newsRepository: NewsRepository, + userDataRepository: UserDataRepository +) { + + private val bookmarkedNewsResourcesStream = userDataRepository.userDataStream.map { + it.bookmarkedNewsResources + } + + /** + * Returns a list of SaveableNewsResources which match the supplied set of topic ids or author + * ids. + * + * @param filterTopicIds - A set of topic ids used to filter the list of news resources. If + * this is empty AND filterAuthorIds is empty the list of news resources will not be filtered. + * @param filterAuthorIds - A set of author ids used to filter the list of news resources. If + * this is empty AND filterTopicIds is empty the list of news resources will not be filtered. + * + */ + operator fun invoke( + filterTopicIds: Set = emptySet(), + filterAuthorIds: Set = emptySet() + ): Flow> = + if (filterTopicIds.isEmpty() && filterAuthorIds.isEmpty()) { + newsRepository.getNewsResourcesStream() + } else { + newsRepository.getNewsResourcesStream( + filterTopicIds = filterTopicIds, + filterAuthorIds = filterAuthorIds + ) + }.mapToSaveableNewsResources(bookmarkedNewsResourcesStream) +} + +private fun Flow>.mapToSaveableNewsResources( + savedNewsResourceIdsStream: Flow> +): Flow> = + filterNot { it.isEmpty() } + .combine(savedNewsResourceIdsStream) { newsResources, savedNewsResourceIds -> + newsResources.map { newsResource -> + SaveableNewsResource( + newsResource = newsResource, + isSaved = savedNewsResourceIds.contains(newsResource.id) + ) + } + } diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCase.kt new file mode 100644 index 000000000..e1709e4cd --- /dev/null +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCase.kt @@ -0,0 +1,49 @@ +/* + * 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.AuthorsRepository +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * A use case which obtains a list of authors sorted alphabetically by name with their followed + * state. + */ +class GetSortedFollowableAuthorsStreamUseCase @Inject constructor( + private val authorsRepository: AuthorsRepository +) { + /** + * Returns a list of authors with their associated followed state sorted alphabetically by name. + * + * @param followedTopicIds - the set of topic ids which are currently being followed. + */ + operator fun invoke(followedAuthorIds: Set): Flow> { + return authorsRepository.getAuthorsStream().map { authors -> + authors + .map { author -> + FollowableAuthor( + author = author, + isFollowed = author.id in followedAuthorIds + ) + } + .sortedBy { it.author.name } + } + } +} diff --git a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/FollowableAuthor.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableAuthor.kt similarity index 85% rename from core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/FollowableAuthor.kt rename to core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableAuthor.kt index 14ef8bf11..8b6e58947 100644 --- a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/FollowableAuthor.kt +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableAuthor.kt @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.model.data +package com.google.samples.apps.nowinandroid.core.domain.model + +import com.google.samples.apps.nowinandroid.core.model.data.Author /** * An [author] with the additional information for whether or not it is followed. diff --git a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/FollowableTopic.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt similarity index 85% rename from core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/FollowableTopic.kt rename to core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt index caa60ae8e..87a77daa4 100644 --- a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/FollowableTopic.kt +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.model.data +package com.google.samples.apps.nowinandroid.core.domain.model + +import com.google.samples.apps.nowinandroid.core.model.data.Topic /** * A [topic] with the additional information for whether or not it is followed. diff --git a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/SaveableNewsResource.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/SaveableNewsResource.kt similarity index 85% rename from core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/SaveableNewsResource.kt rename to core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/SaveableNewsResource.kt index c886e81de..6850d421f 100644 --- a/core/model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/SaveableNewsResource.kt +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/SaveableNewsResource.kt @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.model.data +package com.google.samples.apps.nowinandroid.core.domain.model + +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource /** * A [NewsResource] with the additional information for whether it is saved. diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCaseTest.kt new file mode 100644 index 000000000..c406de47b --- /dev/null +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsStreamUseCaseTest.kt @@ -0,0 +1,119 @@ +/* + * 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 androidx.compose.runtime.snapshotFlow +import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NAME +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic +import com.google.samples.apps.nowinandroid.core.model.data.Topic +import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository +import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository +import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class GetFollowableTopicsStreamUseCaseTest { + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private val topicsRepository = TestTopicsRepository() + private val userDataRepository = TestUserDataRepository() + + val useCase = GetFollowableTopicsStreamUseCase( + topicsRepository, + userDataRepository + ) + + @Test + fun whenNoParams_followableTopicsAreReturnedWithNoSorting() = runTest { + + // Obtain a stream of followable topics. + val followableTopics = useCase() + + // Send some test topics and their followed state. + topicsRepository.sendTopics(testTopics) + userDataRepository.setFollowedTopicIds(setOf(testTopics[0].id, testTopics[2].id)) + + // Check that the order hasn't changed and that the correct topics are marked as followed. + assertEquals( + listOf( + FollowableTopic(testTopics[0], true), + FollowableTopic(testTopics[1], false), + FollowableTopic(testTopics[2], true), + ), + followableTopics.first() + ) + } + + @Test + fun whenFollowedTopicIdsSupplied_differentFollowedTopicsAreReturned() = runTest { + + // Obtain a stream of followable topics, specifying a list of topic ids which are currently + // followed. + val followableTopics = useCase( + followedTopicIdsStream = snapshotFlow { setOf(testTopics[1].id) } + ) + + // Send some test topics and their followed state. + topicsRepository.sendTopics(testTopics) + userDataRepository.setFollowedTopicIds(setOf(testTopics[0].id)) + + // Check that the topic ids supplied to the use case are used for the bookmark state, not + // the topic ids in the user data repository. + assertEquals( + followableTopics.first(), + listOf( + FollowableTopic(testTopics[0], false), + FollowableTopic(testTopics[1], true), + FollowableTopic(testTopics[2], false), + ) + ) + } + + @Test + fun whenSortOrderIsByName_topicsSortedByNameAreReturned() = runTest { + + // Obtain a stream of followable topics, sorted by name. + val followableTopics = useCase( + sortBy = NAME + ) + + // Send some test topics and their followed state. + topicsRepository.sendTopics(testTopics) + userDataRepository.setFollowedTopicIds(setOf()) + + // Check that the followable topics are sorted by the topic name. + assertEquals( + followableTopics.first(), + testTopics + .sortedBy { it.name } + .map { + FollowableTopic(it, false) + } + ) + } +} + +private val testTopics = listOf( + Topic("1", "Headlines", "", "", "", ""), + Topic("2", "Android Studio", "", "", "", ""), + Topic("3", "Compose", "", "", "", ""), +) diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCaseTest.kt new file mode 100644 index 000000000..250a5e89d --- /dev/null +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetPersistentSortedFollowableAuthorsStreamUseCaseTest.kt @@ -0,0 +1,95 @@ +/* + * 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.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.model.data.Author +import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository +import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository +import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class GetPersistentSortedFollowableAuthorsStreamUseCaseTest { + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private val authorsRepository = TestAuthorsRepository() + private val userDataRepository = TestUserDataRepository() + + val useCase = GetPersistentSortedFollowableAuthorsStreamUseCase( + authorsRepository = authorsRepository, + userDataRepository = userDataRepository + ) + + @Test + fun whenFollowedAuthorsSupplied_sortedFollowableAuthorsAreReturned() = runTest { + + // Obtain the stream of authors. + val followableAuthorsStream = useCase() + + // Supply some authors and their followed state in user data. + authorsRepository.sendAuthors(sampleAuthors) + userDataRepository.setFollowedAuthorIds(setOf(sampleAuthor1.id, sampleAuthor3.id)) + + // Check that the authors have been sorted, and that the followed state is correct. + assertEquals( + listOf( + FollowableAuthor(sampleAuthor2, false), + FollowableAuthor(sampleAuthor1, true), + FollowableAuthor(sampleAuthor3, true) + ), + followableAuthorsStream.first() + ) + } +} + +private val sampleAuthor1 = + Author( + id = "Author1", + name = "Mandy", + imageUrl = "", + twitter = "", + mediumPage = "", + bio = "", + ) + +private val sampleAuthor2 = + Author( + id = "Author2", + name = "Andy", + imageUrl = "", + twitter = "", + mediumPage = "", + bio = "", + ) + +private val sampleAuthor3 = + Author( + id = "Author3", + name = "Sandy", + imageUrl = "", + twitter = "", + mediumPage = "", + bio = "", + ) + +private val sampleAuthors = listOf(sampleAuthor1, sampleAuthor2, sampleAuthor3) diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesStreamUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesStreamUseCaseTest.kt new file mode 100644 index 000000000..c8fa5b949 --- /dev/null +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesStreamUseCaseTest.kt @@ -0,0 +1,205 @@ +/* + * 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.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.model.data.Author +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.util.MainDispatcherRule +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class GetSaveableNewsResourcesStreamUseCaseTest { + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private val newsRepository = TestNewsRepository() + private val userDataRepository = TestUserDataRepository() + + val useCase = GetSaveableNewsResourcesStreamUseCase(newsRepository, userDataRepository) + + @Test + fun whenNoFilters_allNewsResourcesAreReturned() = runTest { + + // Obtain the saveable news resources stream. + val saveableNewsResources = useCase() + + // Send some news resources and bookmarks. + newsRepository.sendNewsResources(sampleNewsResources) + userDataRepository.setNewsResourceBookmarks( + setOf(sampleNewsResources[0].id, sampleNewsResources[2].id) + ) + + // Check that the correct news resources are returned with their bookmarked state. + assertEquals( + listOf( + SaveableNewsResource(sampleNewsResources[0], true), + SaveableNewsResource(sampleNewsResources[1], false), + SaveableNewsResource(sampleNewsResources[2], true) + ), + saveableNewsResources.first() + ) + } + + @Test + fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest { + + // Obtain a stream of saveable news resources for the given topic id. + val saveableNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id)) + + // Send some news resources and bookmarks. + newsRepository.sendNewsResources(sampleNewsResources) + userDataRepository.setNewsResourceBookmarks(setOf()) + + // Check that only news resources with the given topic id are returned. + assertEquals( + sampleNewsResources + .filter { it.topics.contains(sampleTopic1) } + .map { SaveableNewsResource(it, false) }, + saveableNewsResources.first() + ) + } + + @Test + fun whenFilteredByAuthorId_matchingNewsResourcesAreReturned() = runTest { + + // Obtain a stream of saveable news resources for the given author id. + val saveableNewsResources = useCase(filterAuthorIds = setOf(sampleAuthor1.id)) + + // Send some news resources and bookmarks. + newsRepository.sendNewsResources(sampleNewsResources) + userDataRepository.setNewsResourceBookmarks(setOf()) + + // Check that only news resources with the given author id are returned. + assertEquals( + sampleNewsResources + .filter { it.authors.contains(sampleAuthor1) } + .map { SaveableNewsResource(it, false) }, + saveableNewsResources.first() + ) + } + + @Test + fun whenFilteredByAuthorIdAndTopicId_matchingNewsResourcesAreReturned() = runTest { + + // Obtain a stream of saveable news resources for the given author id. + val saveableNewsResources = useCase( + filterAuthorIds = setOf(sampleAuthor2.id), + filterTopicIds = setOf(sampleTopic2.id), + ) + + // Send some news resources and bookmarks. + newsRepository.sendNewsResources(sampleNewsResources) + userDataRepository.setNewsResourceBookmarks(setOf()) + + // Check that only news resources with the given author id or topic id are returned. + assertEquals( + sampleNewsResources + .filter { it.authors.contains(sampleAuthor2) || it.topics.contains(sampleTopic2) } + .map { SaveableNewsResource(it, false) }, + saveableNewsResources.first() + ) + } +} + +private val sampleTopic1 = Topic( + id = "Topic1", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", +) + +private val sampleTopic2 = Topic( + id = "Topic2", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", +) + +private val sampleAuthor1 = + Author( + id = "Author1", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "", + bio = "", + ) + +private val sampleAuthor2 = + Author( + id = "Author2", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "", + bio = "", + ) + +private val sampleNewsResources = listOf( + NewsResource( + id = "1", + title = "Thanks for helping us reach 1M YouTube Subscribers", + content = "Thank you everyone for following the Now in Android series and everything the " + + "Android Developers YouTube channel has to offer. During the Android Developer " + + "Summit, our YouTube channel reached 1 million subscribers! Here’s a small video to " + + "thank you all.", + url = "https://youtu.be/-fJ6poHQrjM", + headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg", + publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), + type = Video, + topics = listOf(sampleTopic1), + authors = listOf(sampleAuthor1) + ), + NewsResource( + id = "2", + title = "Transformations and customisations in the Paging Library", + content = "A demonstration of different operations that can be performed with Paging. " + + "Transformations like inserting separators, when to create a new pager, and " + + "customisation options for consuming PagingData.", + url = "https://youtu.be/ZARz0pjm5YM", + headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg", + publishDate = Instant.parse("2021-11-01T00:00:00.000Z"), + type = Video, + topics = listOf(sampleTopic1, sampleTopic2), + authors = listOf(sampleAuthor1) + ), + NewsResource( + id = "3", + title = "Community tip on Paging", + content = "Tips for using the Paging library from the developer community", + url = "https://youtu.be/r5JgIyS3t3s", + headerImageUrl = "https://i.ytimg.com/vi/r5JgIyS3t3s/maxresdefault.jpg", + publishDate = Instant.parse("2021-11-08T00:00:00.000Z"), + type = Video, + topics = listOf(sampleTopic2), + authors = listOf(sampleAuthor2) + ), +) diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCaseTest.kt new file mode 100644 index 000000000..c1ae1e961 --- /dev/null +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSortedFollowableAuthorsStreamUseCaseTest.kt @@ -0,0 +1,89 @@ +/* + * 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.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.model.data.Author +import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository +import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class GetSortedFollowableAuthorsStreamUseCaseTest { + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private val authorsRepository = TestAuthorsRepository() + + val useCase = GetSortedFollowableAuthorsStreamUseCase(authorsRepository) + + @Test + fun whenFollowedAuthorsSupplied_sortedFollowableAuthorsAreReturned() = runTest { + + // Obtain the stream of authors, specifying their followed state. + val followableAuthorsStream = useCase(followedAuthorIds = setOf(sampleAuthor1.id)) + + // Supply some authors. + authorsRepository.sendAuthors(sampleAuthors) + + // Check that the authors have been sorted, and that the followed state is correct. + assertEquals( + followableAuthorsStream.first(), + listOf( + FollowableAuthor(sampleAuthor2, false), + FollowableAuthor(sampleAuthor1, true), + FollowableAuthor(sampleAuthor3, false) + ) + ) + } +} + +private val sampleAuthor1 = + Author( + id = "Author1", + name = "Mandy", + imageUrl = "", + twitter = "", + mediumPage = "", + bio = "", + ) + +private val sampleAuthor2 = + Author( + id = "Author2", + name = "Andy", + imageUrl = "", + twitter = "", + mediumPage = "", + bio = "", + ) + +private val sampleAuthor3 = + Author( + id = "Author2", + name = "Sandy", + imageUrl = "", + twitter = "", + mediumPage = "", + bio = "", + ) + +private val sampleAuthors = listOf(sampleAuthor1, sampleAuthor2, sampleAuthor3) diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt index 4b0dad48d..748b4e724 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt @@ -74,6 +74,16 @@ class TestUserDataRepository : UserDataRepository { } } + /** + * A test-only API to allow setting/unsetting of bookmarks. + * + */ + fun setNewsResourceBookmarks(newsResourceIds: Set) { + currentUserData.let { current -> + _userData.tryEmit(current.copy(bookmarkedNewsResources = newsResourceIds)) + } + } + /** * A test-only API to allow querying the current followed topics. */ diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index de5d1cf30..4496dc841 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -22,6 +22,7 @@ plugins { dependencies { implementation(project(":core:designsystem")) implementation(project(":core:model")) + implementation(project(":core:domain")) implementation(libs.androidx.core.ktx) implementation(libs.coil.kt) diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt index 67a57edcd..32308bd63 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources /** @@ -112,7 +112,10 @@ fun NewsFeedContentPreview() { newsFeed( feedState = NewsFeedUiState.Success( previewNewsResources.map { - SaveableNewsResource(it, false) + SaveableNewsResource( + it, + false + ) } ), onNewsResourcesCheckedChanged = { _, _ -> } diff --git a/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreenTest.kt b/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreenTest.kt index 8c515e78c..b955912f2 100644 --- a/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreenTest.kt +++ b/feature/author/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreenTest.kt @@ -20,11 +20,11 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Author -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor 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.SaveableNewsResource import kotlinx.datetime.Instant import org.junit.Before import org.junit.Rule diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt index 7cd32a64b..85aa039d0 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt @@ -53,9 +53,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackg import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Author -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt index 1693763b5..137285193 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt @@ -20,12 +20,11 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository -import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository +import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Author -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.NewsResource -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.asResult import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination @@ -44,7 +43,7 @@ class AuthorViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val userDataRepository: UserDataRepository, authorsRepository: AuthorsRepository, - newsRepository: NewsRepository + getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase ) : ViewModel() { private val authorId: String = checkNotNull( @@ -62,16 +61,13 @@ class AuthorViewModel @Inject constructor( initialValue = AuthorUiState.Loading ) - val newsUiState: StateFlow = newsUiStateStream( - authorId = authorId, - userDataRepository = userDataRepository, - newsRepository = newsRepository - ) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = NewsUiState.Loading - ) + val newsUiState: StateFlow = + getSaveableNewsResourcesStream.newsUiStateStream(authorId = authorId) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = NewsUiState.Loading + ) fun followAuthorToggle(followed: Boolean) { viewModelScope.launch { @@ -129,46 +125,18 @@ private fun authorUiStateStream( } } -private fun newsUiStateStream( - authorId: String, - newsRepository: NewsRepository, - userDataRepository: UserDataRepository, +private fun GetSaveableNewsResourcesStreamUseCase.newsUiStateStream( + authorId: String ): Flow { // Observe news - val newsStream: Flow> = newsRepository.getNewsResourcesStream( - filterAuthorIds = setOf(element = authorId), - filterTopicIds = emptySet() - ) - - // Observe bookmarks - val bookmarkStream: Flow> = userDataRepository.userDataStream - .map { it.bookmarkedNewsResources } - - return combine( - newsStream, - bookmarkStream, - ::Pair - ) - .asResult() - .map { newsToBookmarksResult -> - when (newsToBookmarksResult) { - is Result.Success -> { - val (news, bookmarks) = newsToBookmarksResult.data - NewsUiState.Success( - news.map { newsResource -> - SaveableNewsResource( - newsResource, - isSaved = bookmarks.contains(newsResource.id) - ) - } - ) - } - is Result.Loading -> { - NewsUiState.Loading - } - is Result.Error -> { - NewsUiState.Error - } + return this( + filterAuthorIds = setOf(element = authorId) + ).asResult() + .map { newsResult -> + when (newsResult) { + is Result.Success -> NewsUiState.Success(newsResult.data) + is Result.Loading -> NewsUiState.Loading + is Result.Error -> NewsUiState.Error } } } diff --git a/feature/author/src/test/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModelTest.kt b/feature/author/src/test/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModelTest.kt index 5a2a26ebc..cb6c17076 100644 --- a/feature/author/src/test/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModelTest.kt +++ b/feature/author/src/test/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModelTest.kt @@ -17,8 +17,9 @@ package com.google.samples.apps.nowinandroid.feature.author import androidx.lifecycle.SavedStateHandle +import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.Author -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor 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.testing.repository.TestAuthorsRepository @@ -51,6 +52,10 @@ class AuthorViewModelTest { private val userDataRepository = TestUserDataRepository() private val authorsRepository = TestAuthorsRepository() private val newsRepository = TestNewsRepository() + private val getSaveableNewsResourcesStreamUseCase = GetSaveableNewsResourcesStreamUseCase( + newsRepository = newsRepository, + userDataRepository = userDataRepository + ) private lateinit var viewModel: AuthorViewModel @Before @@ -63,7 +68,7 @@ class AuthorViewModelTest { ), userDataRepository = userDataRepository, authorsRepository = authorsRepository, - newsRepository = newsRepository + getSaveableNewsResourcesStream = getSaveableNewsResourcesStreamUseCase ) } diff --git a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt index 713741dee..f018be8c0 100644 --- a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt +++ b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import org.junit.Assert.assertEquals diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index cdd1e2575..281c8025e 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -56,7 +56,7 @@ fun BookmarksRoute( modifier: Modifier = Modifier, viewModel: BookmarksViewModel = hiltViewModel() ) { - val feedState by viewModel.feedState.collectAsStateWithLifecycle() + val feedState by viewModel.feedUiState.collectAsStateWithLifecycle() BookmarksScreen( feedState = feedState, removeFromBookmarks = viewModel::removeFromSavedResources, 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 73afb3031..1b9efc6aa 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 @@ -18,18 +18,15 @@ 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.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository -import com.google.samples.apps.nowinandroid.core.model.data.NewsResource -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -38,41 +35,20 @@ import kotlinx.coroutines.launch @HiltViewModel class BookmarksViewModel @Inject constructor( - newsRepository: NewsRepository, - private val userDataRepository: UserDataRepository + private val userDataRepository: UserDataRepository, + getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase ) : ViewModel() { - private val savedNewsResourcesState: StateFlow> = - userDataRepository.userDataStream - .map { userData -> - userData.bookmarkedNewsResources - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptySet() - ) - val feedState: StateFlow = - newsRepository - .getNewsResourcesStream() - .mapToFeedState(savedNewsResourcesState) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = Loading - ) - - private fun Flow>.mapToFeedState( - savedNewsResourcesState: Flow> - ): Flow = - filterNot { it.isEmpty() } - .combine(savedNewsResourcesState) { newsResources, savedNewsResources -> - newsResources - .filter { newsResource -> savedNewsResources.contains(newsResource.id) } - .map { SaveableNewsResource(it, true) } - } - .map, NewsFeedUiState>(NewsFeedUiState::Success) - .onStart { emit(Loading) } + val feedUiState: StateFlow = getSaveableNewsResourcesStream() + .filterNot { it.isEmpty() } + .map { newsResources -> newsResources.filter(SaveableNewsResource::isSaved) } // Only show bookmarked news resources. + .map, NewsFeedUiState>(NewsFeedUiState::Success) + .onStart { emit(Loading) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = Loading + ) fun removeFromSavedResources(newsResourceId: String) { viewModelScope.launch { 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 9589f26ec..78d9801b1 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,6 +16,7 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks +import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository @@ -42,28 +43,32 @@ class BookmarksViewModelTest { private val userDataRepository = TestUserDataRepository() private val newsRepository = TestNewsRepository() + private val getSaveableNewsResourcesStreamUseCase = GetSaveableNewsResourcesStreamUseCase( + newsRepository = newsRepository, + userDataRepository = userDataRepository + ) private lateinit var viewModel: BookmarksViewModel @Before fun setup() { viewModel = BookmarksViewModel( userDataRepository = userDataRepository, - newsRepository = newsRepository + getSaveableNewsResourcesStream = getSaveableNewsResourcesStreamUseCase ) } @Test fun stateIsInitiallyLoading() = runTest { - assertEquals(Loading, viewModel.feedState.value) + assertEquals(Loading, viewModel.feedUiState.value) } @Test fun oneBookmark_showsInFeed() = runTest { - val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } + val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() } newsRepository.sendNewsResources(previewNewsResources) userDataRepository.updateNewsResourceBookmark(previewNewsResources[0].id, true) - val item = viewModel.feedState.value + val item = viewModel.feedUiState.value assertTrue(item is Success) assertEquals((item as Success).feed.size, 1) @@ -72,7 +77,7 @@ class BookmarksViewModelTest { @Test fun oneBookmark_whenRemoving_removesFromFeed() = runTest { - val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } + val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() } // Set the news resources to be used by this test newsRepository.sendNewsResources(previewNewsResources) // Start with the resource saved @@ -80,7 +85,7 @@ class BookmarksViewModelTest { // Use viewModel to remove saved resource viewModel.removeFromSavedResources(previewNewsResources[0].id) // Verify list of saved resources is now empty - val item = viewModel.feedState.value + val item = viewModel.feedUiState.value assertTrue(item is Success) assertEquals((item as Success).feed.size, 0) diff --git a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt index 231d1b861..e06981709 100644 --- a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt +++ b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt @@ -28,10 +28,10 @@ import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performScrollToNode +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Author -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt index 6099baaea..d6d686d4b 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt @@ -57,8 +57,8 @@ import coil.compose.AsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.Author -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank @Composable diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionUiState.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionUiState.kt index 5bde56ed9..e1e0d56d0 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionUiState.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionUiState.kt @@ -16,8 +16,8 @@ package com.google.samples.apps.nowinandroid.feature.foryou -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic /** * A sealed hierarchy describing the interests selection state for the for you screen. diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 1fce535fd..6d8df6daf 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -87,9 +87,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggl import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewTopics @@ -104,7 +104,7 @@ fun ForYouRoute( modifier: Modifier = Modifier, viewModel: ForYouViewModel = hiltViewModel() ) { - val interestsSelectionState by viewModel.interestsSelectionState.collectAsStateWithLifecycle() + val interestsSelectionState by viewModel.interestsSelectionUiState.collectAsStateWithLifecycle() val feedState by viewModel.feedState.collectAsStateWithLifecycle() val isOffline by viewModel.isOffline.collectAsStateWithLifecycle() val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle() 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 c532319d3..8cbf39c31 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 @@ -24,16 +24,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi import androidx.lifecycle.viewmodel.compose.saveable -import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository -import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository -import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic -import com.google.samples.apps.nowinandroid.core.model.data.NewsResource -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.FollowedInterests import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsUiState.None @@ -44,7 +41,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -57,10 +53,10 @@ import kotlinx.coroutines.launch class ForYouViewModel @Inject constructor( networkMonitor: NetworkMonitor, syncStatusMonitor: SyncStatusMonitor, - authorsRepository: AuthorsRepository, - topicsRepository: TopicsRepository, - private val newsRepository: NewsRepository, private val userDataRepository: UserDataRepository, + private val getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase, + getSortedFollowableAuthorsStream: GetSortedFollowableAuthorsStreamUseCase, + getFollowableTopicsStream: GetFollowableTopicsStreamUseCase, savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -82,17 +78,6 @@ class ForYouViewModel @Inject constructor( initialValue = Unknown ) - private val savedNewsResourcesState: StateFlow> = - userDataRepository.userDataStream - .map { userData -> - userData.bookmarkedNewsResources - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptySet() - ) - /** * The in-progress set of topics to be selected, persisted through process death with a * [SavedStateHandle]. @@ -129,17 +114,16 @@ class ForYouViewModel @Inject constructor( followedInterestsUiState, snapshotFlow { inProgressTopicSelection }, snapshotFlow { inProgressAuthorSelection } - ) { followedInterestsUserState, inProgressTopicSelection, inProgressAuthorSelection -> - when (followedInterestsUserState) { + ) { followedInterestsUiState, inProgressTopicSelection, inProgressAuthorSelection -> + when (followedInterestsUiState) { // If we don't know the current selection state, emit loading. Unknown -> flowOf(NewsFeedUiState.Loading) // If the user has followed topics, use those followed topics to populate the feed is FollowedInterests -> { - - newsRepository.getNewsResourcesStream( - filterTopicIds = followedInterestsUserState.topicIds, - filterAuthorIds = followedInterestsUserState.authorIds - ).mapToFeedState(savedNewsResourcesState) + getSaveableNewsResourcesStream( + filterTopicIds = followedInterestsUiState.topicIds, + filterAuthorIds = followedInterestsUiState.authorIds + ).mapToFeedState() } // If the user hasn't followed interests yet, show a realtime populated feed based // on the in-progress interests selections, if there are any. @@ -147,10 +131,10 @@ class ForYouViewModel @Inject constructor( if (inProgressTopicSelection.isEmpty() && inProgressAuthorSelection.isEmpty()) { flowOf(NewsFeedUiState.Success(emptyList())) } else { - newsRepository.getNewsResourcesStream( + getSaveableNewsResourcesStream( filterTopicIds = inProgressTopicSelection, filterAuthorIds = inProgressAuthorSelection - ).mapToFeedState(savedNewsResourcesState) + ).mapToFeedState() } } } @@ -165,33 +149,20 @@ class ForYouViewModel @Inject constructor( initialValue = NewsFeedUiState.Loading ) - val interestsSelectionState: StateFlow = + val interestsSelectionUiState: StateFlow = combine( followedInterestsUiState, - topicsRepository.getTopicsStream(), - authorsRepository.getAuthorsStream(), - snapshotFlow { inProgressTopicSelection }, - snapshotFlow { inProgressAuthorSelection }, - ) { followedInterestsUserState, availableTopics, availableAuthors, inProgressTopicSelection, - inProgressAuthorSelection -> - - when (followedInterestsUserState) { + getFollowableTopicsStream( + followedTopicIdsStream = snapshotFlow { inProgressTopicSelection } + ), + snapshotFlow { inProgressAuthorSelection }.flatMapLatest { + getSortedFollowableAuthorsStream(it) + } + ) { followedInterestsUiState, topics, authors -> + when (followedInterestsUiState) { Unknown -> ForYouInterestsSelectionUiState.Loading is FollowedInterests -> ForYouInterestsSelectionUiState.NoInterestsSelection None -> { - val topics = availableTopics.map { topic -> - FollowableTopic( - topic = topic, - isFollowed = topic.id in inProgressTopicSelection - ) - } - val authors = availableAuthors.map { author -> - FollowableAuthor( - author = author, - isFollowed = author.id in inProgressAuthorSelection - ) - } - if (topics.isEmpty() && authors.isEmpty()) { ForYouInterestsSelectionUiState.LoadFailed } else { @@ -257,17 +228,6 @@ class ForYouViewModel @Inject constructor( } } -private fun Flow>.mapToFeedState( - savedNewsResourcesState: Flow> -): Flow = - filterNot { it.isEmpty() } - .combine(savedNewsResourcesState) { newsResources, savedNewsResources -> - newsResources.map { newsResource -> - SaveableNewsResource( - newsResource = newsResource, - isSaved = savedNewsResources.contains(newsResource.id) - ) - } - } - .map, NewsFeedUiState>(NewsFeedUiState::Success) +private fun Flow>.mapToFeedState(): Flow = + map, NewsFeedUiState>(NewsFeedUiState::Success) .onStart { emit(NewsFeedUiState.Loading) } 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 7a939638f..7919dda9e 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,12 +17,15 @@ package com.google.samples.apps.nowinandroid.feature.foryou import androidx.lifecycle.SavedStateHandle +import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Author -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository @@ -57,6 +60,17 @@ class ForYouViewModelTest { private val authorsRepository = TestAuthorsRepository() private val topicsRepository = TestTopicsRepository() private val newsRepository = TestNewsRepository() + private val getSaveableNewsResourcesStreamUseCase = GetSaveableNewsResourcesStreamUseCase( + newsRepository = newsRepository, + userDataRepository = userDataRepository + ) + private val getSortedFollowableAuthorsStream = GetSortedFollowableAuthorsStreamUseCase( + authorsRepository = authorsRepository + ) + private val getFollowableTopicsStreamUseCase = GetFollowableTopicsStreamUseCase( + topicsRepository = topicsRepository, + userDataRepository = userDataRepository + ) private lateinit var viewModel: ForYouViewModel @Before @@ -65,9 +79,9 @@ class ForYouViewModelTest { networkMonitor = networkMonitor, syncStatusMonitor = syncStatusMonitor, userDataRepository = userDataRepository, - authorsRepository = authorsRepository, - topicsRepository = topicsRepository, - newsRepository = newsRepository, + getSaveableNewsResourcesStream = getSaveableNewsResourcesStreamUseCase, + getSortedFollowableAuthorsStream = getSortedFollowableAuthorsStream, + getFollowableTopicsStream = getFollowableTopicsStreamUseCase, savedStateHandle = SavedStateHandle() ) } @@ -76,7 +90,7 @@ class ForYouViewModelTest { fun stateIsInitiallyLoading() = runTest { assertEquals( ForYouInterestsSelectionUiState.Loading, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals(NewsFeedUiState.Loading, viewModel.feedState.value) } @@ -84,14 +98,14 @@ class ForYouViewModelTest { @Test fun stateIsLoadingWhenFollowedTopicsAreLoading() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) assertEquals( ForYouInterestsSelectionUiState.Loading, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals(NewsFeedUiState.Loading, viewModel.feedState.value) @@ -117,14 +131,14 @@ class ForYouViewModelTest { @Test fun stateIsLoadingWhenFollowedAuthorsAreLoading() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } authorsRepository.sendAuthors(sampleAuthors) assertEquals( ForYouInterestsSelectionUiState.Loading, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals(NewsFeedUiState.Loading, viewModel.feedState.value) @@ -135,14 +149,14 @@ class ForYouViewModelTest { @Test fun stateIsLoadingWhenTopicsAreLoading() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } userDataRepository.setFollowedTopicIds(emptySet()) assertEquals( ForYouInterestsSelectionUiState.Loading, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals(NewsFeedUiState.Success(emptyList()), viewModel.feedState.value) @@ -153,14 +167,14 @@ class ForYouViewModelTest { @Test fun stateIsLoadingWhenAuthorsAreLoading() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } userDataRepository.setFollowedAuthorIds(emptySet()) assertEquals( ForYouInterestsSelectionUiState.Loading, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals(NewsFeedUiState.Success(emptyList()), viewModel.feedState.value) @@ -171,7 +185,7 @@ class ForYouViewModelTest { @Test fun stateIsInterestsSelectionWhenNewsResourcesAreLoading() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -252,7 +266,7 @@ class ForYouViewModelTest { ) ), ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -268,7 +282,7 @@ class ForYouViewModelTest { @Test fun stateIsInterestsSelectionAfterLoadingEmptyFollowedTopicsAndAuthors() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -350,7 +364,7 @@ class ForYouViewModelTest { ) ), ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -367,7 +381,7 @@ class ForYouViewModelTest { @Test fun stateIsWithoutInterestsSelectionAfterLoadingFollowedTopics() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } authorsRepository.sendAuthors(sampleAuthors) @@ -377,7 +391,7 @@ class ForYouViewModelTest { assertEquals( ForYouInterestsSelectionUiState.NoInterestsSelection, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals(NewsFeedUiState.Loading, viewModel.feedState.value) @@ -385,7 +399,7 @@ class ForYouViewModelTest { assertEquals( ForYouInterestsSelectionUiState.NoInterestsSelection, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -407,7 +421,7 @@ class ForYouViewModelTest { @Test fun stateIsWithoutInterestsSelectionAfterLoadingFollowedAuthors() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } authorsRepository.sendAuthors(sampleAuthors) @@ -417,7 +431,7 @@ class ForYouViewModelTest { assertEquals( ForYouInterestsSelectionUiState.NoInterestsSelection, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Loading, @@ -428,7 +442,7 @@ class ForYouViewModelTest { assertEquals( ForYouInterestsSelectionUiState.NoInterestsSelection, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -449,7 +463,7 @@ class ForYouViewModelTest { @Test fun topicSelectionUpdatesAfterSelectingTopic() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -531,7 +545,7 @@ class ForYouViewModelTest { ) ), ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -615,7 +629,7 @@ class ForYouViewModelTest { ) ), ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -640,7 +654,7 @@ class ForYouViewModelTest { @Test fun topicSelectionUpdatesAfterSelectingAuthor() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -722,7 +736,7 @@ class ForYouViewModelTest { ) ), ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -806,7 +820,7 @@ class ForYouViewModelTest { ) ), ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -831,7 +845,7 @@ class ForYouViewModelTest { @Test fun topicSelectionUpdatesAfterUnselectingTopic() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -916,7 +930,7 @@ class ForYouViewModelTest { ) ), ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -932,7 +946,7 @@ class ForYouViewModelTest { @Test fun topicSelectionUpdatesAfterUnselectingAuthor() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -1017,7 +1031,7 @@ class ForYouViewModelTest { ) ), ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -1033,7 +1047,7 @@ class ForYouViewModelTest { @Test fun topicSelectionUpdatesAfterSavingTopicsOnly() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -1047,7 +1061,7 @@ class ForYouViewModelTest { assertEquals( ForYouInterestsSelectionUiState.NoInterestsSelection, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -1074,7 +1088,7 @@ class ForYouViewModelTest { @Test fun topicSelectionUpdatesAfterSavingAuthorsOnly() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -1088,7 +1102,7 @@ class ForYouViewModelTest { assertEquals( ForYouInterestsSelectionUiState.NoInterestsSelection, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -1111,7 +1125,7 @@ class ForYouViewModelTest { @Test fun topicSelectionUpdatesAfterSavingAuthorsAndTopics() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -1126,7 +1140,7 @@ class ForYouViewModelTest { assertEquals( ForYouInterestsSelectionUiState.NoInterestsSelection, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -1153,7 +1167,7 @@ class ForYouViewModelTest { @Test fun topicSelectionIsResetAfterSavingTopicsAndRemovingThem() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -1239,7 +1253,7 @@ class ForYouViewModelTest { ) ) ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -1255,7 +1269,7 @@ class ForYouViewModelTest { @Test fun authorSelectionIsResetAfterSavingAuthorsAndRemovingThem() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -1341,7 +1355,7 @@ class ForYouViewModelTest { ) ) ), - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( @@ -1357,7 +1371,7 @@ class ForYouViewModelTest { @Test fun newsResourceSelectionUpdatesAfterLoadingFollowedTopics() = runTest { val collectJob1 = - launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionState.collect() } + launch(UnconfinedTestDispatcher()) { viewModel.interestsSelectionUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) @@ -1369,7 +1383,7 @@ class ForYouViewModelTest { assertEquals( ForYouInterestsSelectionUiState.NoInterestsSelection, - viewModel.interestsSelectionState.value + viewModel.interestsSelectionUiState.value ) assertEquals( NewsFeedUiState.Success( diff --git a/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt b/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt index 07976953e..4b067734f 100644 --- a/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt +++ b/feature/interests/src/androidTest/java/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt @@ -25,9 +25,9 @@ import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.Author -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.feature.interests.InterestsScreen import com.google.samples.apps.nowinandroid.feature.interests.InterestsTabState diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index b60fddb6a..23c9caa61 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -36,8 +36,8 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTabRo import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt index c6c01c3ca..dcf46caf2 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt @@ -18,11 +18,12 @@ package com.google.samples.apps.nowinandroid.feature.interests import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository -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.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetPersistentSortedFollowableAuthorsStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.TopicSortField +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -36,9 +37,9 @@ import kotlinx.coroutines.launch @HiltViewModel class InterestsViewModel @Inject constructor( - private val userDataRepository: UserDataRepository, - authorsRepository: AuthorsRepository, - topicsRepository: TopicsRepository + val userDataRepository: UserDataRepository, + getFollowableTopicsStream: GetFollowableTopicsStreamUseCase, + getPersistentSortedFollowableAuthorsStream: GetPersistentSortedFollowableAuthorsStreamUseCase ) : ViewModel() { private val _tabState = MutableStateFlow( @@ -50,35 +51,14 @@ class InterestsViewModel @Inject constructor( val tabState: StateFlow = _tabState.asStateFlow() val uiState: StateFlow = combine( - userDataRepository.userDataStream, - authorsRepository.getAuthorsStream(), - topicsRepository.getTopicsStream(), - ) { userData, availableAuthors, availableTopics -> - - InterestsUiState.Interests( - authors = availableAuthors - .map { author -> - FollowableAuthor( - author = author, - isFollowed = author.id in userData.followedAuthors - ) - } - .sortedBy { it.author.name }, - topics = availableTopics - .map { topic -> - FollowableTopic( - topic = topic, - isFollowed = topic.id in userData.followedTopics - ) - } - .sortedBy { it.topic.name } - ) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = InterestsUiState.Loading - ) + getPersistentSortedFollowableAuthorsStream(), + getFollowableTopicsStream(sortBy = TopicSortField.NAME), + InterestsUiState::Interests + ).stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = InterestsUiState.Loading + ) fun followTopic(followedTopicId: String, followed: Boolean) { viewModelScope.launch { diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt index 0c2453f7b..b34181d96 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic @Composable fun TopicsTabContent( diff --git a/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt b/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt index c36625f43..862faec6b 100644 --- a/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt +++ b/feature/interests/src/test/java/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt @@ -16,9 +16,11 @@ package com.google.samples.apps.nowinandroid.interests +import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetPersistentSortedFollowableAuthorsStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.Author -import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository @@ -47,14 +49,23 @@ class InterestsViewModelTest { private val userDataRepository = TestUserDataRepository() private val authorsRepository = TestAuthorsRepository() private val topicsRepository = TestTopicsRepository() + private val getFollowableTopicsStreamUseCase = GetFollowableTopicsStreamUseCase( + topicsRepository = topicsRepository, + userDataRepository = userDataRepository + ) + private val getPersistentSortedFollowableAuthorsStream = + GetPersistentSortedFollowableAuthorsStreamUseCase( + authorsRepository = authorsRepository, + userDataRepository = userDataRepository + ) private lateinit var viewModel: InterestsViewModel @Before fun setup() { viewModel = InterestsViewModel( userDataRepository = userDataRepository, - authorsRepository = authorsRepository, - topicsRepository = topicsRepository, + getFollowableTopicsStream = getFollowableTopicsStreamUseCase, + getPersistentSortedFollowableAuthorsStream = getPersistentSortedFollowableAuthorsStream ) } diff --git a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt index 182f2b1fd..0f20a5e5e 100644 --- a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt +++ b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt @@ -24,10 +24,10 @@ import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performScrollToNode -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource 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.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic import kotlinx.datetime.Instant import org.junit.Before diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 942922720..716bb5b7c 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -51,8 +51,8 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackg import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews 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 3877fdb57..862e130f1 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 @@ -19,12 +19,11 @@ package com.google.samples.apps.nowinandroid.feature.topic import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository 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.model.data.FollowableTopic -import com.google.samples.apps.nowinandroid.core.model.data.NewsResource -import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource 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 @@ -44,7 +43,8 @@ class TopicViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val userDataRepository: UserDataRepository, topicsRepository: TopicsRepository, - newsRepository: NewsRepository + // newsRepository: NewsRepository, + getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase ) : ViewModel() { private val topicId: String = checkNotNull(savedStateHandle[TopicDestination.topicIdArg]) @@ -63,7 +63,7 @@ class TopicViewModel @Inject constructor( val newUiState: StateFlow = newsUiStateStream( topicId = topicId, userDataRepository = userDataRepository, - newsRepository = newsRepository + getSaveableNewsResourcesStream = getSaveableNewsResourcesStream ) .stateIn( scope = viewModelScope, @@ -129,11 +129,11 @@ private fun topicUiStateStream( private fun newsUiStateStream( topicId: String, - newsRepository: NewsRepository, + getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase, userDataRepository: UserDataRepository, ): Flow { // Observe news - val newsStream: Flow> = newsRepository.getNewsResourcesStream( + val newsStream: Flow> = getSaveableNewsResourcesStream( filterAuthorIds = emptySet(), filterTopicIds = setOf(element = topicId), ) @@ -152,14 +152,7 @@ private fun newsUiStateStream( when (newsToBookmarksResult) { is Result.Success -> { val (news, bookmarks) = newsToBookmarksResult.data - NewsUiState.Success( - news.map { newsResource -> - SaveableNewsResource( - newsResource, - isSaved = bookmarks.contains(newsResource.id) - ) - } - ) + NewsUiState.Success(news) } is Result.Loading -> { NewsUiState.Loading 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 5b9457f0a..94e0e9337 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,7 +17,8 @@ package com.google.samples.apps.nowinandroid.feature.topic import androidx.lifecycle.SavedStateHandle -import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesStreamUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.Topic @@ -51,6 +52,10 @@ class TopicViewModelTest { private val userDataRepository = TestUserDataRepository() private val topicsRepository = TestTopicsRepository() private val newsRepository = TestNewsRepository() + private val getSaveableNewsResourcesStreamUseCase = GetSaveableNewsResourcesStreamUseCase( + newsRepository = newsRepository, + userDataRepository = userDataRepository + ) private lateinit var viewModel: TopicViewModel @Before @@ -60,7 +65,7 @@ class TopicViewModelTest { SavedStateHandle(mapOf(TopicDestination.topicIdArg to testInputTopics[0].topic.id)), userDataRepository = userDataRepository, topicsRepository = topicsRepository, - newsRepository = newsRepository + getSaveableNewsResourcesStream = getSaveableNewsResourcesStreamUseCase ) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9d86e6622..fbf8ba493 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,6 +41,7 @@ include(":core:database") include(":core:datastore") include(":core:datastore-test") include(":core:designsystem") +include(":core:domain") include(":core:model") include(":core:navigation") include(":core:network")