Add news resource cards to for you screen

Change-Id: Ia68950804e304ccd4a9f4ba498dd2cf383315fbd
pull/2/head
Alex Vanyo 4 years ago
parent 506c98cc90
commit 1dc478c441

@ -36,12 +36,10 @@ data class TopicEntity(
val id: Int, val id: Int,
val name: String, val name: String,
val description: String, val description: String,
val followed: Boolean,
) )
fun TopicEntity.asExternalModel() = Topic( fun TopicEntity.asExternalModel() = Topic(
id = id, id = id,
name = name, name = name,
description = description, description = description,
followed = followed,
) )

@ -23,5 +23,4 @@ fun NetworkTopic.asEntity() = TopicEntity(
id = id, id = id,
name = name, name = name,
description = description, description = description,
followed = followed
) )

@ -16,14 +16,21 @@
package com.google.samples.apps.nowinandroid.core.domain.repository.fake package com.google.samples.apps.nowinandroid.core.domain.repository.fake
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import com.google.samples.apps.nowinandroid.core.network.fake.FakeDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
/** /**
@ -38,10 +45,25 @@ class FakeNewsRepository @Inject constructor(
) : NewsRepository { ) : NewsRepository {
override fun getNewsResourcesStream(): Flow<List<NewsResource>> = override fun getNewsResourcesStream(): Flow<List<NewsResource>> =
flowOf(emptyList()) flow {
emit(
networkJson.decodeFromString<List<NetworkNewsResource>>(FakeDataSource.data)
.map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel)
)
}
.flowOn(ioDispatcher)
override fun getNewsResourcesStream(filterTopicIds: Set<Int>): Flow<List<NewsResource>> = override fun getNewsResourcesStream(filterTopicIds: Set<Int>): Flow<List<NewsResource>> =
flowOf(emptyList()) flow {
emit(
networkJson.decodeFromString<List<NetworkNewsResource>>(FakeDataSource.data)
.filter { it.topics.intersect(filterTopicIds).isNotEmpty() }
.map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel)
)
}
.flowOn(ioDispatcher)
override suspend fun sync() = true override suspend fun sync() = true
} }

@ -56,7 +56,6 @@ class PopulatedNewsResourceKtTest {
id = 3, id = 3,
name = "name", name = "name",
description = "description", description = "description",
followed = true,
) )
), ),
) )
@ -83,7 +82,6 @@ class PopulatedNewsResourceKtTest {
id = 3, id = 3,
name = "name", name = "name",
description = "description", description = "description",
followed = true,
) )
) )
), ),

@ -0,0 +1,25 @@
/*
* 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.model.data
/**
* A [topic] with the additional information for whether or not it is followed.
*/
data class FollowableTopic(
val topic: Topic,
val isFollowed: Boolean
)

@ -0,0 +1,25 @@
/*
* 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.model.data
/**
* A [NewsResource] with the additional information for whether it is saved.
*/
data class SaveableNewsResource(
val newsResource: NewsResource,
val isSaved: Boolean,
)

@ -23,5 +23,4 @@ data class Topic(
val id: Int, val id: Int,
val name: String, val name: String,
val description: String, val description: String,
val followed: Boolean = false
) )

@ -17,8 +17,8 @@
package com.google.samples.apps.nowinandroid.core.network.fake package com.google.samples.apps.nowinandroid.core.network.fake
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.network.model.NetworkTopic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
@ -153,8 +153,7 @@ object FakeDataSource {
@Language("JSON") @Language("JSON")
val data = """ val data = """
{ [
"resources": [
{ {
"id": 1, "id": 1,
"episodeId": 52, "episodeId": 52,
@ -1478,7 +1477,5 @@ object FakeDataSource {
] ]
} }
] ]
}
""".trimIndent() """.trimIndent()
} }

