Add People to interests screen

Screenshot: https://screenshot.googleplex.com/3XqkTwJQwgfoPsT

Change-Id: Iac7dc06c84523b4db8d05f538b74a08741d2c347
pull/2/head
Manuel Vivo 4 years ago committed by Don Turner
parent de2f07d1a4
commit 3558a6931e

@ -68,7 +68,7 @@ class NavigationTest {
private lateinit var forYou: String private lateinit var forYou: String
private lateinit var episodes: String private lateinit var episodes: String
private lateinit var saved: String private lateinit var saved: String
private lateinit var topics: String private lateinit var interests: String
private lateinit var sampleTopic: String private lateinit var sampleTopic: String
@Before @Before
@ -80,7 +80,7 @@ class NavigationTest {
forYou = getString(R.string.for_you) forYou = getString(R.string.for_you)
episodes = getString(R.string.episodes) episodes = getString(R.string.episodes)
saved = getString(R.string.saved) saved = getString(R.string.saved)
topics = getString(R.string.following) interests = getString(R.string.interests)
sampleTopic = "Headlines" sampleTopic = "Headlines"
} }
} }
@ -105,8 +105,8 @@ class NavigationTest {
composeTestRule.apply { composeTestRule.apply {
// GIVEN the user follows a topic // GIVEN the user follows a topic
onNodeWithText(sampleTopic).performClick() onNodeWithText(sampleTopic).performClick()
// WHEN the user navigates to the Topics destination // WHEN the user navigates to the Interests destination
onNodeWithText(topics).performClick() onNodeWithText(interests).performClick()
// AND the user navigates to the For You destination // AND the user navigates to the For You destination
onNodeWithText(forYou).performClick() onNodeWithText(forYou).performClick()
// THEN the state of the For You destination is restored // THEN the state of the For You destination is restored
@ -146,7 +146,7 @@ class NavigationTest {
// GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown. // GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown.
onNodeWithContentDescription(navigateUp).assertDoesNotExist() onNodeWithContentDescription(navigateUp).assertDoesNotExist()
// TODO: Add top level destinations here, see b/226357686. // TODO: Add top level destinations here, see b/226357686.
onNodeWithText(topics).performClick() onNodeWithText(interests).performClick()
onNodeWithContentDescription(navigateUp).assertDoesNotExist() onNodeWithContentDescription(navigateUp).assertDoesNotExist()
} }
} }
@ -157,8 +157,8 @@ class NavigationTest {
@Test(expected = NoActivityResumedException::class) @Test(expected = NoActivityResumedException::class)
fun homeDestination_back_quitsApp() { fun homeDestination_back_quitsApp() {
composeTestRule.apply { composeTestRule.apply {
// GIVEN the user navigates to the Topics destination // GIVEN the user navigates to the Interests destination
onNodeWithText(topics).performClick() onNodeWithText(interests).performClick()
// and then navigates to the For you destination // and then navigates to the For you destination
onNodeWithText(forYou).performClick() onNodeWithText(forYou).performClick()
// WHEN the user uses the system button/gesture to go back // WHEN the user uses the system button/gesture to go back
@ -174,8 +174,8 @@ class NavigationTest {
@Test @Test
fun navigationBar_backFromAnyDestination_returnsToForYou() { fun navigationBar_backFromAnyDestination_returnsToForYou() {
composeTestRule.apply { composeTestRule.apply {
// GIVEN the user navigated to the Topics destination // GIVEN the user navigated to the Interests destination
onNodeWithText(topics).performClick() onNodeWithText(interests).performClick()
// TODO: Add another destination here to increase test coverage, see b/226357686. // TODO: Add another destination here to increase test coverage, see b/226357686.
// WHEN the user uses the system button/gesture to go back, // WHEN the user uses the system button/gesture to go back,
Espresso.pressBack() Espresso.pressBack()
@ -187,16 +187,16 @@ class NavigationTest {
@Test @Test
fun navigationBar_multipleBackStackFollowing() { fun navigationBar_multipleBackStackFollowing() {
composeTestRule.apply { composeTestRule.apply {
onNodeWithText(topics).performClick() onNodeWithText(interests).performClick()
onNodeWithText("Android Studio").performClick() // TODO: Grab string from fake data onNodeWithText("Android Studio").performClick() // TODO: Grab string from fake data
// Switch tab // Switch tab
onNodeWithText(forYou).performClick() onNodeWithText(forYou).performClick()
// Come back to Following // Come back to Following
onNodeWithText(topics).performClick() onNodeWithText(interests).performClick()
// Verify we're not in the list of topics // Verify we're not in the list of interests
onNodeWithText("Android Auto").assertDoesNotExist() // TODO: Grab string from fake data onNodeWithText("Android Auto").assertDoesNotExist() // TODO: Grab string from fake data
} }
} }

@ -199,7 +199,7 @@ private sealed class Destination(
route = NiaDestinations.FOLLOWING_ROUTE, route = NiaDestinations.FOLLOWING_ROUTE,
selectedIcon = Icons.Filled.Grid3x3, selectedIcon = Icons.Filled.Grid3x3,
unselectedIcon = Icons.Outlined.Grid3x3, unselectedIcon = Icons.Outlined.Grid3x3,
iconTextId = R.string.following iconTextId = R.string.interests
) )
} }

@ -26,12 +26,12 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.google.samples.apps.nowinandroid.feature.following.FollowingRoute import com.google.samples.apps.nowinandroid.feature.following.InterestsRoute
import com.google.samples.apps.nowinandroid.feature.foryou.ForYouRoute import com.google.samples.apps.nowinandroid.feature.foryou.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.topic.TopicDestinations import com.google.samples.apps.nowinandroid.feature.topic.InterestsDestinations
import com.google.samples.apps.nowinandroid.feature.topic.InterestsScreens.TOPIC_SCREEN
import com.google.samples.apps.nowinandroid.feature.topic.TopicDestinationsArgs import com.google.samples.apps.nowinandroid.feature.topic.TopicDestinationsArgs
import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute
import com.google.samples.apps.nowinandroid.feature.topic.TopicScreens.TOPIC_SCREEN
/** /**
* Top-level navigation graph. Navigation is organized as explained at * Top-level navigation graph. Navigation is organized as explained at
@ -60,17 +60,18 @@ fun NiaNavGraph(
Text("SAVED", modifier) Text("SAVED", modifier)
} }
navigation( navigation(
startDestination = TopicDestinations.TOPICS_ROUTE, startDestination = InterestsDestinations.INTERESTS_ROUTE,
route = NiaDestinations.FOLLOWING_ROUTE route = NiaDestinations.FOLLOWING_ROUTE
) { ) {
composable(TopicDestinations.TOPICS_ROUTE) { composable(InterestsDestinations.INTERESTS_ROUTE) {
FollowingRoute( InterestsRoute(
navigateToTopic = { navController.navigate("$TOPIC_SCREEN/$it") }, navigateToTopic = { navController.navigate("$TOPIC_SCREEN/$it") },
navigateToAuthor = { /* TO IMPLEMENT */ },
modifier = modifier modifier = modifier
) )
} }
composable( composable(
TopicDestinations.TOPIC_ROUTE, InterestsDestinations.TOPIC_ROUTE,
arguments = listOf( arguments = listOf(
navArgument(TopicDestinationsArgs.TOPIC_ID_ARG) { navArgument(TopicDestinationsArgs.TOPIC_ID_ARG) {
type = NavType.IntType type = NavType.IntType

@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon import androidx.compose.material.Icon
@ -42,7 +41,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable @Composable
fun NiaToolbar( fun NiaToolbar(
@ -54,9 +52,7 @@ fun NiaToolbar(
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier.fillMaxWidth()
.fillMaxWidth()
.padding(bottom = 32.dp)
) { ) {
IconButton(onClick = { onSearchClick() }) { IconButton(onClick = { onSearchClick() }) {
Icon( Icon(

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.following package com.google.samples.apps.nowinandroid.following
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertCountEquals
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
@ -24,9 +25,12 @@ 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.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.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.FollowingTabState
import com.google.samples.apps.nowinandroid.feature.following.FollowingUiState import com.google.samples.apps.nowinandroid.feature.following.FollowingUiState
import com.google.samples.apps.nowinandroid.feature.following.R import com.google.samples.apps.nowinandroid.feature.following.R
import org.junit.Before import org.junit.Before
@ -34,18 +38,17 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
/** /**
* UI test for checking the correct behaviour of the Following screen; * UI test for checking the correct behaviour of the Interests screen;
* Verifies that, when a specific UiState is set, the corresponding * Verifies that, when a specific UiState is set, the corresponding
* composables and details are shown * composables and details are shown
*/ */
class FollowingScreenTest { class InterestsScreenTest {
@get:Rule @get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>() val composeTestRule = createAndroidComposeRule<ComponentActivity>()
private lateinit var followingLoading: String private lateinit var followingLoading: String
private lateinit var followingErrorHeader: String private lateinit var followingEmptyHeader: String
private lateinit var followingTopicCardIcon: String
private lateinit var followingTopicCardFollowButton: String private lateinit var followingTopicCardFollowButton: String
private lateinit var followingTopicCardUnfollowButton: String private lateinit var followingTopicCardUnfollowButton: String
@ -53,23 +56,29 @@ class FollowingScreenTest {
fun setup() { fun setup() {
composeTestRule.activity.apply { composeTestRule.activity.apply {
followingLoading = getString(R.string.following_loading) followingLoading = getString(R.string.following_loading)
followingErrorHeader = getString(R.string.following_error_header) followingEmptyHeader = getString(R.string.following_empty_header)
followingTopicCardIcon = getString(R.string.following_topic_card_icon_content_desc)
followingTopicCardFollowButton = followingTopicCardFollowButton =
getString(R.string.following_topic_card_follow_button_content_desc) getString(R.string.interests_card_follow_button_content_desc)
followingTopicCardUnfollowButton = followingTopicCardUnfollowButton =
getString(R.string.following_topic_card_unfollow_button_content_desc) getString(R.string.interests_card_unfollow_button_content_desc)
} }
} }
@Test @Test
fun niaLoadingIndicator_whenScreenIsLoading_showLoading() { fun niaLoadingIndicator_inTopics_whenScreenIsLoading_showLoading() {
composeTestRule.setContent { composeTestRule.setContent {
FollowingScreen( InterestsScreen(uiState = FollowingUiState.Loading, tabIndex = 0)
uiState = FollowingUiState.Loading, }
followTopic = { _, _ -> },
navigateToTopic = {} composeTestRule
) .onNodeWithContentDescription(followingLoading)
.assertExists()
}
@Test
fun niaLoadingIndicator_inAuthors_whenScreenIsLoading_showLoading() {
composeTestRule.setContent {
InterestsScreen(uiState = FollowingUiState.Loading, tabIndex = 1)
} }
composeTestRule composeTestRule
@ -78,12 +87,11 @@ class FollowingScreenTest {
} }
@Test @Test
fun followingWithTopics_whenTopicsFollowed_showFollowedAndUnfollowedTopicsWithInfo() { fun interestsWithTopics_whenTopicsFollowed_showFollowedAndUnfollowedTopicsWithInfo() {
composeTestRule.setContent { composeTestRule.setContent {
FollowingScreen( InterestsScreen(
uiState = FollowingUiState.Topics(topics = testTopics), uiState = FollowingUiState.Interests(topics = testTopics, authors = listOf()),
followTopic = { _, _ -> }, tabIndex = 0
navigateToTopic = {}
) )
} }
@ -102,12 +110,36 @@ class FollowingScreenTest {
.assertCountEquals(testTopics.count()) .assertCountEquals(testTopics.count())
composeTestRule composeTestRule
.onAllNodesWithContentDescription(followingTopicCardIcon) .onAllNodesWithContentDescription(followingTopicCardFollowButton)
.assertCountEquals(testTopics.count()) .assertCountEquals(numberOfUnfollowedTopics)
composeTestRule
.onAllNodesWithContentDescription(followingTopicCardUnfollowButton)
.assertCountEquals(testAuthors.filter { it.isFollowed }.size)
}
@Test
fun interestsWithTopics_whenAuthorsFollowed_showFollowedAndUnfollowedTopicsWithInfo() {
composeTestRule.setContent {
InterestsScreen(
uiState = FollowingUiState.Interests(topics = listOf(), authors = testAuthors),
tabIndex = 1
)
}
composeTestRule
.onNodeWithText("Android Dev")
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Android Dev 2")
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Android Dev 3")
.assertIsDisplayed()
composeTestRule composeTestRule
.onAllNodesWithContentDescription(followingTopicCardFollowButton) .onAllNodesWithContentDescription(followingTopicCardFollowButton)
.assertCountEquals(numberOfUnfollowedTopics) .assertCountEquals(numberOfUnfollowedAuthors)
composeTestRule composeTestRule
.onAllNodesWithContentDescription(followingTopicCardUnfollowButton) .onAllNodesWithContentDescription(followingTopicCardUnfollowButton)
@ -115,19 +147,42 @@ class FollowingScreenTest {
} }
@Test @Test
fun followingError_whenErrorOccurs_thenShowEmptyErrorScreen() { fun topicsEmpty_whenDataIsEmptyOccurs_thenShowEmptyScreen() {
composeTestRule.setContent { composeTestRule.setContent {
FollowingScreen( InterestsScreen(uiState = FollowingUiState.Empty, tabIndex = 0)
uiState = FollowingUiState.Error, }
followTopic = { _, _ -> },
navigateToTopic = {} composeTestRule
) .onNodeWithText(followingEmptyHeader)
.assertIsDisplayed()
}
@Test
fun authorsEmpty_whenDataIsEmptyOccurs_thenShowEmptyScreen() {
composeTestRule.setContent {
InterestsScreen(uiState = FollowingUiState.Empty, tabIndex = 1)
} }
composeTestRule composeTestRule
.onNodeWithText(followingErrorHeader) .onNodeWithText(followingEmptyHeader)
.assertIsDisplayed() .assertIsDisplayed()
} }
@Composable
private fun InterestsScreen(uiState: FollowingUiState, tabIndex: Int = 0) {
FollowingScreen(
uiState = uiState,
tabState = FollowingTabState(
titles = listOf(R.string.following_topics, R.string.following_people),
currentIndex = tabIndex
),
followAuthor = { _, _ -> },
followTopic = { _, _ -> },
navigateToAuthor = {},
navigateToTopic = {},
switchTab = {},
)
}
} }
private const val TOPIC_1_NAME = "Headlines" private const val TOPIC_1_NAME = "Headlines"
@ -174,4 +229,38 @@ private val testTopics = listOf(
) )
) )
private val testAuthors = listOf(
FollowableAuthor(
Author(
id = 0,
name = "Android Dev",
imageUrl = "",
twitter = "",
mediumPage = ""
),
isFollowed = true
),
FollowableAuthor(
Author(
id = 1,
name = "Android Dev 2",
imageUrl = "",
twitter = "",
mediumPage = ""
),
isFollowed = false
),
FollowableAuthor(
Author(
id = 2,
name = "Android Dev 3",
imageUrl = "",
twitter = "",
mediumPage = ""
),
isFollowed = false
)
)
private val numberOfUnfollowedTopics = testTopics.filter { !it.isFollowed }.size private val numberOfUnfollowedTopics = testTopics.filter { !it.isFollowed }.size
private val numberOfUnfollowedAuthors = testAuthors.filter { !it.isFollowed }.size

@ -0,0 +1,189 @@
/*
* 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.feature.following
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.Android
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.ui.FollowButton
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
import com.google.samples.apps.nowinandroid.feature.following.R.string
@Composable
fun FollowingItem(
name: String,
following: Boolean,
topicImageUrl: String,
onClick: () -> Unit,
onFollowButtonClick: (Boolean) -> Unit,
modifier: Modifier = Modifier,
iconModifier: Modifier = Modifier,
description: String = "",
itemSeparation: Dp = 16.dp
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(1f)
.clickable { onClick() }
.padding(vertical = itemSeparation)
) {
FollowingIcon(topicImageUrl, iconModifier.size(64.dp))
Spacer(modifier = Modifier.width(16.dp))
InterestContent(name, description)
}
FollowButton(
following = following,
onFollowChange = onFollowButtonClick,
notFollowingContentDescription = stringResource(
id = string.interests_card_follow_button_content_desc
),
followingContentDescription = stringResource(
id = string.interests_card_unfollow_button_content_desc
)
)
}
}
@Composable
private fun InterestContent(name: String, description: String, modifier: Modifier = Modifier) {
Column(modifier) {
Text(
text = name,
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(
vertical = if (description.isEmpty()) 0.dp else 4.dp
)
)
if (description.isNotEmpty()) {
Text(
text = description,
style = MaterialTheme.typography.body2
)
}
}
}
@Composable
private fun FollowingIcon(topicImageUrl: String, modifier: Modifier = Modifier) {
if (topicImageUrl.isEmpty()) {
Icon(
imageVector = Filled.Android,
tint = Color.Magenta,
contentDescription = null,
modifier = modifier
)
} else {
AsyncImage(
model = topicImageUrl,
contentDescription = null,
modifier = modifier
)
}
}
@Preview
@Composable
private fun FollowingCardPreview() {
NiaTheme {
Surface {
FollowingItem(
name = "Compose",
description = "Description",
following = false,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { }
)
}
}
}
@Preview
@Composable
private fun FollowingCardLongNamePreview() {
NiaTheme {
Surface {
FollowingItem(
name = "This is a very very very very long name",
description = "Description",
following = true,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { }
)
}
}
}
@Preview
@Composable
private fun FollowingCardLongDescriptionPreview() {
NiaTheme {
Surface {
FollowingItem(
name = "Compose",
description = "This is a very very very very very very very " +
"very very very long description",
following = false,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { }
)
}
}
}
@Preview
@Composable
private fun FollowingCardWithEmptyDescriptionPreview() {
NiaTheme {
Surface {
FollowingItem(
name = "Compose",
description = "",
following = true,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { }
)
}
}
}

@ -16,221 +16,119 @@
package com.google.samples.apps.nowinandroid.feature.following package com.google.samples.apps.nowinandroid.feature.following
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Android
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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 coil.compose.AsyncImage
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.ui.FollowButton
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
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow
@Composable @Composable
fun FollowingRoute( fun InterestsRoute(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navigateToAuthor: () -> Unit,
navigateToTopic: (Int) -> Unit, navigateToTopic: (Int) -> Unit,
viewModel: FollowingViewModel = hiltViewModel() viewModel: FollowingViewModel = hiltViewModel()
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
val tabState by viewModel.tabState.collectAsState()
FollowingScreen( FollowingScreen(
modifier = modifier,
uiState = uiState, uiState = uiState,
tabState = tabState,
followTopic = viewModel::followTopic, followTopic = viewModel::followTopic,
navigateToTopic = navigateToTopic followAuthor = viewModel::followAuthor,
navigateToAuthor = navigateToAuthor,
navigateToTopic = navigateToTopic,
switchTab = viewModel::switchTab,
modifier = modifier
) )
} }
@Composable @Composable
fun FollowingScreen( fun FollowingScreen(
uiState: FollowingUiState, uiState: FollowingUiState,
tabState: FollowingTabState,
followAuthor: (Int, Boolean) -> Unit,
followTopic: (Int, Boolean) -> Unit, followTopic: (Int, Boolean) -> Unit,
navigateToAuthor: () -> Unit,
navigateToTopic: (Int) -> Unit, navigateToTopic: (Int) -> Unit,
switchTab: (Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
NiaToolbar(titleRes = R.string.following) NiaToolbar(titleRes = R.string.interests)
when (uiState) { when (uiState) {
FollowingUiState.Loading -> FollowingUiState.Loading ->
NiaLoadingIndicator( NiaLoadingIndicator(
modifier = modifier, modifier = modifier,
contentDesc = stringResource(id = R.string.following_loading), contentDesc = stringResource(id = R.string.following_loading),
) )
is FollowingUiState.Topics -> is FollowingUiState.Interests ->
FollowingWithTopicsScreen( FollowingContent(
uiState = uiState, tabState, switchTab, uiState, navigateToTopic, followTopic,
onTopicClick = navigateToTopic, navigateToAuthor, followAuthor
onFollowButtonClick = followTopic,
) )
is FollowingUiState.Error -> FollowingErrorScreen() is FollowingUiState.Empty -> InterestsEmptyScreen()
} }
} }
} }
@Composable @Composable
fun FollowingWithTopicsScreen( private fun FollowingContent(
modifier: Modifier = Modifier, tabState: FollowingTabState,
uiState: FollowingUiState.Topics, switchTab: (Int) -> Unit,
onTopicClick: (Int) -> Unit, uiState: FollowingUiState.Interests,
onFollowButtonClick: (Int, Boolean) -> Unit navigateToTopic: (Int) -> Unit,
followTopic: (Int, Boolean) -> Unit,
navigateToAuthor: () -> Unit,
followAuthor: (Int, Boolean) -> Unit,
modifier: Modifier = Modifier
) { ) {
LazyColumn( Column(modifier) {
modifier = modifier NiaTabRow(selectedTabIndex = tabState.currentIndex) {
) { tabState.titles.forEachIndexed { index, titleId ->
uiState.topics.forEach { followableTopic -> NiaTab(
item { selected = index == tabState.currentIndex,
FollowingTopicCard( onClick = { switchTab(index) },
followableTopic = followableTopic, text = { Text(text = stringResource(id = titleId)) }
onTopicClick = { onTopicClick(followableTopic.topic.id) },
onFollowButtonClick = onFollowButtonClick
) )
} }
} }
} when (tabState.currentIndex) {
} 0 -> {
TopicsTabContent(
@Composable topics = uiState.topics,
fun FollowingErrorScreen() { onTopicClick = navigateToTopic,
Text(text = stringResource(id = R.string.following_error_header)) onFollowButtonClick = followTopic,
} modifier = Modifier.padding(top = 8.dp)
)
@Composable }
fun FollowingTopicCard( 1 -> {
followableTopic: FollowableTopic, AuthorsTabContent(
onTopicClick: () -> Unit, authors = uiState.authors,
onFollowButtonClick: (Int, Boolean) -> Unit, onAuthorClick = { navigateToAuthor() },
) { onFollowButtonClick = followAuthor,
Row( modifier = Modifier.padding(top = 8.dp)
verticalAlignment = Alignment.CenterVertically, )
modifier = }
Modifier.padding(
start = 24.dp,
end = 8.dp,
bottom = 24.dp
)
) {
TopicIcon(
modifier = Modifier.padding(end = 24.dp),
topicImageUrl = followableTopic.topic.imageUrl,
onClick = onTopicClick
)
Column(
Modifier
.wrapContentSize(Alignment.CenterStart)
.weight(1f)
.clickable { onTopicClick() }
) {
TopicTitle(topicName = followableTopic.topic.name)
TopicDescription(topicDescription = followableTopic.topic.shortDescription)
} }
FollowButton(
following = followableTopic.isFollowed,
onFollowChange = { following ->
onFollowButtonClick(followableTopic.topic.id, following)
},
notFollowingContentDescription = stringResource(
id = R.string.following_topic_card_follow_button_content_desc
),
followingContentDescription = stringResource(
id = R.string.following_topic_card_unfollow_button_content_desc
)
)
}
}
@Composable
fun TopicTitle(
topicName: String,
modifier: Modifier = Modifier
) {
Text(
text = topicName,
style = MaterialTheme.typography.h5,
modifier = modifier.padding(top = 12.dp, bottom = 8.dp)
)
}
@Composable
fun TopicDescription(topicDescription: String) {
Text(
text = topicDescription,
style = MaterialTheme.typography.body2,
modifier = Modifier.wrapContentSize(Alignment.CenterStart)
)
}
@Composable
fun TopicIcon(
modifier: Modifier = Modifier,
topicImageUrl: String,
onClick: () -> Unit
) {
val iconModifier = modifier.size(64.dp)
.clickable { onClick() }
val contentDescription = stringResource(id = R.string.following_topic_card_icon_content_desc)
if (topicImageUrl.isEmpty()) {
Icon(
imageVector = Icons.Filled.Android,
tint = Color.Magenta,
contentDescription = contentDescription,
modifier = iconModifier
)
} else {
AsyncImage(
model = topicImageUrl,
contentDescription = contentDescription,
modifier = iconModifier
)
} }
} }
@Preview("Topic card")
@Composable @Composable
fun TopicCardPreview() { private fun InterestsEmptyScreen() {
NiaTheme { Text(text = stringResource(id = R.string.following_empty_header))
Surface {
FollowingTopicCard(
FollowableTopic(
Topic(
id = 0,
name = "Compose",
shortDescription = "Short description",
longDescription = "Long description",
url = "URL",
imageUrl = "imageUrl"
),
isFollowed = false
),
onTopicClick = {},
onFollowButtonClick = { _, _ -> }
)
}
}
} }

@ -18,44 +18,61 @@ 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.AuthorsRepository
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.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
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
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class FollowingViewModel @Inject constructor( class FollowingViewModel @Inject constructor(
private val authorsRepository: AuthorsRepository,
private val topicsRepository: TopicsRepository private val topicsRepository: TopicsRepository
) : ViewModel() { ) : ViewModel() {
private val followedTopicIdsStream = topicsRepository.getFollowedTopicIdsStream() private val _tabState = MutableStateFlow(
.map<Set<Int>, FollowingState> { followedTopics -> FollowingTabState(
FollowingState.Topics(topics = followedTopics) titles = listOf(R.string.following_topics, R.string.following_people),
} currentIndex = 0
.catch { emit(FollowingState.Error) } )
)
val tabState: StateFlow<FollowingTabState> = _tabState.asStateFlow()
val uiState: StateFlow<FollowingUiState> = combine( val uiState: StateFlow<FollowingUiState> = combine(
followedTopicIdsStream, authorsRepository.getAuthorsStream(),
authorsRepository.getFollowedAuthorIdsStream(),
topicsRepository.getTopicsStream(), topicsRepository.getTopicsStream(),
) { followedTopicIdsState, topics -> topicsRepository.getFollowedTopicIdsStream(),
if (followedTopicIdsState is FollowingState.Topics) { ) { availableAuthors, followedAuthorIdsState, availableTopics, followedTopicIdsState ->
mapFollowedAndUnfollowedTopics(topics)
} else { FollowingUiState.Interests(
flowOf(FollowingUiState.Error) authors = availableAuthors
} .map { author ->
FollowableAuthor(
author = author,
isFollowed = author.id in followedAuthorIdsState
)
}
.sortedBy { it.author.name },
topics = availableTopics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in followedTopicIdsState
)
}
.sortedBy { it.topic.name }
)
} }
.flatMapLatest { it }
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000), started = SharingStarted.WhileSubscribed(5_000),
@ -68,28 +85,33 @@ class FollowingViewModel @Inject constructor(
} }
} }
private fun mapFollowedAndUnfollowedTopics(topics: List<Topic>): Flow<FollowingUiState.Topics> = fun followAuthor(followedAuthorId: Int, followed: Boolean) {
topicsRepository.getFollowedTopicIdsStream().map { followedTopicIds -> viewModelScope.launch {
FollowingUiState.Topics( authorsRepository.toggleFollowedAuthorId(followedAuthorId, followed)
topics = topics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in followedTopicIds,
)
}
.sortedBy { it.topic.name }
)
} }
} }
private sealed interface FollowingState { fun switchTab(newIndex: Int) {
data class Topics(val topics: Set<Int>) : FollowingState if (newIndex != tabState.value.currentIndex) {
object Error : FollowingState _tabState.update {
it.copy(currentIndex = newIndex)
}
}
}
} }
data class FollowingTabState(
val titles: List<Int>,
val currentIndex: Int
)
sealed interface FollowingUiState { sealed interface FollowingUiState {
object Loading : FollowingUiState object Loading : FollowingUiState
data class Topics(val topics: List<FollowableTopic>) : FollowingUiState
object Error : FollowingUiState data class Interests(
val authors: List<FollowableAuthor>,
val topics: List<FollowableTopic>
) : FollowingUiState
object Empty : FollowingUiState
} }

@ -0,0 +1,77 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.feature.following
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
@Composable
fun TopicsTabContent(
topics: List<FollowableTopic>,
onTopicClick: (Int) -> Unit,
onFollowButtonClick: (Int, Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.padding(horizontal = 16.dp)
) {
topics.forEach { followableTopic ->
item {
FollowingItem(
name = followableTopic.topic.name,
following = followableTopic.isFollowed,
description = followableTopic.topic.shortDescription,
topicImageUrl = followableTopic.topic.imageUrl,
onClick = { onTopicClick(followableTopic.topic.id) },
onFollowButtonClick = { onFollowButtonClick(followableTopic.topic.id, it) }
)
}
}
}
}
@Composable
fun AuthorsTabContent(
authors: List<FollowableAuthor>,
onAuthorClick: () -> Unit,
onFollowButtonClick: (Int, Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.padding(horizontal = 16.dp)
) {
authors.forEach { followableTopic ->
item {
FollowingItem(
name = followableTopic.author.name,
following = followableTopic.isFollowed,
topicImageUrl = followableTopic.author.imageUrl,
onClick = onAuthorClick,
onFollowButtonClick = { onFollowButtonClick(followableTopic.author.id, it) },
iconModifier = Modifier.clip(CircleShape)
)
}
}
}
}

@ -14,10 +14,11 @@
~ limitations under the License. ~ limitations under the License.
--> -->
<resources> <resources>
<string name="following">Following</string> <string name="interests">Interests</string>
<string name="following_loading">Loading topics</string> <string name="following_topics">Topics</string>
<string name="following_error_header">"Error loading topics"</string> <string name="following_people">People</string>
<string name="following_topic_card_icon_content_desc">Topic icon</string> <string name="following_loading">Loading data</string>
<string name="following_topic_card_follow_button_content_desc">Follow Topic button</string> <string name="following_empty_header">"No available data"</string>
<string name="following_topic_card_unfollow_button_content_desc">Unfollow Topic button</string> <string name="interests_card_follow_button_content_desc">Follow interest button</string>
<string name="interests_card_unfollow_button_content_desc">Unfollow interest button</string>
</resources> </resources>

@ -17,8 +17,11 @@
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.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.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.TestAuthorsRepository
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 com.google.samples.apps.nowinandroid.feature.following.FollowingUiState import com.google.samples.apps.nowinandroid.feature.following.FollowingUiState
@ -34,12 +37,13 @@ class FollowingViewModelTest {
@get:Rule @get:Rule
val dispatcherRule = TestDispatcherRule() val dispatcherRule = TestDispatcherRule()
private val authorsRepository = TestAuthorsRepository()
private val topicsRepository = TestTopicsRepository() private val topicsRepository = TestTopicsRepository()
private lateinit var viewModel: FollowingViewModel private lateinit var viewModel: FollowingViewModel
@Before @Before
fun setup() { fun setup() {
viewModel = FollowingViewModel(topicsRepository = topicsRepository) viewModel = FollowingViewModel(authorsRepository, topicsRepository)
} }
@Test @Test
@ -54,24 +58,36 @@ class FollowingViewModelTest {
fun uiState_whenFollowedTopicsAreLoading_thenShowLoading() = runTest { fun uiState_whenFollowedTopicsAreLoading_thenShowLoading() = runTest {
viewModel.uiState.test { viewModel.uiState.test {
assertEquals(FollowingUiState.Loading, awaitItem()) assertEquals(FollowingUiState.Loading, awaitItem())
authorsRepository.setFollowedAuthorIds(setOf(1))
topicsRepository.setFollowedTopicIds(emptySet()) topicsRepository.setFollowedTopicIds(emptySet())
cancel() cancel()
} }
} }
@Test @Test
fun uiState_whenFollowingNewTopic_thenShowUpdatedTopics() = runTest { fun uiState_whenFollowedAuthorsAreLoading_thenShowLoading() = runTest {
viewModel.uiState.test {
assertEquals(FollowingUiState.Loading, awaitItem())
authorsRepository.setFollowedAuthorIds(emptySet())
topicsRepository.setFollowedTopicIds(setOf(1))
cancel()
}
}
@Test
fun uiState_whenFollowingNewTopic_thenShowUpdatedTopics() = runTest {
val toggleTopicId = testOutputTopics[1].topic.id val toggleTopicId = testOutputTopics[1].topic.id
viewModel.uiState viewModel.uiState
.test { .test {
awaitItem() awaitItem()
authorsRepository.sendAuthors(emptyList())
authorsRepository.setFollowedAuthorIds(emptySet())
topicsRepository.sendTopics(testInputTopics.map { it.topic }) topicsRepository.sendTopics(testInputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[0].topic.id)) topicsRepository.setFollowedTopicIds(setOf(testInputTopics[0].topic.id))
assertEquals( assertEquals(
false, false,
(awaitItem() as FollowingUiState.Topics) (awaitItem() as FollowingUiState.Interests)
.topics.first { it.topic.id == toggleTopicId }.isFollowed .topics.first { it.topic.id == toggleTopicId }.isFollowed
) )
@ -81,9 +97,32 @@ class FollowingViewModelTest {
) )
assertEquals( assertEquals(
true, FollowingUiState.Interests(topics = testOutputTopics, authors = emptyList()),
(awaitItem() as FollowingUiState.Topics) awaitItem()
.topics.first { it.topic.id == toggleTopicId }.isFollowed )
cancel()
}
}
@Test
fun uiState_whenFollowingNewAuthor_thenShowUpdatedAuthors() = runTest {
viewModel.uiState
.test {
awaitItem()
authorsRepository.sendAuthors(testInputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[0].author.id))
topicsRepository.sendTopics(listOf())
topicsRepository.setFollowedTopicIds(setOf())
awaitItem()
viewModel.followAuthor(
followedAuthorId = testInputAuthors[1].author.id,
followed = true
)
assertEquals(
FollowingUiState.Interests(topics = emptyList(), authors = testOutputAuthors),
awaitItem()
) )
cancel() cancel()
} }
@ -95,6 +134,8 @@ class FollowingViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
awaitItem() awaitItem()
authorsRepository.sendAuthors(emptyList())
authorsRepository.setFollowedAuthorIds(emptySet())
topicsRepository.sendTopics(testOutputTopics.map { it.topic }) topicsRepository.sendTopics(testOutputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds( topicsRepository.setFollowedTopicIds(
setOf(testOutputTopics[0].topic.id, testOutputTopics[1].topic.id) setOf(testOutputTopics[0].topic.id, testOutputTopics[1].topic.id)
@ -102,7 +143,7 @@ class FollowingViewModelTest {
assertEquals( assertEquals(
true, true,
(awaitItem() as FollowingUiState.Topics) (awaitItem() as FollowingUiState.Interests)
.topics.first { it.topic.id == toggleTopicId }.isFollowed .topics.first { it.topic.id == toggleTopicId }.isFollowed
) )
@ -112,9 +153,34 @@ class FollowingViewModelTest {
) )
assertEquals( assertEquals(
false, FollowingUiState.Interests(topics = testInputTopics, authors = emptyList()),
(awaitItem() as FollowingUiState.Topics) awaitItem()
.topics.first { it.topic.id == toggleTopicId }.isFollowed )
cancel()
}
}
@Test
fun uiState_whenUnfollowingAuthors_thenShowUpdatedAuthors() = runTest {
viewModel.uiState
.test {
awaitItem()
authorsRepository.sendAuthors(testOutputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds(
setOf(testOutputAuthors[0].author.id, testOutputAuthors[1].author.id)
)
topicsRepository.sendTopics(listOf())
topicsRepository.setFollowedTopicIds(setOf())
awaitItem()
viewModel.followAuthor(
followedAuthorId = testOutputAuthors[1].author.id,
followed = false
)
assertEquals(
FollowingUiState.Interests(topics = emptyList(), authors = testInputAuthors),
awaitItem()
) )
cancel() cancel()
} }
@ -129,6 +195,72 @@ private const val TOPIC_LONG_DESC = "At vero eos et accusamus et iusto odio dign
private const val TOPIC_URL = "URL" private const val TOPIC_URL = "URL"
private const val TOPIC_IMAGE_URL = "Image URL" private const val TOPIC_IMAGE_URL = "Image URL"
private val testInputAuthors = listOf(
FollowableAuthor(
Author(
id = 0,
name = "Android Dev",
imageUrl = "",
twitter = "",
mediumPage = ""
),
isFollowed = true
),
FollowableAuthor(
Author(
id = 1,
name = "Android Dev 2",
imageUrl = "",
twitter = "",
mediumPage = ""
),
isFollowed = false
),
FollowableAuthor(
Author(
id = 2,
name = "Android Dev 3",
imageUrl = "",
twitter = "",
mediumPage = ""
),
isFollowed = false
)
)
private val testOutputAuthors = listOf(
FollowableAuthor(
Author(
id = 0,
name = "Android Dev",
imageUrl = "",
twitter = "",
mediumPage = ""
),
isFollowed = true
),
FollowableAuthor(
Author(
id = 1,
name = "Android Dev 2",
imageUrl = "",
twitter = "",
mediumPage = ""
),
isFollowed = true
),
FollowableAuthor(
Author(
id = 2,
name = "Android Dev 3",
imageUrl = "",
twitter = "",
mediumPage = ""
),
isFollowed = false
)
)
private val testInputTopics = listOf( private val testInputTopics = listOf(
FollowableTopic( FollowableTopic(
Topic( Topic(

@ -16,11 +16,11 @@
package com.google.samples.apps.nowinandroid.feature.topic package com.google.samples.apps.nowinandroid.feature.topic
import com.google.samples.apps.nowinandroid.feature.topic.InterestsScreens.TOPIC_SCREEN
import com.google.samples.apps.nowinandroid.feature.topic.TopicDestinationsArgs.TOPIC_ID_ARG import com.google.samples.apps.nowinandroid.feature.topic.TopicDestinationsArgs.TOPIC_ID_ARG
import com.google.samples.apps.nowinandroid.feature.topic.TopicScreens.TOPIC_SCREEN
object TopicDestinations { object InterestsDestinations {
const val TOPICS_ROUTE = "topics" const val INTERESTS_ROUTE = "interests"
const val TOPIC_ROUTE = "$TOPIC_SCREEN/{$TOPIC_ID_ARG}" const val TOPIC_ROUTE = "$TOPIC_SCREEN/{$TOPIC_ID_ARG}"
} }
@ -28,6 +28,6 @@ object TopicDestinationsArgs {
const val TOPIC_ID_ARG = "topicId" const val TOPIC_ID_ARG = "topicId"
} }
object TopicScreens { object InterestsScreens {
const val TOPIC_SCREEN = "topic" const val TOPIC_SCREEN = "topic"
} }

Loading…
Cancel
Save