Merge pull request #508 from android/mm/chipcolor

Update topic chips to show a different color based on whether the user is following that topic
pull/524/head
MagicalMeghan 2 years ago committed by GitHub
commit de5ceb40ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -18,52 +18,43 @@ 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.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository 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.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
/** /**
* A use case responsible for obtaining news resources with their associated bookmarked (also known * A use case responsible for obtaining news resources with their associated bookmarked (also known
* as "saved") state. * as "saved") state.
*/ */
class GetSaveableNewsResourcesUseCase @Inject constructor( class GetUserNewsResourcesUseCase @Inject constructor(
private val newsRepository: NewsRepository, private val newsRepository: NewsRepository,
userDataRepository: UserDataRepository private val userDataRepository: UserDataRepository
) { ) {
private val bookmarkedNewsResources = userDataRepository.userData.map {
it.bookmarkedNewsResources
}
/** /**
* Returns a list of SaveableNewsResources which match the supplied set of topic ids. * Returns a list of UserNewsResources which match the supplied set of topic ids.
* *
* @param filterTopicIds - A set of topic ids used to filter the list of news resources. If * @param filterTopicIds - A set of topic ids used to filter the list of news resources. If
* this is empty the list of news resources will not be filtered. * this is empty the list of news resources will not be filtered.
*/ */
operator fun invoke( operator fun invoke(
filterTopicIds: Set<String> = emptySet() filterTopicIds: Set<String> = emptySet()
): Flow<List<SaveableNewsResource>> = ): Flow<List<UserNewsResource>> =
if (filterTopicIds.isEmpty()) { if (filterTopicIds.isEmpty()) {
newsRepository.getNewsResources() newsRepository.getNewsResources()
} else { } else {
newsRepository.getNewsResources(filterTopicIds = filterTopicIds) newsRepository.getNewsResources(filterTopicIds = filterTopicIds)
}.mapToSaveableNewsResources(bookmarkedNewsResources) }.mapToUserNewsResources(userDataRepository.userData)
} }
private fun Flow<List<NewsResource>>.mapToSaveableNewsResources( private fun Flow<List<NewsResource>>.mapToUserNewsResources(
savedNewsResourceIdsStream: Flow<Set<String>> userDataStream: Flow<UserData>
): Flow<List<SaveableNewsResource>> = ): Flow<List<UserNewsResource>> =
filterNot { it.isEmpty() } filterNot { it.isEmpty() }
.combine(savedNewsResourceIdsStream) { newsResources, savedNewsResourceIds -> .combine(userDataStream) { newsResources, userData ->
newsResources.map { newsResource -> newsResources.mapToUserNewsResources(userData)
SaveableNewsResource(
newsResource = newsResource,
isSaved = savedNewsResourceIds.contains(newsResource.id)
)
}
} }

@ -17,11 +17,27 @@
package com.google.samples.apps.nowinandroid.core.domain.model package com.google.samples.apps.nowinandroid.core.domain.model
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
/** /**
* A [topic] with the additional information for whether or not it is followed. * A [topic] with the additional information for whether or not it is followed.
*/ */
data class FollowableTopic( data class FollowableTopic( // TODO consider changing to UserTopic and flattening
val topic: Topic, val topic: Topic,
val isFollowed: Boolean val isFollowed: Boolean
) )
val previewFollowableTopics = listOf(
FollowableTopic(
previewTopics[0],
isFollowed = false
),
FollowableTopic(
previewTopics[1],
isFollowed = true
),
FollowableTopic(
previewTopics[2],
isFollowed = false
)
)

@ -1,27 +0,0 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.domain.model
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
/**
* A [NewsResource] with the additional information for whether it is saved.
*/
data class SaveableNewsResource(
val newsResource: NewsResource,
val isSaved: Boolean,
)

@ -0,0 +1,129 @@
/*
* 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.model
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Codelab
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Unknown
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
/* ktlint-disable max-line-length */
/**
* A [NewsResource] with additional user information such as whether the user is following the
* news resource's topics and whether they have saved (bookmarked) this news resource.
*/
data class UserNewsResource internal constructor(
val id: String,
val title: String,
val content: String,
val url: String,
val headerImageUrl: String?,
val publishDate: Instant,
val type: NewsResourceType,
val followableTopics: List<FollowableTopic>,
val isSaved: Boolean
) {
constructor(newsResource: NewsResource, userData: UserData) : this(
id = newsResource.id,
title = newsResource.title,
content = newsResource.content,
url = newsResource.url,
headerImageUrl = newsResource.headerImageUrl,
publishDate = newsResource.publishDate,
type = newsResource.type,
followableTopics = newsResource.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id)
)
},
isSaved = userData.bookmarkedNewsResources.contains(newsResource.id)
)
}
fun List<NewsResource>.mapToUserNewsResources(userData: UserData): List<UserNewsResource> {
return map { UserNewsResource(it, userData) }
}
val previewUserNewsResources = listOf(
UserNewsResource(
id = "1",
title = "Android Basics with Compose",
content = "We released the first two units of Android Basics with Compose, our first free course that teaches Android Development with Jetpack Compose to anyone; you do not need any prior programming experience other than basic computer literacy to get started. Youll learn the fundamentals of programming in Kotlin while building Android apps using Jetpack Compose, Androids modern toolkit that simplifies and accelerates native UI development. These two units are just the beginning; more will be coming soon. Check out Android Basics with Compose to get started on your Android development journey",
url = "https://android-developers.googleblog.com/2022/05/new-android-basics-with-compose-course.html",
headerImageUrl = "https://developer.android.com/images/hero-assets/android-basics-compose.svg",
publishDate = LocalDateTime(
year = 2022,
monthNumber = 5,
dayOfMonth = 4,
hour = 23,
minute = 0,
second = 0,
nanosecond = 0
).toInstant(TimeZone.UTC),
type = Codelab,
followableTopics = listOf(previewFollowableTopics[1]),
isSaved = true
),
UserNewsResource(
id = "2",
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! Heres 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,
followableTopics = listOf(previewFollowableTopics[0], previewFollowableTopics[1]),
isSaved = false
),
UserNewsResource(
id = "3",
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,
followableTopics = listOf(previewFollowableTopics[2]),
isSaved = false
),
UserNewsResource(
id = "4",
title = "New Jetpack Release",
content = "New Jetpack release includes updates to libraries such as CameraX, Benchmark, and" +
"more!",
url = "https://developer.android.com/jetpack/androidx/versions/all-channel",
headerImageUrl = "",
publishDate = Instant.parse("2022-10-01T00:00:00.000Z"),
type = Unknown,
followableTopics = listOf(previewFollowableTopics[2]),
isSaved = true
)
)