@ -24,7 +24,6 @@ import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -42,14 +41,6 @@ class FakeNiANetwork @Inject constructor(
override suspend fun getNewsResources(): List<NetworkNewsResource> = override suspend fun getNewsResources(): List<NetworkNewsResource> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
networkJson.decodeFromString<ResourceData>(FakeDataSource.data).resources networkJson.decodeFromString(FakeDataSource.data)
} }
} }
/**
* Representation of resources as fetched from [FakeDataSource]
*/
@Serializable
private data class ResourceData(
val resources: List<NetworkNewsResource>
)

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.core.testing.repository
import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -37,7 +38,7 @@ class TestNewsRepository : NewsRepository {
filterTopicIds: Set<Int> filterTopicIds: Set<Int>
): Flow<List<NewsResource>> = ): Flow<List<NewsResource>> =
getNewsResourcesStream().map { newsResources -> getNewsResourcesStream().map { newsResources ->
newsResources.filter { it.topics.intersect(filterTopicIds).isNotEmpty() } newsResources.filter { it.topics.map(Topic::id).intersect(filterTopicIds).isNotEmpty() }
} }
/** /**

@ -192,7 +192,6 @@ private val newsResource = NewsResource(
id = 1, id = 1,
name = "Name", name = "Name",
description = "Description", description = "Description",
followed = false
) )
) )
) )

@ -24,6 +24,7 @@ import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
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.model.data.Topic
import com.google.samples.apps.nowinandroid.feature.following.FollowingScreen import com.google.samples.apps.nowinandroid.feature.following.FollowingScreen
import com.google.samples.apps.nowinandroid.feature.following.FollowingUiState import com.google.samples.apps.nowinandroid.feature.following.FollowingUiState
@ -110,7 +111,7 @@ class FollowingScreenTest {
composeTestRule composeTestRule
.onAllNodesWithContentDescription(followingTopicCardUnfollowButton) .onAllNodesWithContentDescription(followingTopicCardUnfollowButton)
.assertCountEquals(testTopics.filter { it.followed }.size) .assertCountEquals(testTopics.filter { it.isFollowed }.size)
} }
@Test @Test
@ -135,22 +136,30 @@ private const val TOPIC_3_NAME = "Tools"
private const val TOPIC_DESC = "At vero eos et accusamus et iusto odio dignissimos ducimus qui." private const val TOPIC_DESC = "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
private val testTopics = listOf( private val testTopics = listOf(
FollowableTopic(
Topic( Topic(
id = 0, id = 0,
name = TOPIC_1_NAME, name = TOPIC_1_NAME,
description = TOPIC_DESC, description = TOPIC_DESC,
followed = true
), ),
isFollowed = true
),
FollowableTopic(
Topic( Topic(
id = 1, id = 1,
name = TOPIC_2_NAME, name = TOPIC_2_NAME,
description = TOPIC_DESC description = TOPIC_DESC
), ),
isFollowed = false
),
FollowableTopic(
Topic( Topic(
id = 2, id = 2,
name = TOPIC_3_NAME, name = TOPIC_3_NAME,
description = TOPIC_DESC description = TOPIC_DESC
),
isFollowed = false
) )
) )
private val numberOfUnfollowedTopics = testTopics.filter { !it.followed }.size private val numberOfUnfollowedTopics = testTopics.filter { !it.isFollowed }.size

@ -45,6 +45,7 @@ import androidx.compose.ui.res.stringResource
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 androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
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.model.data.Topic
import com.google.samples.apps.nowinandroid.core.ui.NiaLoadingIndicator import com.google.samples.apps.nowinandroid.core.ui.NiaLoadingIndicator
import com.google.samples.apps.nowinandroid.core.ui.NiaToolbar import com.google.samples.apps.nowinandroid.core.ui.NiaToolbar
@ -105,10 +106,10 @@ fun FollowingWithTopicsScreen(
LazyColumn( LazyColumn(
modifier = modifier modifier = modifier
) { ) {
uiState.topics.forEach { uiState.topics.forEach { followableTopic ->
item { item {
FollowingTopicCard( FollowingTopicCard(
topic = it, followableTopic = followableTopic,
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
onFollowButtonClick = onFollowButtonClick onFollowButtonClick = onFollowButtonClick
) )
@ -124,7 +125,7 @@ fun FollowingErrorScreen() {
@Composable @Composable
fun FollowingTopicCard( fun FollowingTopicCard(
topic: Topic, followableTopic: FollowableTopic,
onTopicClick: () -> Unit, onTopicClick: () -> Unit,
onFollowButtonClick: (Int, Boolean) -> Unit, onFollowButtonClick: (Int, Boolean) -> Unit,
) { ) {
@ -147,13 +148,13 @@ fun FollowingTopicCard(
.weight(1f) .weight(1f)
.clickable { onTopicClick() } .clickable { onTopicClick() }
) { ) {
TopicTitle(topicName = topic.name) TopicTitle(topicName = followableTopic.topic.name)
TopicDescription(topicDescription = topic.description) TopicDescription(topicDescription = followableTopic.topic.description)
} }
FollowButton( FollowButton(
topicId = topic.id, topicId = followableTopic.topic.id,
onClick = onFollowButtonClick, onClick = onFollowButtonClick,
isFollowed = topic.followed isFollowed = followableTopic.isFollowed
) )
} }
} }
@ -244,11 +245,14 @@ fun TopicCardPreview() {
NiaTheme { NiaTheme {
Surface { Surface {
FollowingTopicCard( FollowingTopicCard(
FollowableTopic(
Topic( Topic(
id = 0, id = 0,
name = "Compose", name = "Compose",
description = "Description" description = "Description"
), ),
isFollowed = false
),
onTopicClick = {}, onTopicClick = {},
onFollowButtonClick = { _, _ -> } onFollowButtonClick = { _, _ -> }
) )

@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.feature.following
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository
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.model.data.Topic
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@ -39,10 +40,10 @@ class FollowingViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private val followedTopicIdsStream = topicsRepository.getFollowedTopicIdsStream() private val followedTopicIdsStream = topicsRepository.getFollowedTopicIdsStream()
.catch { FollowingState.Error } .map<Set<Int>, FollowingState> { followedTopics ->
.map { followedTopics ->
FollowingState.Topics(topics = followedTopics) FollowingState.Topics(topics = followedTopics)
} }
.catch { emit(FollowingState.Error) }
val uiState: StateFlow<FollowingUiState> = combine( val uiState: StateFlow<FollowingUiState> = combine(
followedTopicIdsStream, followedTopicIdsStream,
@ -70,15 +71,14 @@ class FollowingViewModel @Inject constructor(
private fun mapFollowedAndUnfollowedTopics(topics: List<Topic>): Flow<FollowingUiState.Topics> = private fun mapFollowedAndUnfollowedTopics(topics: List<Topic>): Flow<FollowingUiState.Topics> =
topicsRepository.getFollowedTopicIdsStream().map { followedTopicIds -> topicsRepository.getFollowedTopicIdsStream().map { followedTopicIds ->
FollowingUiState.Topics( FollowingUiState.Topics(
topics = topics = topics
topics.map { .map { topic ->
Topic( FollowableTopic(
it.id, topic = topic,
it.name, isFollowed = topic.id in followedTopicIds,
it.description,
followedTopicIds.contains(it.id)
) )
}.sortedBy { it.name } }
.sortedBy { it.topic.name }
) )
} }
} }
@ -90,6 +90,6 @@ private sealed interface FollowingState {
sealed interface FollowingUiState { sealed interface FollowingUiState {
object Loading : FollowingUiState object Loading : FollowingUiState
data class Topics(val topics: List<Topic>) : FollowingUiState data class Topics(val topics: List<FollowableTopic>) : FollowingUiState
object Error : FollowingUiState object Error : FollowingUiState
} }

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.following package com.google.samples.apps.nowinandroid.following
import app.cash.turbine.test import app.cash.turbine.test
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.model.data.Topic
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.util.TestDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule
@ -63,13 +64,13 @@ class FollowingViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
awaitItem() awaitItem()
topicsRepository.sendTopics(testInputTopics) topicsRepository.sendTopics(testInputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[0].id)) topicsRepository.setFollowedTopicIds(setOf(testInputTopics[0].topic.id))
awaitItem() awaitItem()
viewModel.followTopic( viewModel.followTopic(
followedTopicId = testInputTopics[1].id, followedTopicId = testInputTopics[1].topic.id,
followed = !testInputTopics[1].followed followed = true
) )
assertEquals( assertEquals(
@ -85,19 +86,21 @@ class FollowingViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
awaitItem() awaitItem()
topicsRepository.sendTopics(testOutputTopics) topicsRepository.sendTopics(testOutputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds( topicsRepository.setFollowedTopicIds(
setOf(testOutputTopics[0].id, testOutputTopics[1].id) setOf(testOutputTopics[0].topic.id, testOutputTopics[1].topic.id)
) )
awaitItem() awaitItem()
viewModel.followTopic( viewModel.followTopic(
followedTopicId = testOutputTopics[1].id, followedTopicId = testOutputTopics[1].topic.id,
followed = !testOutputTopics[1].followed followed = false
) )
assertEquals( assertEquals(
FollowingUiState.Topics(topics = testInputTopics), FollowingUiState.Topics(
topics = testInputTopics
),
awaitItem() awaitItem()
) )
cancel() cancel()
@ -111,41 +114,55 @@ private const val TOPIC_3_NAME = "Compose"
private const val TOPIC_DESC = "At vero eos et accusamus et iusto odio dignissimos ducimus qui." private const val TOPIC_DESC = "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
private val testInputTopics = listOf( private val testInputTopics = listOf(
FollowableTopic(
Topic( Topic(
id = 0, id = 0,
name = TOPIC_1_NAME, name = TOPIC_1_NAME,
description = TOPIC_DESC, description = TOPIC_DESC,
followed = true
), ),
isFollowed = true
),
FollowableTopic(
Topic( Topic(
id = 1, id = 1,
name = TOPIC_2_NAME, name = TOPIC_2_NAME,
description = TOPIC_DESC description = TOPIC_DESC
), ),
isFollowed = false
),
FollowableTopic(
Topic( Topic(
id = 2, id = 2,
name = TOPIC_3_NAME, name = TOPIC_3_NAME,
description = TOPIC_DESC description = TOPIC_DESC
),
isFollowed = false
) )
) )
private val testOutputTopics = listOf( private val testOutputTopics = listOf(
FollowableTopic(
Topic( Topic(
id = 0, id = 0,
name = TOPIC_1_NAME, name = TOPIC_1_NAME,
description = TOPIC_DESC, description = TOPIC_DESC,
followed = true
), ),
isFollowed = true
),
FollowableTopic(
Topic( Topic(
id = 1, id = 1,
name = TOPIC_2_NAME, name = TOPIC_2_NAME,
description = TOPIC_DESC, description = TOPIC_DESC
followed = true ),
isFollowed = true
), ),
FollowableTopic(
Topic( Topic(
id = 2, id = 2,
name = TOPIC_3_NAME, name = TOPIC_3_NAME,
description = TOPIC_DESC, description = TOPIC_DESC
followed = false, ),
isFollowed = false
) )
) )

@ -53,6 +53,7 @@ dependencies {
androidTestImplementation project(':core-testing') androidTestImplementation project(':core-testing')
implementation libs.kotlinx.coroutines.android implementation libs.kotlinx.coroutines.android
implementation libs.kotlinx.datetime
implementation libs.androidx.hilt.navigation.compose implementation libs.androidx.hilt.navigation.compose
implementation libs.androidx.lifecycle.viewModelCompose implementation libs.androidx.lifecycle.viewModelCompose

@ -26,6 +26,7 @@ import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
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.model.data.Topic
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -40,7 +41,8 @@ class ForYouScreenTest {
ForYouScreen( ForYouScreen(
uiState = ForYouFeedUiState.Loading, uiState = ForYouFeedUiState.Loading,
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {} saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }
) )
} }
@ -56,27 +58,37 @@ class ForYouScreenTest {
composeTestRule.setContent { composeTestRule.setContent {
ForYouScreen( ForYouScreen(
uiState = ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection( uiState = ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection(
selectedTopics = listOf( topics = listOf(
Topic( FollowableTopic(
topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" description = ""
) to false, ),
Topic( isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" description = ""
) to false, ),
Topic( isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "" description = "",
) to false ),
isFollowed = false
),
), ),
feed = emptyList() feed = emptyList()
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {} saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }
) )
} }
@ -110,27 +122,37 @@ class ForYouScreenTest {
composeTestRule.setContent { composeTestRule.setContent {
ForYouScreen( ForYouScreen(
uiState = ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection( uiState = ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection(
selectedTopics = listOf( topics = listOf(
Topic( FollowableTopic(
topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" description = ""
) to false, ),
Topic( isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" description = ""
) to true, ),
Topic( isFollowed = true
),
FollowableTopic(
topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "" description = "",
) to false ),
isFollowed = false
),
), ),
feed = emptyList() feed = emptyList()
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {} saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }
) )
} }

@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -39,8 +40,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
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.model.data.Topic
import com.google.samples.apps.nowinandroid.core.ui.NewsResourceCardExpanded
import com.google.samples.apps.nowinandroid.core.ui.NiaLoadingIndicator import com.google.samples.apps.nowinandroid.core.ui.NiaLoadingIndicator
import kotlinx.datetime.Instant
@Composable @Composable
fun ForYouRoute( fun ForYouRoute(
@ -53,7 +60,8 @@ fun ForYouRoute(
modifier = modifier, modifier = modifier,
uiState = uiState, uiState = uiState,
onTopicCheckedChanged = viewModel::updateTopicSelection, onTopicCheckedChanged = viewModel::updateTopicSelection,
saveFollowedTopics = viewModel::saveFollowedTopics saveFollowedTopics = viewModel::saveFollowedTopics,
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved
) )
} }
@ -62,6 +70,7 @@ fun ForYouScreen(
uiState: ForYouFeedUiState, uiState: ForYouFeedUiState,
onTopicCheckedChanged: (Int, Boolean) -> Unit, onTopicCheckedChanged: (Int, Boolean) -> Unit,
saveFollowedTopics: () -> Unit, saveFollowedTopics: () -> Unit,
onNewsResourcesCheckedChanged: (Int, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
@ -81,9 +90,15 @@ fun ForYouScreen(
is ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection -> Unit is ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection -> Unit
} }
// items(uiState.feed) { _: NewsResource -> items(uiState.feed) { (newsResource: NewsResource, isBookmarked: Boolean) ->
// // TODO: News item NewsResourceCardExpanded(
// } newsResource = newsResource,
isBookmarked = isBookmarked,
onToggleBookmark = {
onNewsResourcesCheckedChanged(newsResource.id, !isBookmarked)
}
)
}
} }
} }
} }
@ -104,7 +119,7 @@ private fun LazyListScope.TopicSelection(
crossAxisSpacing = 8.dp, crossAxisSpacing = 8.dp,
modifier = Modifier.padding(horizontal = 40.dp) modifier = Modifier.padding(horizontal = 40.dp)
) { ) {
uiState.selectedTopics.forEach { (topic, isSelected) -> uiState.topics.forEach { (topic, isSelected) ->
key(topic.id) { key(topic.id) {
// TODO: Add toggleable semantics // TODO: Add toggleable semantics
OutlinedButton( OutlinedButton(
@ -149,7 +164,8 @@ fun ForYouScreenLoading() {
ForYouScreen( ForYouScreen(
uiState = ForYouFeedUiState.Loading, uiState = ForYouFeedUiState.Loading,
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {} saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }
) )
} }
@ -158,27 +174,104 @@ fun ForYouScreenLoading() {
fun ForYouScreenTopicSelection() { fun ForYouScreenTopicSelection() {
ForYouScreen( ForYouScreen(
uiState = ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection( uiState = ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection(
selectedTopics = listOf( topics = listOf(
FollowableTopic(
topic = Topic(
id = 0,
name = "Headlines",
description = ""
),
isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 1,
name = "UI",
description = ""
),
isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 2,
name = "Tools",
description = "",
),
isFollowed = false
),
),
feed = listOf(
SaveableNewsResource(
newsResource = NewsResource(
id = 1,
episodeId = 52,
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",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video,
topics = listOf(
Topic( Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" description = ""
) to false, )
),
authors = emptyList()
),
isSaved = false
),
SaveableNewsResource(
newsResource = NewsResource(
id = 2,
episodeId = 52,
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",
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = Video,
topics = listOf(
Topic( Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" description = ""
) to true, ),
),
authors = emptyList()
),
isSaved = false
),
SaveableNewsResource(
newsResource = NewsResource(
id = 3,
episodeId = 52,
title = "Community tip on Paging",
content = "Tips for using the Paging library from the developer community",
url = "https://youtu.be/r5JgIyS3t3s",
publishDate = Instant.parse("2021-11-08T00:00:00.000Z"),
type = Video,
topics = listOf(
Topic( Topic(
id = 2, id = 1,
name = "Tools", name = "UI",
description = "" description = ""
) to false
), ),
feed = emptyList() ),
authors = emptyList()
),
isSaved = false
),
)
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {} saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }
) )
} }
@ -190,6 +283,7 @@ fun PopulatedFeed() {
feed = emptyList() feed = emptyList()
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {} saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }
) )
} }

@ -24,8 +24,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository
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.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource
import com.google.samples.apps.nowinandroid.feature.foryou.util.saveable import com.google.samples.apps.nowinandroid.feature.foryou.util.saveable
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@ -51,7 +52,7 @@ class ForYouViewModel @Inject constructor(
FollowedTopicsState.None FollowedTopicsState.None
} else { } else {
FollowedTopicsState.FollowedTopics( FollowedTopicsState.FollowedTopics(
topics = followedTopics topicIds = followedTopics
) )
} }
} }
@ -61,6 +62,16 @@ class ForYouViewModel @Inject constructor(
initialValue = FollowedTopicsState.Unknown initialValue = FollowedTopicsState.Unknown
) )
/**
* TODO: Temporary saving of news resources persisted through process death with a
* [SavedStateHandle].
*
* This should be persisted to disk instead.
*/
private var savedNewsResources by savedStateHandle.saveable {
mutableStateOf<Set<Int>>(emptySet())
}
/** /**
* The in-progress set of topics to be selected, persisted through process death with a * The in-progress set of topics to be selected, persisted through process death with a
* [SavedStateHandle]. * [SavedStateHandle].
@ -73,15 +84,26 @@ class ForYouViewModel @Inject constructor(
followedTopicsStateFlow, followedTopicsStateFlow,
topicsRepository.getTopicsStream(), topicsRepository.getTopicsStream(),
snapshotFlow { inProgressTopicSelection }, snapshotFlow { inProgressTopicSelection },
) { followedTopicsUserState, availableTopics, inProgressTopicSelection -> snapshotFlow { savedNewsResources }
) { followedTopicsUserState, availableTopics, inProgressTopicSelection, savedNewsResources ->
fun mapToSaveableFeed(feed: List<NewsResource>): List<SaveableNewsResource> =
feed.map { newsResource ->
SaveableNewsResource(
newsResource = newsResource,
isSaved = savedNewsResources.contains(newsResource.id)
)
}
when (followedTopicsUserState) { when (followedTopicsUserState) {
// If we don't know the current selection state, just emit loading. // If we don't know the current selection state, just emit loading.
FollowedTopicsState.Unknown -> flowOf<ForYouFeedUiState>(ForYouFeedUiState.Loading) FollowedTopicsState.Unknown -> flowOf<ForYouFeedUiState>(ForYouFeedUiState.Loading)
// If the user has followed topics, use those followed topics to populate the feed // If the user has followed topics, use those followed topics to populate the feed
is FollowedTopicsState.FollowedTopics -> { is FollowedTopicsState.FollowedTopics -> {
newsRepository.getNewsResourcesStream( newsRepository.getNewsResourcesStream(
filterTopicIds = followedTopicsUserState.topics filterTopicIds = followedTopicsUserState.topicIds
) )
.map(::mapToSaveableFeed)
.map { feed -> .map { feed ->
ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection(
feed = feed feed = feed
@ -94,10 +116,14 @@ class ForYouViewModel @Inject constructor(
newsRepository.getNewsResourcesStream( newsRepository.getNewsResourcesStream(
filterTopicIds = inProgressTopicSelection filterTopicIds = inProgressTopicSelection
) )
.map(::mapToSaveableFeed)
.map { feed -> .map { feed ->
ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection( ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection(
selectedTopics = availableTopics.map { topic -> topics = availableTopics.map { topic ->
topic to (topic.id in inProgressTopicSelection) FollowableTopic(
topic = topic,
isFollowed = topic.id in inProgressTopicSelection
)
}, },
feed = feed feed = feed
) )
@ -127,6 +153,17 @@ class ForYouViewModel @Inject constructor(
} }
} }
fun updateNewsResourceSaved(newsResourceId: Int, isChecked: Boolean) {
withMutableSnapshot {
savedNewsResources =
if (isChecked) {
savedNewsResources + newsResourceId
} else {
savedNewsResources - newsResourceId
}
}
}
fun saveFollowedTopics() { fun saveFollowedTopics() {
if (inProgressTopicSelection.isEmpty()) return if (inProgressTopicSelection.isEmpty()) return
@ -152,10 +189,10 @@ private sealed interface FollowedTopicsState {
object None : FollowedTopicsState object None : FollowedTopicsState
/** /**
* The user has followed the given (non-empty) set of [topics]. * The user has followed the given (non-empty) set of [topicIds].
*/ */
data class FollowedTopics( data class FollowedTopics(
val topics: Set<Int>, val topicIds: Set<Int>,
) : FollowedTopicsState ) : FollowedTopicsState
} }
@ -177,23 +214,23 @@ sealed interface ForYouFeedUiState {
/** /**
* The list of news resources contained in this [PopulatedFeed]. * The list of news resources contained in this [PopulatedFeed].
*/ */
val feed: List<NewsResource> val feed: List<SaveableNewsResource>
/** /**
* The feed, along with a list of topics that can be selected. * The feed, along with a list of topics that can be selected.
*/ */
data class FeedWithTopicSelection( data class FeedWithTopicSelection(
val selectedTopics: List<Pair<Topic, Boolean>>, val topics: List<FollowableTopic>,
override val feed: List<NewsResource> override val feed: List<SaveableNewsResource>
) : PopulatedFeed { ) : PopulatedFeed {
val canSaveSelectedTopics: Boolean = selectedTopics.any { it.second } val canSaveSelectedTopics: Boolean = topics.any { it.isFollowed }
} }
/** /**
* Just the feed. * Just the feed.
*/ */
data class FeedWithoutTopicSelection( data class FeedWithoutTopicSelection(
override val feed: List<NewsResource> override val feed: List<SaveableNewsResource>
) : PopulatedFeed ) : PopulatedFeed
} }
} }

