166: Refactor LobstersItem r=msfjarvis a=msfjarvis

Breaks apart PostDetails into multiple individual composables and reimplements the layout logic with a more easy to understand mental model.


Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
bors[bot] 2021-03-21 19:10:24 +00:00 committed by GitHub
commit faa6fbcc39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 152 additions and 74 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -1,11 +1,13 @@
package dev.msfjarvis.lobsters.ui.posts package dev.msfjarvis.lobsters.ui.posts
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.test.captureToImage import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.onRoot
import com.karumi.shot.ScreenshotTest import com.karumi.shot.ScreenshotTest
import dev.msfjarvis.lobsters.ui.DarkTestTheme import dev.msfjarvis.lobsters.ui.DarkTestTheme
import dev.msfjarvis.lobsters.ui.LightTestTheme
import kotlin.test.Test import kotlin.test.Test
import org.junit.Rule import org.junit.Rule
@ -15,7 +17,7 @@ class LobstersItemTest : ScreenshotTest {
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
@Test @Test
fun postsAreRenderedCorrectlyOnScreen() { fun singlePost() {
composeTestRule.setContent { composeTestRule.setContent {
DarkTestTheme { DarkTestTheme {
LobstersItem( LobstersItem(
@ -29,4 +31,44 @@ class LobstersItemTest : ScreenshotTest {
} }
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap()) compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
} }
@Test
fun multiplePosts() {
composeTestRule.setContent {
LightTestTheme {
LazyColumn {
items(10) {
LobstersItem(
post = TEST_POST,
viewPost = { /*TODO*/ },
viewComments = { /*TODO*/ },
toggleSave = { /*TODO*/ },
isSaved = true,
)
}
}
}
}
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
}
@Test
fun multiplePostsWithLesserTags() {
composeTestRule.setContent {
LightTestTheme {
LazyColumn {
items(10) {
LobstersItem(
post = TEST_POST.copy(tags = listOf("openbsd", "linux")),
viewPost = { /*TODO*/ },
viewComments = { /*TODO*/ },
toggleSave = { /*TODO*/ },
isSaved = true,
)
}
}
}
}
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
}
} }

View file

@ -4,12 +4,14 @@ import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -43,7 +45,7 @@ val TEST_POST = SavedPost(
createdAt = "2020-09-21T07:11:14.000-05:00", createdAt = "2020-09-21T07:11:14.000-05:00",
commentsUrl = "https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on", commentsUrl = "https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on",
submitterName = "Vigdis", submitterName = "Vigdis",
submitterAvatarUrl = "/avatars/Vigdis-100.png", submitterAvatarUrl = "/404.html",
tags = listOf("openbsd", "linux", "containers", "hack the planet", "no thanks"), tags = listOf("openbsd", "linux", "containers", "hack the planet", "no thanks"),
) )
@ -55,97 +57,126 @@ fun LobstersItem(
viewPost: () -> Unit, viewPost: () -> Unit,
viewComments: () -> Unit, viewComments: () -> Unit,
toggleSave: () -> Unit, toggleSave: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.clickable { viewPost.invoke() }, .clickable { viewPost.invoke() }
.then(modifier),
) { ) {
Column(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 4.dp),
) {
PostTitle(
title = post.title,
modifier = Modifier
.padding(bottom = 4.dp),
)
Row( Row(
modifier = Modifier.padding(start = 12.dp), modifier = Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Box( TagRow(
modifier = Modifier.weight(0.8f), tags = post.tags,
) { modifier = Modifier.weight(0.65f),
PostDetails(
post,
) )
}
Box(
modifier = Modifier.weight(0.1f),
) {
SaveButton( SaveButton(
isSaved, isSaved = isSaved,
toggleSave, onClick = toggleSave,
)
Spacer(
modifier = Modifier.width(8.dp),
) )
}
Box(
modifier = Modifier.weight(0.1f),
) {
CommentsButton( CommentsButton(
onClick = viewComments, onClick = viewComments,
) )
} }
SubmitterName(
name = post.submitterName,
avatarUrl = post.submitterAvatarUrl,
)
} }
} }
} }
@Composable @Composable
fun PostDetails( fun PostTitle(
post: SavedPost, title: String,
) { modifier: Modifier = Modifier,
Column(
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp),
) { ) {
Text( Text(
text = post.title, text = title,
color = titleColor, color = titleColor,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier modifier = Modifier.then(modifier),
.padding(bottom = 4.dp),
)
TagRow(
tags = post.tags,
modifier = Modifier
.padding(bottom = 4.dp),
) )
}
@Composable
fun SubmitterName(
name: String,
avatarUrl: String,
modifier: Modifier = Modifier,
) {
Row( Row(
modifier = Modifier.then(modifier),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) {
SubmitterAvatar(
name = name,
avatarUrl = avatarUrl,
)
SubmitterNameText(
name = name,
)
}
}
@Composable
fun SubmitterAvatar(
name: String,
avatarUrl: String,
) { ) {
CoilImage( CoilImage(
data = "${LobstersApi.BASE_URL}/${post.submitterAvatarUrl}", data = "${LobstersApi.BASE_URL}/$avatarUrl",
contentDescription = stringResource( contentDescription = stringResource(
R.string.avatar_content_description, R.string.avatar_content_description,
post.submitterName name,
), ),
fadeIn = true, fadeIn = true,
requestBuilder = { requestBuilder = {
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
}, },
modifier = Modifier modifier = Modifier
.requiredSize(24.dp) .requiredSize(24.dp),
.padding(bottom = 4.dp),
) )
}
@Composable
fun SubmitterNameText(
name: String,
) {
Text( Text(
text = stringResource(id = R.string.submitted_by, post.submitterName), text = stringResource(id = R.string.submitted_by, name),
modifier = Modifier modifier = Modifier
.padding(start = 4.dp), .padding(start = 4.dp),
) )
} }
}
}
@Composable @Composable
fun SaveButton( fun SaveButton(
isSaved: Boolean, isSaved: Boolean,
onSaveButtonClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
IconToggleButton( IconToggleButton(
checked = isSaved, checked = isSaved,
onCheckedChange = { onSaveButtonClick.invoke() }, onCheckedChange = { onClick.invoke() },
modifier = Modifier modifier = Modifier
.requiredSize(24.dp), .requiredSize(32.dp)
.then(modifier),
) { ) {
Crossfade(targetState = isSaved) { saved -> Crossfade(targetState = isSaved) { saved ->
IconResource( IconResource(
@ -160,11 +191,13 @@ fun SaveButton(
@Composable @Composable
fun CommentsButton( fun CommentsButton(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
IconButton( IconButton(
onClick = onClick, onClick = onClick,
modifier = Modifier modifier = Modifier
.requiredSize(24.dp), .requiredSize(32.dp)
.then(modifier),
) { ) {
IconResource( IconResource(
resourceId = R.drawable.ic_insert_comment_24px, resourceId = R.drawable.ic_insert_comment_24px,
@ -178,9 +211,11 @@ fun CommentsButton(
fun TagRow( fun TagRow(
tags: List<String>, tags: List<String>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) {
Box(
modifier = Modifier.then(modifier),
) { ) {
FlowLayout( FlowLayout(
modifier = Modifier.then(modifier),
horizontalSpacing = 8.dp, horizontalSpacing = 8.dp,
verticalSpacing = 8.dp, verticalSpacing = 8.dp,
) { ) {
@ -195,6 +230,7 @@ fun TagRow(
} }
} }
} }
}
@Composable @Composable
@Preview @Preview