Improving For You onboaring [http://b/223204846]

Test: updated tests that were failing in firebase testlab due to the UI change.

Change-Id: If892a7a81eecc69bf71f41d75c11af363903f962
pull/2/head
Ran Nachmany 4 years ago committed by Don Turner
parent 6f1206ef92
commit 9ebea8ad29

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.ui
import androidx.compose.material.window.ExperimentalMaterialWindowApi import androidx.compose.material.window.ExperimentalMaterialWindowApi
import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertIsSelected
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
@ -68,6 +69,7 @@ class NavigationTest {
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 topics: String
private lateinit var sampleTopic: String
@Before @Before
fun setup() { fun setup() {
@ -79,14 +81,15 @@ class NavigationTest {
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) topics = getString(R.string.following)
sampleTopic = "Headlines"
} }
} }
@Test @Test
fun firstScreen_isForYou() { fun firstScreen_isForYou() {
composeTestRule.apply { composeTestRule.apply {
// VERIFY first topic is displayed // VERIFY for you is selected
onNodeWithText("HEADLINES").assertExists() onNodeWithText(forYou).assertIsSelected()
} }
} }
@ -101,13 +104,13 @@ class NavigationTest {
fun navigationBar_navigateToPreviouslySelectedTab_restoresContent() { fun navigationBar_navigateToPreviouslySelectedTab_restoresContent() {
composeTestRule.apply { composeTestRule.apply {
// GIVEN the user follows a topic // GIVEN the user follows a topic
onNodeWithText("HEADLINES").performClick() onNodeWithText(sampleTopic).performClick()
// WHEN the user navigates to the Topics destination // WHEN the user navigates to the Topics destination
onNodeWithText(topics).performClick() onNodeWithText(topics).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
onNodeWithText("HEADLINES").assertIsOn() onNodeWithContentDescription(sampleTopic).assertIsOn()
} }
} }
@ -118,11 +121,11 @@ class NavigationTest {
fun navigationBar_reselectTab_keepsState() { fun navigationBar_reselectTab_keepsState() {
composeTestRule.apply { composeTestRule.apply {
// GIVEN the user follows a topic // GIVEN the user follows a topic
onNodeWithText("HEADLINES").performClick() onNodeWithText(sampleTopic).performClick()
// WHEN the user taps the For You navigation bar item // WHEN the user taps the For You navigation bar item
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
onNodeWithText("HEADLINES").assertIsOn() onNodeWithContentDescription(sampleTopic).assertIsOn()
} }
} }
@ -177,7 +180,7 @@ class NavigationTest {
// 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()
// THEN the app shows the For You destination // THEN the app shows the For You destination
onNodeWithText("HEADLINES").assertExists() onNodeWithText(forYou).assertExists()
} }
} }
} }

@ -21,8 +21,6 @@ import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertIsOff
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
@ -93,21 +91,18 @@ class ForYouScreenTest {
} }
composeTestRule composeTestRule
.onNodeWithText("HEADLINES") .onNodeWithText("Headlines")
.assertIsDisplayed() .assertIsDisplayed()
.assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
.onNodeWithText("UI") .onNodeWithText("UI")
.assertIsDisplayed() .assertIsDisplayed()
.assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
.onNodeWithText("TOOLS") .onNodeWithText("Tools")
.assertIsDisplayed() .assertIsDisplayed()
.assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
@ -157,21 +152,18 @@ class ForYouScreenTest {
} }
composeTestRule composeTestRule
.onNodeWithText("HEADLINES") .onNodeWithText("Headlines")
.assertIsDisplayed() .assertIsDisplayed()
.assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
.onNodeWithText("UI") .onNodeWithText("UI")
.assertIsDisplayed() .assertIsDisplayed()
.assertIsOn()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule
.onNodeWithText("TOOLS") .onNodeWithText("Tools")
.assertIsDisplayed() .assertIsDisplayed()
.assertIsOff()
.assertHasClickAction() .assertHasClickAction()
composeTestRule composeTestRule