@ -18,11 +18,16 @@ package com.google.samples.apps.nowinandroid.feature.foryou
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
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.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.util.TestDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -91,26 +96,35 @@ class ForYouViewModelTest {
awaitItem() awaitItem()
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) topicsRepository.setFollowedTopicIds(emptySet())
newsRepository.sendNewsResources(emptyList()) newsRepository.sendNewsResources(sampleNewsResources)
assertEquals( assertEquals(
ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection( ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection(
selectedTopics = listOf( topics = listOf(
Topic( FollowableTopic(
topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" description = ""
) to false, ),
Topic( isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" description = ""
) to false, ),
Topic( isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "", description = "",
) to false ),
isFollowed = false
),
), ),
feed = emptyList() feed = emptyList()
), ),
@ -127,11 +141,16 @@ class ForYouViewModelTest {
awaitItem() awaitItem()
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(setOf(0, 1)) topicsRepository.setFollowedTopicIds(setOf(0, 1))
newsRepository.sendNewsResources(emptyList()) newsRepository.sendNewsResources(sampleNewsResources)
assertEquals( assertEquals(
ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection(
feed = emptyList() feed = sampleNewsResources.map {
SaveableNewsResource(
newsResource = it,
isSaved = false
)
}
), ),
awaitItem() awaitItem()
) )
@ -146,31 +165,49 @@ class ForYouViewModelTest {
awaitItem() awaitItem()
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) topicsRepository.setFollowedTopicIds(emptySet())
newsRepository.sendNewsResources(emptyList()) newsRepository.sendNewsResources(sampleNewsResources)
awaitItem() awaitItem()
viewModel.updateTopicSelection(1, isChecked = true) viewModel.updateTopicSelection(1, isChecked = true)
assertEquals( assertEquals(
ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection( ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection(
selectedTopics = listOf( topics = listOf(
Topic( FollowableTopic(
topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" description = ""
) to false, ),
Topic( isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" description = ""
) to true, ),
Topic( isFollowed = true
),
FollowableTopic(
topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "" description = "",
) to false
), ),
feed = emptyList() isFollowed = false
)
),
feed = listOf(
SaveableNewsResource(
newsResource = sampleNewsResources[1],
isSaved = false
),
SaveableNewsResource(
newsResource = sampleNewsResources[2],
isSaved = false
)
)
), ),
awaitItem() awaitItem()
) )
@ -185,7 +222,7 @@ class ForYouViewModelTest {
awaitItem() awaitItem()
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) topicsRepository.setFollowedTopicIds(emptySet())
newsRepository.sendNewsResources(emptyList()) newsRepository.sendNewsResources(sampleNewsResources)
awaitItem() awaitItem()
viewModel.updateTopicSelection(1, isChecked = true) viewModel.updateTopicSelection(1, isChecked = true)
@ -195,22 +232,31 @@ class ForYouViewModelTest {
assertEquals( assertEquals(
ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection( ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection(
selectedTopics = listOf( topics = listOf(
Topic( FollowableTopic(
topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" description = ""
) to false, ),
Topic( isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" description = ""
) to false, ),
Topic( isFollowed = false
),
FollowableTopic(
topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "" description = "",
) to false ),
isFollowed = false
)
), ),
feed = emptyList() feed = emptyList()
), ),
@ -227,7 +273,7 @@ class ForYouViewModelTest {
awaitItem() awaitItem()
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) topicsRepository.setFollowedTopicIds(emptySet())
newsRepository.sendNewsResources(emptyList()) newsRepository.sendNewsResources(sampleNewsResources)
awaitItem() awaitItem()
viewModel.updateTopicSelection(1, isChecked = true) viewModel.updateTopicSelection(1, isChecked = true)
@ -237,7 +283,16 @@ class ForYouViewModelTest {
assertEquals( assertEquals(
ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection(
feed = emptyList() feed = listOf(
SaveableNewsResource(
newsResource = sampleNewsResources[1],
isSaved = false
),
SaveableNewsResource(
newsResource = sampleNewsResources[2],
isSaved = false
)
)
), ),
awaitItem() awaitItem()
) )
@ -245,6 +300,35 @@ class ForYouViewModelTest {
cancel() cancel()
} }
} }
@Test
fun newsResourceSelectionUpdatesAfterLoadingFollowedTopics() = runTest {
viewModel.uiState
.test {
awaitItem()
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(setOf(1))
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateNewsResourceSaved(2, true)
assertEquals(
ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection(
feed = listOf(
SaveableNewsResource(
newsResource = sampleNewsResources[1],
isSaved = true
),
SaveableNewsResource(
newsResource = sampleNewsResources[2],
isSaved = false
)
)
),
awaitItem()
)
cancel()
}
}
} }
private val sampleTopics = listOf( private val sampleTopics = listOf(
@ -264,3 +348,62 @@ private val sampleTopics = listOf(
description = "" description = ""
) )
) )
private val sampleNewsResources = listOf(
NewsResource(
id = 1,
episodeId = 52,
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",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video,
topics = listOf(
Topic(
id = 0,
name = "Headlines",
description = ""
)
),
authors = emptyList()
),
NewsResource(
id = 2,
episodeId = 52,
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",
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = Video,
topics = listOf(
Topic(
id = 1,
name = "UI",
description = ""
),
),
authors = emptyList()
),
NewsResource(
id = 3,
episodeId = 52,
title = "Community tip on Paging",
content = "Tips for using the Paging library from the developer community",
url = "https://youtu.be/r5JgIyS3t3s",
publishDate = Instant.parse("2021-11-08T00:00:00.000Z"),
type = Video,
topics = listOf(
Topic(
id = 1,
name = "UI",
description = ""
),
),
authors = emptyList()
),
)

Loading…
Cancel
Save