@ -16,12 +16,13 @@
package com.google.samples.apps.nowinandroid.core.domain 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.domain.model.mapToUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource 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.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.Topic 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.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -30,7 +31,7 @@ import kotlinx.datetime.Instant
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
class GetSaveableNewsResourcesUseCaseTest { class GetUserNewsResourcesUseCaseTest {
@get:Rule @get:Rule
val mainDispatcherRule = MainDispatcherRule() val mainDispatcherRule = MainDispatcherRule()
@ -38,47 +39,48 @@ class GetSaveableNewsResourcesUseCaseTest {
private val newsRepository = TestNewsRepository() private val newsRepository = TestNewsRepository()
private val userDataRepository = TestUserDataRepository() private val userDataRepository = TestUserDataRepository()
val useCase = GetSaveableNewsResourcesUseCase(newsRepository, userDataRepository) val useCase = GetUserNewsResourcesUseCase(newsRepository, userDataRepository)
@Test @Test
fun whenNoFilters_allNewsResourcesAreReturned() = runTest { fun whenNoFilters_allNewsResourcesAreReturned() = runTest {
// Obtain the saveable news resources stream. // Obtain the user news resources stream.
val saveableNewsResources = useCase() val userNewsResources = useCase()
// Send some news resources and bookmarks. // Send some news resources and user data into the data repositories.
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
userDataRepository.setNewsResourceBookmarks(
setOf(sampleNewsResources[0].id, sampleNewsResources[2].id) // Construct the test user data with bookmarks and followed topics.
val userData = emptyUserData.copy(
bookmarkedNewsResources = setOf(sampleNewsResources[0].id, sampleNewsResources[2].id),
followedTopics = setOf(sampleTopic1.id)
) )
userDataRepository.setUserData(userData)
// Check that the correct news resources are returned with their bookmarked state. // Check that the correct news resources are returned with their bookmarked state.
assertEquals( assertEquals(
listOf( sampleNewsResources.mapToUserNewsResources(userData),
SaveableNewsResource(sampleNewsResources[0], true), userNewsResources.first()
SaveableNewsResource(sampleNewsResources[1], false),
SaveableNewsResource(sampleNewsResources[2], true)
),
saveableNewsResources.first()
) )
} }
@Test @Test
fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest { fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest {
// Obtain a stream of saveable news resources for the given topic id. // Obtain a stream of user news resources for the given topic id.
val saveableNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id)) val userNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id))
// Send some news resources and bookmarks. // Send test data into the repositories.
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
userDataRepository.setNewsResourceBookmarks(setOf()) userDataRepository.setUserData(emptyUserData)
// Check that only news resources with the given topic id are returned. // Check that only news resources with the given topic id are returned.
assertEquals( assertEquals(
sampleNewsResources sampleNewsResources
.filter { it.topics.contains(sampleTopic1) } .filter { it.topics.contains(sampleTopic1) }
.map { SaveableNewsResource(it, false) }, .mapToUserNewsResources(emptyUserData),
saveableNewsResources.first() userNewsResources.first()
) )
} }
} }

@ -0,0 +1,106 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.domain
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Article
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.datetime.Clock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class UserNewsResourceTest {
/**
* Given: Some user data and news resources
* When: They are combined using `UserNewsResource.from`
* Then: The correct UserNewsResources are constructed
*/
@Test
fun userNewsResourcesAreConstructedFromNewsResourcesAndUserData() {
val newsResource1 = NewsResource(
id = "N1",
title = "Test news title",
content = "Test news content",
url = "Test URL",
headerImageUrl = "Test image URL",
publishDate = Clock.System.now(),
type = Article,
topics = listOf(
Topic(
id = "T1",
name = "Topic 1",
shortDescription = "Topic 1 short description",
longDescription = "Topic 1 long description",
url = "Topic 1 URL",
imageUrl = "Topic 1 image URL"
),
Topic(
id = "T2",
name = "Topic 2",
shortDescription = "Topic 2 short description",
longDescription = "Topic 2 long description",
url = "Topic 2 URL",
imageUrl = "Topic 2 image URL"
),
)
)
val userData = UserData(
bookmarkedNewsResources = setOf("N1"),
followedTopics = setOf("T1"),
themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM,
shouldHideOnboarding = true
)
val userNewsResource = UserNewsResource(newsResource1, userData)
// Check that the simple field mappings have been done correctly.
assertEquals(newsResource1.id, userNewsResource.id)
assertEquals(newsResource1.title, userNewsResource.title)
assertEquals(newsResource1.content, userNewsResource.content)
assertEquals(newsResource1.url, userNewsResource.url)
assertEquals(newsResource1.headerImageUrl, userNewsResource.headerImageUrl)
assertEquals(newsResource1.publishDate, userNewsResource.publishDate)
// Check that each Topic has been converted to a FollowedTopic correctly.
assertEquals(newsResource1.topics.size, userNewsResource.followableTopics.size)
for (topic in newsResource1.topics) {
// Construct the expected FollowableTopic.
val followableTopic = FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id)
)
assertTrue(userNewsResource.followableTopics.contains(followableTopic))
}
// Check that the saved flag is set correctly.
assertEquals(
userData.bookmarkedNewsResources.contains(newsResource1.id),
userNewsResource.isSaved
)
}
}