@ -18,32 +18,40 @@ package com.google.samples.apps.nowinandroid.feature.foryou
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.grid.GridCells.Fixed
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
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.runtime.key import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign
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.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
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.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
@ -51,6 +59,12 @@ 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.NewsResourceCardExpanded
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.component.NiaToggleButton
import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTypography
import com.google.samples.apps.nowinandroid.feature.foryou.ForYouFeedUiState.PopulatedFeed
import com.google.samples.apps.nowinandroid.feature.foryou.ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection
import com.google.samples.apps.nowinandroid.feature.foryou.ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
@Composable @Composable
@ -59,7 +73,6 @@ fun ForYouRoute(
viewModel: ForYouViewModel = hiltViewModel() viewModel: ForYouViewModel = hiltViewModel()
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
ForYouScreen( ForYouScreen(
modifier = modifier, modifier = modifier,
uiState = uiState, uiState = uiState,
@ -77,98 +90,149 @@ fun ForYouScreen(
onNewsResourcesCheckedChanged: (Int, Boolean) -> Unit, onNewsResourcesCheckedChanged: (Int, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box(modifier = modifier.fillMaxSize()) { LazyColumn(
modifier = modifier.fillMaxSize()
) {
when (uiState) { when (uiState) {
ForYouFeedUiState.Loading -> { is ForYouFeedUiState.Loading -> {
NiaLoadingIndicator( item {
modifier = modifier, NiaLoadingIndicator(
contentDesc = stringResource(id = R.string.for_you_loading), modifier = modifier,
) contentDesc = stringResource(id = R.string.for_you_loading),
)
}
} }
is ForYouFeedUiState.PopulatedFeed -> { is PopulatedFeed -> {
LazyColumn { when (uiState) {
when (uiState) { is FeedWithTopicSelection -> {
is ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection -> { item {
TopicSelection(uiState, onTopicCheckedChanged, saveFollowedTopics) TopicSelection(uiState, onTopicCheckedChanged)
}
item {
// Done button
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = saveFollowedTopics,
enabled = uiState.canSaveSelectedTopics,
modifier = Modifier
.padding(horizontal = 40.dp)
.width(364.dp)
) {
Text(text = stringResource(R.string.done))
}
}
} }
is ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection -> Unit
} }
is FeedWithoutTopicSelection -> Unit
}
items(uiState.feed) { (newsResource: NewsResource, isBookmarked: Boolean) -> items(uiState.feed) { (newsResource: NewsResource, isBookmarked: Boolean) ->
val launchResourceIntent = val launchResourceIntent =
Intent(Intent.ACTION_VIEW, Uri.parse(newsResource.url)) Intent(Intent.ACTION_VIEW, Uri.parse(newsResource.url))
val context = LocalContext.current val context = LocalContext.current
NewsResourceCardExpanded( NewsResourceCardExpanded(
newsResource = newsResource, newsResource = newsResource,
isBookmarked = isBookmarked, isBookmarked = isBookmarked,
onToggleBookmark = { onClick = { startActivity(context, launchResourceIntent, null) },
onNewsResourcesCheckedChanged(newsResource.id, !isBookmarked) onToggleBookmark = {
}, onNewsResourcesCheckedChanged(newsResource.id, !isBookmarked)
onClick = { }
startActivity(context, launchResourceIntent, null) )
},
)
}
} }
} }
} }
} }
} }
/** @Composable
* The topic selection items private fun TopicSelection(
*/ uiState: ForYouFeedUiState,
private fun LazyListScope.TopicSelection( onTopicCheckedChanged: (Int, Boolean) -> Unit
uiState: ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection,
onTopicCheckedChanged: (Int, Boolean) -> Unit,
saveFollowedTopics: () -> Unit
) { ) {
item { Column(Modifier.padding(top = 24.dp)) {
FlowRow(
mainAxisSpacing = 8.dp, Text(
crossAxisSpacing = 8.dp, text = stringResource(R.string.onboarding_guidance_title),
modifier = Modifier.padding(horizontal = 40.dp) textAlign = TextAlign.Center,
) { modifier = Modifier.fillMaxWidth(),
uiState.topics.forEach { (topic, isSelected) -> style = NiaTypography.titleMedium
key(topic.id) { )
// TODO: Add toggleable semantics
OutlinedButton( Text(
onClick = { text = stringResource(R.string.onboarding_guidance_subtitle),
onTopicCheckedChanged(topic.id, !isSelected) modifier = Modifier
}, .fillMaxWidth()
shape = RoundedCornerShape(50), .padding(top = 8.dp, start = 16.dp, end = 16.dp),
colors = if (isSelected) { textAlign = TextAlign.Center,
ButtonDefaults.buttonColors() style = NiaTypography.bodyMedium
} else { )
ButtonDefaults.outlinedButtonColors()
},
modifier = Modifier.toggleable(
value = isSelected, role = Role.Button, onValueChange = {}
)
) {
Text(
text = topic.name.uppercase(),
)
}
}
}
}
}
item { LazyHorizontalGrid(
Button( rows = Fixed(3),
onClick = saveFollowedTopics, horizontalArrangement = Arrangement.spacedBy(12.dp),
enabled = uiState.canSaveSelectedTopics, verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier modifier = Modifier
.padding(horizontal = 40.dp) .height(192.dp)
.padding(top = 24.dp, bottom = 24.dp)
.fillMaxWidth() .fillMaxWidth()
) { ) {
Text(text = stringResource(R.string.done)) val state: FeedWithTopicSelection = uiState as FeedWithTopicSelection
items(state.topics) {
SingleTopicButton(
name = it.topic.name,
topicId = it.topic.id,
isSelected = it.isFollowed,
onClick = onTopicCheckedChanged
)
}
} }
} }
} }
@Composable
private fun SingleTopicButton(
name: String,
topicId: Int,
isSelected: Boolean,
onClick: (Int, Boolean) -> Unit
) {
Box(
modifier = Modifier
.width(264.dp)
.height(56.dp)
.padding(start = 12.dp, end = 8.dp)
.background(
MaterialTheme.colors.surface,
shape = RoundedCornerShape(corner = CornerSize(8.dp))
)
.clickable(onClick = { onClick(topicId, !isSelected) }),
contentAlignment = Alignment.CenterStart
) {
Text(
text = name,
style = NiaTypography.titleSmall,
modifier = Modifier.padding(12.dp),
color = MaterialTheme.colors.onSurface
)
NiaToggleButton(
checked = isSelected,
modifier = Modifier.align(alignment = Alignment.CenterEnd),
onCheckedChange = { checked -> onClick(topicId, !isSelected) },
icon = {
Icon(imageVector = NiaIcons.Add, contentDescription = name)
},
checkedIcon = {
Icon(imageVector = NiaIcons.Check, contentDescription = name)
}
)
}
}
@Preview @Preview
@Composable @Composable
fun ForYouScreenLoading() { fun ForYouScreenLoading() {
@ -184,7 +248,7 @@ fun ForYouScreenLoading() {
@Composable @Composable
fun ForYouScreenTopicSelection() { fun ForYouScreenTopicSelection() {
ForYouScreen( ForYouScreen(
uiState = ForYouFeedUiState.PopulatedFeed.FeedWithTopicSelection( uiState = FeedWithTopicSelection(
topics = listOf( topics = listOf(
FollowableTopic( FollowableTopic(
topic = Topic( topic = Topic(
@ -311,7 +375,7 @@ fun ForYouScreenTopicSelection() {
@Composable @Composable
fun PopulatedFeed() { fun PopulatedFeed() {
ForYouScreen( ForYouScreen(
uiState = ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( uiState = FeedWithoutTopicSelection(
feed = emptyList() feed = emptyList()
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },

@ -20,4 +20,6 @@
<string name="done">Done</string> <string name="done">Done</string>
<string name="for_you_loading">Loading for you…</string> <string name="for_you_loading">Loading for you…</string>
<string name="navigate_up">Navigate up</string> <string name="navigate_up">Navigate up</string>
<string name="onboarding_guidance_title">What are you interested in?</string>
<string name="onboarding_guidance_subtitle">Updates from topics you follow will appear here. Follow some things to get started.</string>
</resources> </resources>

@ -4,7 +4,7 @@ androidDesugarJdkLibs = "1.1.5"
androidGradlePlugin = "7.1.2" androidGradlePlugin = "7.1.2"
androidxActivity = "1.4.0" androidxActivity = "1.4.0"
androidxAppCompat = "1.3.0" androidxAppCompat = "1.3.0"
androidxCompose = "1.2.0-alpha03" androidxCompose = "1.2.0-alpha06"
androidxMaterialWindow = "1.2.0-SNAPSHOT" androidxMaterialWindow = "1.2.0-SNAPSHOT"
androidxComposeMaterial3 = "1.0.0-alpha07" androidxComposeMaterial3 = "1.0.0-alpha07"
androidxCore = "1.7.0" androidxCore = "1.7.0"

@ -20,7 +20,7 @@ dependencyResolutionManagement {
// Register the AndroidX snapshot repository first so snapshots don't attempt (and fail) // Register the AndroidX snapshot repository first so snapshots don't attempt (and fail)
// to download from the non-snapshot repositories // to download from the non-snapshot repositories
maven { maven {
url 'https://androidx.dev/snapshots/builds/8273139/artifacts/repository' url 'https://androidx.dev/snapshots/builds/8350530/artifacts/repository'
content { content {
// The AndroidX snapshot repository will only have androidx artifacts, don't // The AndroidX snapshot repository will only have androidx artifacts, don't
// bother trying to find other ones // bother trying to find other ones

Loading…
Cancel
Save