@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
private val emptyUserData = UserData( val emptyUserData = UserData(
bookmarkedNewsResources = emptySet(), bookmarkedNewsResources = emptySet(),
followedTopics = emptySet(), followedTopics = emptySet(),
themeBrand = ThemeBrand.DEFAULT, themeBrand = ThemeBrand.DEFAULT,
@ -98,4 +98,11 @@ class TestUserDataRepository : UserDataRepository {
*/ */
fun getCurrentFollowedTopics(): Set<String>? = fun getCurrentFollowedTopics(): Set<String>? =
_userData.replayCache.firstOrNull()?.followedTopics _userData.replayCache.firstOrNull()?.followedTopics
/**
* A test-only API to allow setting of user data directly.
*/
fun setUserData(userData: UserData) {
_userData.tryEmit(userData)
}
} }

@ -17,10 +17,12 @@
package com.google.samples.apps.nowinandroid.core.ui package com.google.samples.apps.nowinandroid.core.ui
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertContentDescriptionEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.domain.model.previewFollowableTopics
import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -30,12 +32,12 @@ class NewsResourceCardTest {
@Test @Test
fun testMetaDataDisplay_withCodelabResource() { fun testMetaDataDisplay_withCodelabResource() {
val newsWithKnownResourceType = previewNewsResources[0] val newsWithKnownResourceType = previewUserNewsResources[0]
var dateFormatted = "" var dateFormatted = ""
composeTestRule.setContent { composeTestRule.setContent {
NewsResourceCardExpanded( NewsResourceCardExpanded(
newsResource = newsWithKnownResourceType, userNewsResource = newsWithKnownResourceType,
isBookmarked = false, isBookmarked = false,
onToggleBookmark = {}, onToggleBookmark = {},
onClick = {} onClick = {}
@ -57,12 +59,12 @@ class NewsResourceCardTest {
@Test @Test
fun testMetaDataDisplay_withUnknownResource() { fun testMetaDataDisplay_withUnknownResource() {
val newsWithUnknownResourceType = previewNewsResources[3] val newsWithUnknownResourceType = previewUserNewsResources[3]
var dateFormatted = "" var dateFormatted = ""
composeTestRule.setContent { composeTestRule.setContent {
NewsResourceCardExpanded( NewsResourceCardExpanded(
newsResource = newsWithUnknownResourceType, userNewsResource = newsWithUnknownResourceType,
isBookmarked = false, isBookmarked = false,
onToggleBookmark = {}, onToggleBookmark = {},
onClick = {} onClick = {}
@ -75,4 +77,23 @@ class NewsResourceCardTest {
.onNodeWithText(dateFormatted) .onNodeWithText(dateFormatted)
.assertIsDisplayed() .assertIsDisplayed()
} }
@Test
fun testTopicsChipColorBackground_matchesFollowedState() {
composeTestRule.setContent {
NewsResourceTopics(topics = previewFollowableTopics)
}
for (followableTopic in previewFollowableTopics) {
val topicName = followableTopic.topic.name
val expectedContentDescription = if (followableTopic.isFollowed) {
"$topicName is followed"
} else {
"$topicName is not followed"
}
composeTestRule
.onNodeWithText(topicName.uppercase())
.assertContentDescriptionEquals(expectedContentDescription)
}
}
} }

@ -37,8 +37,8 @@ import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources
/** /**
* An extension on [LazyListScope] defining a feed with news resources. * An extension on [LazyListScope] defining a feed with news resources.
@ -51,21 +51,21 @@ fun LazyGridScope.newsFeed(
when (feedState) { when (feedState) {
NewsFeedUiState.Loading -> Unit NewsFeedUiState.Loading -> Unit
is NewsFeedUiState.Success -> { is NewsFeedUiState.Success -> {
items(feedState.feed, key = { it.newsResource.id }) { saveableNewsResource -> items(feedState.feed, key = { it.id }) { userNewsResource ->
val resourceUrl by remember { val resourceUrl by remember {
mutableStateOf(Uri.parse(saveableNewsResource.newsResource.url)) mutableStateOf(Uri.parse(userNewsResource.url))
} }
val context = LocalContext.current val context = LocalContext.current
val backgroundColor = MaterialTheme.colorScheme.background.toArgb() val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
NewsResourceCardExpanded( NewsResourceCardExpanded(
newsResource = saveableNewsResource.newsResource, userNewsResource = userNewsResource,
isBookmarked = saveableNewsResource.isSaved, isBookmarked = userNewsResource.isSaved,
onClick = { launchCustomChromeTab(context, resourceUrl, backgroundColor) }, onClick = { launchCustomChromeTab(context, resourceUrl, backgroundColor) },
onToggleBookmark = { onToggleBookmark = {
onNewsResourcesCheckedChanged( onNewsResourcesCheckedChanged(
saveableNewsResource.newsResource.id, userNewsResource.id,
!saveableNewsResource.isSaved !userNewsResource.isSaved
) )
} }
) )
@ -100,7 +100,7 @@ sealed interface NewsFeedUiState {
/** /**
* The list of news resources contained in this feed. * The list of news resources contained in this feed.
*/ */
val feed: List<SaveableNewsResource> val feed: List<UserNewsResource>
) : NewsFeedUiState ) : NewsFeedUiState
} }
@ -125,12 +125,7 @@ private fun NewsFeedContentPreview() {
LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) { LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) {
newsFeed( newsFeed(
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
previewNewsResources.map { previewUserNewsResources
SaveableNewsResource(
it,
false
)
}
), ),
onNewsResourcesCheckedChanged = { _, _ -> } onNewsResourcesCheckedChanged = { _, _ -> }
) )

@ -46,6 +46,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -56,10 +57,11 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconT
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons 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.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Locale import java.util.Locale
@ -73,7 +75,7 @@ import kotlinx.datetime.toJavaInstant
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NewsResourceCardExpanded( fun NewsResourceCardExpanded(
newsResource: NewsResource, userNewsResource: UserNewsResource,
isBookmarked: Boolean, isBookmarked: Boolean,
onToggleBookmark: () -> Unit, onToggleBookmark: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
@ -91,9 +93,9 @@ fun NewsResourceCardExpanded(
} }
) { ) {
Column { Column {
if (!newsResource.headerImageUrl.isNullOrEmpty()) { if (!userNewsResource.headerImageUrl.isNullOrEmpty()) {
Row { Row {
NewsResourceHeaderImage(newsResource.headerImageUrl) NewsResourceHeaderImage(userNewsResource.headerImageUrl)
} }
} }
Box( Box(
@ -103,18 +105,18 @@ fun NewsResourceCardExpanded(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Row { Row {
NewsResourceTitle( NewsResourceTitle(
newsResource.title, userNewsResource.title,
modifier = Modifier.fillMaxWidth((.8f)) modifier = Modifier.fillMaxWidth((.8f))
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
BookmarkButton(isBookmarked, onToggleBookmark) BookmarkButton(isBookmarked, onToggleBookmark)
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
NewsResourceMetaData(newsResource.publishDate, newsResource.type) NewsResourceMetaData(userNewsResource.publishDate, userNewsResource.type)
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
NewsResourceShortDescription(newsResource.content) NewsResourceShortDescription(userNewsResource.content)
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
NewsResourceTopics(newsResource.topics) NewsResourceTopics(userNewsResource.followableTopics)
} }
} }
} }
@ -228,7 +230,7 @@ fun NewsResourceShortDescription(
@Composable @Composable
fun NewsResourceTopics( fun NewsResourceTopics(
topics: List<Topic>, topics: List<FollowableTopic>,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// Store the ID of the Topic which has its "following" menu expanded, if any. // Store the ID of the Topic which has its "following" menu expanded, if any.
@ -239,17 +241,35 @@ fun NewsResourceTopics(
modifier = modifier.horizontalScroll(rememberScrollState()), // causes narrow chips modifier = modifier.horizontalScroll(rememberScrollState()), // causes narrow chips
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
for (topic in topics) { for (followableTopic in topics) {
NiaTopicTag( NiaTopicTag(
expanded = expandedTopicId == topic.id, expanded = expandedTopicId == followableTopic.topic.id,
followed = true, // ToDo: Check if topic is followed followed = followableTopic.isFollowed,
onDropdownMenuToggle = { show -> onDropdownMenuToggle = { show ->
expandedTopicId = if (show) topic.id else null expandedTopicId = if (show) followableTopic.topic.id else null
}, },
onFollowClick = { }, // ToDo onFollowClick = { }, // ToDo
onUnfollowClick = { }, // ToDo onUnfollowClick = { }, // ToDo
onBrowseClick = { }, // ToDo onBrowseClick = { }, // ToDo
text = { Text(text = topic.name.uppercase(Locale.getDefault())) } text = {
val contentDescription = if (followableTopic.isFollowed) {
stringResource(
R.string.topic_chip_content_description_when_followed,
followableTopic.topic.name
)
} else {
stringResource(
R.string.topic_chip_content_description_when_not_followed,
followableTopic.topic.name
)
}
Text(
text = followableTopic.topic.name.uppercase(Locale.getDefault()),
modifier = Modifier.semantics {
this.contentDescription = contentDescription
}
)
}
) )
} }
} }
@ -281,7 +301,7 @@ private fun ExpandedNewsResourcePreview() {
NiaTheme { NiaTheme {
Surface { Surface {
NewsResourceCardExpanded( NewsResourceCardExpanded(
newsResource = previewNewsResources[0], userNewsResource = previewUserNewsResources[0],
isBookmarked = true, isBookmarked = true,
onToggleBookmark = {}, onToggleBookmark = {},
onClick = {} onClick = {}

@ -23,42 +23,37 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
/** /**
* Extension function for displaying a [List] of [NewsResourceCardExpanded] backed by a generic * Extension function for displaying a [List] of [NewsResourceCardExpanded] backed by a list of
* [List] [T]. * [UserNewsResource]s.
* *
* [newsResourceMapper] maps type [T] to a [NewsResource]
* [isBookmarkedMapper] maps type [T] to whether the [NewsResource] is bookmarked
* [onToggleBookmark] defines the action invoked when a user wishes to bookmark an item * [onToggleBookmark] defines the action invoked when a user wishes to bookmark an item
* [onItemClick] optional parameter for action to be performed when the card is clicked. The * [onItemClick] optional parameter for action to be performed when the card is clicked. The
* default action launches an intent matching the card. * default action launches an intent matching the card.
*/ */
fun <T> LazyListScope.newsResourceCardItems( fun LazyListScope.userNewsResourceCardItems(
items: List<T>, items: List<UserNewsResource>,
newsResourceMapper: (item: T) -> NewsResource, onToggleBookmark: (item: UserNewsResource) -> Unit,
isBookmarkedMapper: (item: T) -> Boolean, onItemClick: ((item: UserNewsResource) -> Unit)? = null,
onToggleBookmark: (item: T) -> Unit,
onItemClick: ((item: T) -> Unit)? = null,
itemModifier: Modifier = Modifier, itemModifier: Modifier = Modifier,
) = items( ) = items(
items = items, items = items,
key = { newsResourceMapper(it).id }, key = { it.id },
itemContent = { item -> itemContent = { userNewsResource ->
val newsResource = newsResourceMapper(item) val resourceUrl = Uri.parse(userNewsResource.url)
val resourceUrl = Uri.parse(newsResource.url)
val backgroundColor = MaterialTheme.colorScheme.background.toArgb() val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
val context = LocalContext.current val context = LocalContext.current
NewsResourceCardExpanded( NewsResourceCardExpanded(
newsResource = newsResource, userNewsResource = userNewsResource,
isBookmarked = isBookmarkedMapper(item), isBookmarked = userNewsResource.isSaved,
onToggleBookmark = { onToggleBookmark(item) }, onToggleBookmark = { onToggleBookmark(userNewsResource) },
onClick = { onClick = {
when (onItemClick) { when (onItemClick) {
null -> launchCustomChromeTab(context, resourceUrl, backgroundColor) null -> launchCustomChromeTab(context, resourceUrl, backgroundColor)
else -> onItemClick(item) else -> onItemClick(userNewsResource)
} }
}, },
modifier = itemModifier modifier = itemModifier

@ -21,4 +21,7 @@
<string name="card_tap_action">Open Resource Link</string> <string name="card_tap_action">Open Resource Link</string>
<string name="card_meta_data_text">%1$s • %2$s</string> <string name="card_meta_data_text">%1$s • %2$s</string>
<string name="topic_chip_content_description_when_followed">%1$s is followed</string>
<string name="topic_chip_content_description_when_not_followed">%1$s is not followed</string>
</resources> </resources>

@ -30,8 +30,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performScrollToNode
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -67,8 +66,7 @@ class BookmarksScreenTest {
composeTestRule.setContent { composeTestRule.setContent {
BookmarksScreen( BookmarksScreen(
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
previewNewsResources.take(2) previewUserNewsResources.take(2)
.map { SaveableNewsResource(it, true) }
), ),
removeFromBookmarks = { } removeFromBookmarks = { }
) )
@ -76,7 +74,7 @@ class BookmarksScreenTest {
composeTestRule composeTestRule
.onNodeWithText( .onNodeWithText(
previewNewsResources[0].title, previewUserNewsResources[0].title,
substring = true substring = true
) )
.assertExists() .assertExists()
@ -85,14 +83,14 @@ class BookmarksScreenTest {
composeTestRule.onNode(hasScrollToNodeAction()) composeTestRule.onNode(hasScrollToNodeAction())
.performScrollToNode( .performScrollToNode(
hasText( hasText(
previewNewsResources[1].title, previewUserNewsResources[1].title,
substring = true substring = true
) )
) )
composeTestRule composeTestRule
.onNodeWithText( .onNodeWithText(
previewNewsResources[1].title, previewUserNewsResources[1].title,
substring = true substring = true
) )
.assertExists() .assertExists()
@ -106,11 +104,10 @@ class BookmarksScreenTest {
composeTestRule.setContent { composeTestRule.setContent {
BookmarksScreen( BookmarksScreen(
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
previewNewsResources.take(2) previewUserNewsResources.take(2)
.map { SaveableNewsResource(it, true) }
), ),
removeFromBookmarks = { newsResourceId -> removeFromBookmarks = { newsResourceId ->
assertEquals(previewNewsResources[0].id, newsResourceId) assertEquals(previewUserNewsResources[0].id, newsResourceId)
removeFromBookmarksCalled = true removeFromBookmarksCalled = true
} }
) )
@ -124,7 +121,7 @@ class BookmarksScreenTest {
).filter( ).filter(
hasAnyAncestor( hasAnyAncestor(
hasText( hasText(
previewNewsResources[0].title, previewUserNewsResources[0].title,
substring = true substring = true
) )
) )

@ -52,8 +52,7 @@ import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel 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.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
@ -184,9 +183,7 @@ private fun BookmarksGridPreview() {
NiaTheme { NiaTheme {
BookmarksGrid( BookmarksGrid(
feedState = Success( feedState = Success(
previewNewsResources.map { previewUserNewsResources
SaveableNewsResource(it, false)
}
), ),
removeFromBookmarks = {} removeFromBookmarks = {}
) )

@ -19,8 +19,8 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -36,13 +36,13 @@ import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class BookmarksViewModel @Inject constructor( class BookmarksViewModel @Inject constructor(
private val userDataRepository: UserDataRepository, private val userDataRepository: UserDataRepository,
getSaveableNewsResources: GetSaveableNewsResourcesUseCase getSaveableNewsResources: GetUserNewsResourcesUseCase
) : ViewModel() { ) : ViewModel() {
val feedUiState: StateFlow<NewsFeedUiState> = getSaveableNewsResources() val feedUiState: StateFlow<NewsFeedUiState> = getSaveableNewsResources()
.filterNot { it.isEmpty() } .filterNot { it.isEmpty() }
.map { newsResources -> newsResources.filter(SaveableNewsResource::isSaved) } // Only show bookmarked news resources. .map { newsResources -> newsResources.filter(UserNewsResource::isSaved) } // Only show bookmarked news resources.
.map<List<SaveableNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success) .map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart { emit(Loading) } .onStart { emit(Loading) }
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,

@ -16,7 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.bookmarks package com.google.samples.apps.nowinandroid.feature.bookmarks
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources 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.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
@ -43,7 +43,7 @@ class BookmarksViewModelTest {
private val userDataRepository = TestUserDataRepository() private val userDataRepository = TestUserDataRepository()
private val newsRepository = TestNewsRepository() private val newsRepository = TestNewsRepository()
private val getSaveableNewsResourcesUseCase = GetSaveableNewsResourcesUseCase( private val getUserNewsResourcesUseCase = GetUserNewsResourcesUseCase(
newsRepository = newsRepository, newsRepository = newsRepository,
userDataRepository = userDataRepository userDataRepository = userDataRepository
) )
@ -53,7 +53,7 @@ class BookmarksViewModelTest {
fun setup() { fun setup() {
viewModel = BookmarksViewModel( viewModel = BookmarksViewModel(
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
getSaveableNewsResources = getSaveableNewsResourcesUseCase getSaveableNewsResources = getUserNewsResourcesUseCase
) )
} }

@ -29,9 +29,8 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performScrollToNode
import com.google.samples.apps.nowinandroid.core.domain.model.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.domain.model.previewUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.Topic 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 import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -225,9 +224,7 @@ class ForYouScreenTest {
isSyncing = false, isSyncing = false,
onboardingUiState = OnboardingUiState.NotShown, onboardingUiState = OnboardingUiState.NotShown,
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
feed = previewNewsResources.map { feed = previewUserNewsResources
SaveableNewsResource(it, false)
}
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},
@ -237,7 +234,7 @@ class ForYouScreenTest {
composeTestRule composeTestRule
.onNodeWithText( .onNodeWithText(
previewNewsResources[0].title, previewUserNewsResources[0].title,
substring = true substring = true
) )
.assertExists() .assertExists()
@ -246,14 +243,14 @@ class ForYouScreenTest {
composeTestRule.onNode(hasScrollToNodeAction()) composeTestRule.onNode(hasScrollToNodeAction())
.performScrollToNode( .performScrollToNode(
hasText( hasText(
previewNewsResources[1].title, previewUserNewsResources[1].title,
substring = true substring = true
) )
) )
composeTestRule composeTestRule
.onNodeWithText( .onNodeWithText(
previewNewsResources[1].title, previewUserNewsResources[1].title,
substring = true substring = true
) )
.assertExists() .assertExists()

@ -83,8 +83,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverl
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons 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.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.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.domain.model.previewUserNewsResources
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.model.data.previewTopics
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
@ -396,9 +395,7 @@ fun ForYouScreenPopulatedFeed() {
isSyncing = false, isSyncing = false,
onboardingUiState = OnboardingUiState.NotShown, onboardingUiState = OnboardingUiState.NotShown,
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
feed = previewNewsResources.map { feed = previewUserNewsResources
SaveableNewsResource(it, false)
}
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},
@ -417,9 +414,7 @@ fun ForYouScreenOfflinePopulatedFeed() {
isSyncing = false, isSyncing = false,
onboardingUiState = OnboardingUiState.NotShown, onboardingUiState = OnboardingUiState.NotShown,
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
feed = previewNewsResources.map { feed = previewUserNewsResources
SaveableNewsResource(it, false)
}
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},
@ -440,9 +435,7 @@ fun ForYouScreenTopicSelection() {
topics = previewTopics.map { FollowableTopic(it, false) }, topics = previewTopics.map { FollowableTopic(it, false) },
), ),
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
feed = previewNewsResources.map { feed = previewUserNewsResources
SaveableNewsResource(it, false)
}
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},
@ -478,9 +471,7 @@ fun ForYouScreenPopulatedAndLoading() {
isSyncing = true, isSyncing = true,
onboardingUiState = OnboardingUiState.Loading, onboardingUiState = OnboardingUiState.Loading,
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
feed = previewNewsResources.map { feed = previewUserNewsResources
SaveableNewsResource(it, false)
}
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},

@ -21,8 +21,8 @@ import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@ -41,7 +41,7 @@ import kotlinx.coroutines.launch
class ForYouViewModel @Inject constructor( class ForYouViewModel @Inject constructor(
syncStatusMonitor: SyncStatusMonitor, syncStatusMonitor: SyncStatusMonitor,
private val userDataRepository: UserDataRepository, private val userDataRepository: UserDataRepository,
private val getSaveableNewsResources: GetSaveableNewsResourcesUseCase, private val getSaveableNewsResources: GetUserNewsResourcesUseCase,
getFollowableTopics: GetFollowableTopicsUseCase getFollowableTopics: GetFollowableTopicsUseCase
) : ViewModel() { ) : ViewModel() {
@ -117,6 +117,6 @@ class ForYouViewModel @Inject constructor(
} }
} }
private fun Flow<List<SaveableNewsResource>>.mapToFeedState(): Flow<NewsFeedUiState> = private fun Flow<List<UserNewsResource>>.mapToFeedState(): Flow<NewsFeedUiState> =
map<List<SaveableNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success) map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart { emit(NewsFeedUiState.Loading) } .onStart { emit(NewsFeedUiState.Loading) }

@ -17,15 +17,17 @@
package com.google.samples.apps.nowinandroid.feature.foryou package com.google.samples.apps.nowinandroid.feature.foryou
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video 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.model.data.Topic
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository 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.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncStatusMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncStatusMonitor
@ -54,7 +56,7 @@ class ForYouViewModelTest {
private val userDataRepository = TestUserDataRepository() private val userDataRepository = TestUserDataRepository()
private val topicsRepository = TestTopicsRepository() private val topicsRepository = TestTopicsRepository()
private val newsRepository = TestNewsRepository() private val newsRepository = TestNewsRepository()
private val getSaveableNewsResourcesUseCase = GetSaveableNewsResourcesUseCase( private val getUserNewsResourcesUseCase = GetUserNewsResourcesUseCase(
newsRepository = newsRepository, newsRepository = newsRepository,
userDataRepository = userDataRepository userDataRepository = userDataRepository
) )
@ -69,7 +71,7 @@ class ForYouViewModelTest {
viewModel = ForYouViewModel( viewModel = ForYouViewModel(
syncStatusMonitor = syncStatusMonitor, syncStatusMonitor = syncStatusMonitor,
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
getSaveableNewsResources = getSaveableNewsResourcesUseCase, getSaveableNewsResources = getUserNewsResourcesUseCase,
getFollowableTopics = getFollowableTopicsUseCase getFollowableTopics = getFollowableTopicsUseCase
) )
} }
@ -263,7 +265,10 @@ class ForYouViewModelTest {
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
userDataRepository.setFollowedTopicIds(setOf("0", "1"))
val followedTopicIds = setOf("0", "1")
val userData = emptyUserData.copy(followedTopics = followedTopicIds)
userDataRepository.setUserData(userData)
viewModel.dismissOnboarding() viewModel.dismissOnboarding()
assertEquals( assertEquals(
@ -280,13 +285,7 @@ class ForYouViewModelTest {
) )
assertEquals( assertEquals(
NewsFeedUiState.Success( NewsFeedUiState.Success(
feed = feed = sampleNewsResources.mapToUserNewsResources(userData)
sampleNewsResources.map {
SaveableNewsResource(
newsResource = it,
isSaved = false
)
}
), ),
viewModel.feedState.value viewModel.feedState.value
) )
@ -307,41 +306,9 @@ class ForYouViewModelTest {
assertEquals( assertEquals(
OnboardingUiState.Shown( OnboardingUiState.Shown(
topics = listOf( topics = sampleTopics.map {
FollowableTopic( FollowableTopic(it, false)
topic = Topic( }
id = "0",
name = "Headlines",
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
),
FollowableTopic(
topic = Topic(
id = "1",
name = "UI",
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
),
FollowableTopic(
topic = Topic(
id = "2",
name = "Tools",
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
)
),
), ),
viewModel.onboardingUiState.value viewModel.onboardingUiState.value
) )
@ -352,59 +319,25 @@ class ForYouViewModelTest {
viewModel.feedState.value viewModel.feedState.value
) )
viewModel.updateTopicSelection("1", isChecked = true) val followedTopicId = sampleTopics[1].id
viewModel.updateTopicSelection(followedTopicId, isChecked = true)
assertEquals( assertEquals(
OnboardingUiState.Shown( OnboardingUiState.Shown(
topics = listOf( topics = sampleTopics.map {
FollowableTopic( FollowableTopic(it, it.id == followedTopicId)
topic = Topic( }
id = "0",
name = "Headlines",
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
),
FollowableTopic(
topic = Topic(
id = "1",
name = "UI",
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = true
),
FollowableTopic(
topic = Topic(
id = "2",
name = "Tools",
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
)
),
), ),
viewModel.onboardingUiState.value viewModel.onboardingUiState.value
) )
val userData = emptyUserData.copy(followedTopics = setOf(followedTopicId))
assertEquals( assertEquals(
NewsFeedUiState.Success( NewsFeedUiState.Success(
feed = listOf( feed = listOf(
SaveableNewsResource( UserNewsResource(sampleNewsResources[1], userData),
newsResource = sampleNewsResources[1], UserNewsResource(sampleNewsResources[2], userData),
isSaved = false
),
SaveableNewsResource(
newsResource = sampleNewsResources[2],
isSaved = false
)
) )
), ),
viewModel.feedState.value viewModel.feedState.value
@ -484,11 +417,25 @@ class ForYouViewModelTest {
launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() } launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() }
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
val followedTopicIds = setOf("1")
val userData = emptyUserData.copy(
followedTopics = followedTopicIds,
shouldHideOnboarding = true
)
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
userDataRepository.setFollowedTopicIds(setOf("1")) userDataRepository.setUserData(userData)
userDataRepository.setShouldHideOnboarding(true)
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateNewsResourceSaved("2", true)
val bookmarkedNewsResourceId = "2"
viewModel.updateNewsResourceSaved(
newsResourceId = bookmarkedNewsResourceId,
isChecked = true
)
val userDataExpected = userData.copy(
bookmarkedNewsResources = setOf(bookmarkedNewsResourceId)
)
assertEquals( assertEquals(
OnboardingUiState.NotShown, OnboardingUiState.NotShown,
@ -497,14 +444,8 @@ class ForYouViewModelTest {
assertEquals( assertEquals(
NewsFeedUiState.Success( NewsFeedUiState.Success(
feed = listOf( feed = listOf(
SaveableNewsResource( UserNewsResource(newsResource = sampleNewsResources[1], userDataExpected),
newsResource = sampleNewsResources[1], UserNewsResource(newsResource = sampleNewsResources[2], userDataExpected)
isSaved = true
),
SaveableNewsResource(
newsResource = sampleNewsResources[2],
isSaved = false
)
) )
), ),
viewModel.feedState.value viewModel.feedState.value

@ -25,10 +25,11 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performScrollToNode
import com.google.samples.apps.nowinandroid.core.domain.model.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.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource 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.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -99,14 +100,7 @@ class TopicScreenTest {
composeTestRule.setContent { composeTestRule.setContent {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Loading, topicUiState = TopicUiState.Loading,
newsUiState = NewsUiState.Success( newsUiState = NewsUiState.Success(sampleUserNewsResources),
sampleNewsResources.mapIndexed { index, newsResource ->
SaveableNewsResource(
newsResource = newsResource,
isSaved = index % 2 == 0,
)
}
),
onBackClick = { }, onBackClick = { },
onFollowClick = { }, onFollowClick = { },
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
@ -126,12 +120,7 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Success(testTopic), topicUiState = TopicUiState.Success(testTopic),
newsUiState = NewsUiState.Success( newsUiState = NewsUiState.Success(
sampleNewsResources.mapIndexed { index, newsResource -> sampleUserNewsResources
SaveableNewsResource(
newsResource = newsResource,
isSaved = index % 2 == 0,
)
}
), ),
onBackClick = { }, onBackClick = { },
onFollowClick = { }, onFollowClick = { },
@ -143,7 +132,7 @@ class TopicScreenTest {
composeTestRule composeTestRule
.onAllNodes(hasScrollToNodeAction()) .onAllNodes(hasScrollToNodeAction())
.onFirst() .onFirst()
.performScrollToNode(hasText(sampleNewsResources.first().title)) .performScrollToNode(hasText(sampleUserNewsResources.first().title))
} }
} }
@ -188,14 +177,16 @@ private val testTopics = listOf(
) )
) )
private val sampleNewsResources = listOf( private val sampleUserNewsResources = listOf(
UserNewsResource(
newsResource =
NewsResource( NewsResource(
id = "1", id = "1",
title = "Thanks for helping us reach 1M YouTube Subscribers", title = "Thanks for helping us reach 1M YouTube Subscribers",
content = "Thank you everyone for following the Now in Android series and everything the " + content = "Thank you everyone for following the Now in Android series and" +
"Android Developers YouTube channel has to offer. During the Android Developer " + " everything the Android Developers YouTube channel has to offer. During the " +
"Summit, our YouTube channel reached 1 million subscribers! Heres a small video to " + "Android Developer Summit, our YouTube channel reached 1 million subscribers!" +
"thank you all.", " Heres a small video to thank you all.",
url = "https://youtu.be/-fJ6poHQrjM", url = "https://youtu.be/-fJ6poHQrjM",
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg", headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
@ -210,5 +201,7 @@ private val sampleNewsResources = listOf(
imageUrl = "" imageUrl = ""
) )
) )
),
userData = emptyUserData.copy(bookmarkedNewsResources = setOf("1"))
) )
) )

@ -53,12 +53,11 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadi
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons 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.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.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.domain.model.previewUserNewsResources
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.model.data.previewTopics
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems import com.google.samples.apps.nowinandroid.core.ui.userNewsResourceCardItems
import com.google.samples.apps.nowinandroid.feature.topic.R.string import com.google.samples.apps.nowinandroid.feature.topic.R.string
import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading
@ -146,7 +145,7 @@ private fun LazyListScope.TopicBody(
TopicHeader(name, description, imageUrl) TopicHeader(name, description, imageUrl)
} }
TopicCards(news, onBookmarkChanged) userNewsResourceCards(news, onBookmarkChanged)
} }
@Composable @Composable
@ -174,17 +173,16 @@ private fun TopicHeader(name: String, description: String, imageUrl: String) {
} }
} }
private fun LazyListScope.TopicCards( // TODO: Could/should this be replaced with [LazyGridScope.newsFeed]?
private fun LazyListScope.userNewsResourceCards(
news: NewsUiState, news: NewsUiState,
onBookmarkChanged: (String, Boolean) -> Unit onBookmarkChanged: (String, Boolean) -> Unit
) { ) {
when (news) { when (news) {
is NewsUiState.Success -> { is NewsUiState.Success -> {
newsResourceCardItems( userNewsResourceCardItems(
items = news.news, items = news.news,
newsResourceMapper = { it.newsResource }, onToggleBookmark = { onBookmarkChanged(it.id, !it.isSaved) },
isBookmarkedMapper = { it.isSaved },
onToggleBookmark = { onBookmarkChanged(it.newsResource.id, !it.isSaved) },
itemModifier = Modifier.padding(24.dp) itemModifier = Modifier.padding(24.dp)
) )
} }
@ -257,12 +255,7 @@ fun TopicScreenPopulated() {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Success(FollowableTopic(previewTopics[0], false)), topicUiState = TopicUiState.Success(FollowableTopic(previewTopics[0], false)),
newsUiState = NewsUiState.Success( newsUiState = NewsUiState.Success(
previewNewsResources.mapIndexed { index, newsResource -> previewUserNewsResources
SaveableNewsResource(
newsResource = newsResource,
isSaved = index % 2 == 0,
)
}
), ),
onBackClick = {}, onBackClick = {},
onFollowClick = {}, onFollowClick = {},

@ -22,9 +22,9 @@ import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository 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.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic 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.Result
import com.google.samples.apps.nowinandroid.core.result.asResult import com.google.samples.apps.nowinandroid.core.result.asResult
@ -45,7 +45,7 @@ class TopicViewModel @Inject constructor(
stringDecoder: StringDecoder, stringDecoder: StringDecoder,
private val userDataRepository: UserDataRepository, private val userDataRepository: UserDataRepository,
topicsRepository: TopicsRepository, topicsRepository: TopicsRepository,
getSaveableNewsResources: GetSaveableNewsResourcesUseCase getSaveableNewsResources: GetUserNewsResourcesUseCase
) : ViewModel() { ) : ViewModel() {
private val topicArgs: TopicArgs = TopicArgs(savedStateHandle, stringDecoder) private val topicArgs: TopicArgs = TopicArgs(savedStateHandle, stringDecoder)
@ -130,11 +130,11 @@ private fun topicUiState(
private fun newsUiState( private fun newsUiState(
topicId: String, topicId: String,
getSaveableNewsResources: GetSaveableNewsResourcesUseCase, getSaveableNewsResources: GetUserNewsResourcesUseCase,
userDataRepository: UserDataRepository, userDataRepository: UserDataRepository,
): Flow<NewsUiState> { ): Flow<NewsUiState> {
// Observe news // Observe news
val newsStream: Flow<List<SaveableNewsResource>> = getSaveableNewsResources( val newsStream: Flow<List<UserNewsResource>> = getSaveableNewsResources(
filterTopicIds = setOf(element = topicId), filterTopicIds = setOf(element = topicId),
) )
@ -171,7 +171,7 @@ sealed interface TopicUiState {
} }
sealed interface NewsUiState { sealed interface NewsUiState {
data class Success(val news: List<SaveableNewsResource>) : NewsUiState data class Success(val news: List<UserNewsResource>) : NewsUiState
object Error : NewsUiState object Error : NewsUiState
object Loading : NewsUiState object Loading : NewsUiState
} }

@ -17,7 +17,7 @@
package com.google.samples.apps.nowinandroid.feature.topic package com.google.samples.apps.nowinandroid.feature.topic
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource 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.NewsResourceType.Video
@ -53,7 +53,7 @@ class TopicViewModelTest {
private val userDataRepository = TestUserDataRepository() private val userDataRepository = TestUserDataRepository()
private val topicsRepository = TestTopicsRepository() private val topicsRepository = TestTopicsRepository()
private val newsRepository = TestNewsRepository() private val newsRepository = TestNewsRepository()
private val getSaveableNewsResourcesUseCase = GetSaveableNewsResourcesUseCase( private val getUserNewsResourcesUseCase = GetUserNewsResourcesUseCase(
newsRepository = newsRepository, newsRepository = newsRepository,
userDataRepository = userDataRepository userDataRepository = userDataRepository
) )
@ -66,7 +66,7 @@ class TopicViewModelTest {
stringDecoder = FakeStringDecoder(), stringDecoder = FakeStringDecoder(),
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
topicsRepository = topicsRepository, topicsRepository = topicsRepository,
getSaveableNewsResources = getSaveableNewsResourcesUseCase getSaveableNewsResources = getUserNewsResourcesUseCase
) )
} }

Loading…
Cancel
Save