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
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
import com.karumi.shot.ScreenshotTest
import dev.msfjarvis.lobsters.ui.DarkTestTheme
import dev.msfjarvis.lobsters.ui.LightTestTheme
import kotlin.test.Test
import org.junit.Rule
@ -15,7 +17,7 @@ class LobstersItemTest : ScreenshotTest {
val composeTestRule = createComposeRule()
@Test
fun postsAreRenderedCorrectlyOnScreen() {
fun singlePost() {
composeTestRule.setContent {
DarkTestTheme {
LobstersItem(
@ -29,4 +31,44 @@ class LobstersItemTest : ScreenshotTest {
}
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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
@ -43,7 +45,7 @@ val TEST_POST = SavedPost(
createdAt = "2020-09-21T07:11:14.000-05:00",
commentsUrl = "https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on",
submitterName = "Vigdis",
submitterAvatarUrl = "/avatars/Vigdis-100.png",
submitterAvatarUrl = "/404.html",
tags = listOf("openbsd", "linux", "containers", "hack the planet", "no thanks"),
)
@ -55,97 +57,126 @@ fun LobstersItem(
viewPost: () -> Unit,
viewComments: () -> Unit,
toggleSave: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
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(
modifier = Modifier.padding(start = 12.dp),
modifier = Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Box(
modifier = Modifier.weight(0.8f),
) {
PostDetails(
post,
TagRow(
tags = post.tags,
modifier = Modifier.weight(0.65f),
)
}
Box(
modifier = Modifier.weight(0.1f),
) {
SaveButton(
isSaved,
toggleSave,
isSaved = isSaved,
onClick = toggleSave,
)
Spacer(
modifier = Modifier.width(8.dp),
)
}
Box(
modifier = Modifier.weight(0.1f),
) {
CommentsButton(
onClick = viewComments,
)
}
SubmitterName(
name = post.submitterName,
avatarUrl = post.submitterAvatarUrl,
)
}
}
}
@Composable
fun PostDetails(
post: SavedPost,
fun PostTitle(
title: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp),
) {
Text(
text = post.title,
text = title,
color = titleColor,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(bottom = 4.dp),
)
TagRow(
tags = post.tags,
modifier = Modifier
.padding(bottom = 4.dp),
modifier = Modifier.then(modifier),
)
}
@Composable
fun SubmitterName(
name: String,
avatarUrl: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier.then(modifier),
verticalAlignment = Alignment.CenterVertically,
) {
SubmitterAvatar(
name = name,
avatarUrl = avatarUrl,
)
SubmitterNameText(
name = name,
)
}
}
@Composable
fun SubmitterAvatar(
name: String,
avatarUrl: String,
) {
CoilImage(
data = "${LobstersApi.BASE_URL}/${post.submitterAvatarUrl}",
data = "${LobstersApi.BASE_URL}/$avatarUrl",
contentDescription = stringResource(
R.string.avatar_content_description,
post.submitterName
name,
),
fadeIn = true,
requestBuilder = {
transformations(CircleCropTransformation())
},
modifier = Modifier
.requiredSize(24.dp)
.padding(bottom = 4.dp),
.requiredSize(24.dp),
)
}
@Composable
fun SubmitterNameText(
name: String,
) {
Text(
text = stringResource(id = R.string.submitted_by, post.submitterName),
text = stringResource(id = R.string.submitted_by, name),
modifier = Modifier
.padding(start = 4.dp),
)
}
}
}
@Composable
fun SaveButton(
isSaved: Boolean,
onSaveButtonClick: () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
IconToggleButton(
checked = isSaved,
onCheckedChange = { onSaveButtonClick.invoke() },
onCheckedChange = { onClick.invoke() },
modifier = Modifier
.requiredSize(24.dp),
.requiredSize(32.dp)
.then(modifier),
) {
Crossfade(targetState = isSaved) { saved ->
IconResource(
@ -160,11 +191,13 @@ fun SaveButton(
@Composable
fun CommentsButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
IconButton(
onClick = onClick,
modifier = Modifier
.requiredSize(24.dp),
.requiredSize(32.dp)
.then(modifier),
) {
IconResource(
resourceId = R.drawable.ic_insert_comment_24px,
@ -179,8 +212,10 @@ fun TagRow(
tags: List<String>,
modifier: Modifier = Modifier,
) {
Box(
modifier = Modifier.then(modifier),
) {
FlowLayout(
modifier = Modifier.then(modifier),
horizontalSpacing = 8.dp,
verticalSpacing = 8.dp,
) {
@ -194,6 +229,7 @@ fun TagRow(
)
}
}
}
}
@Composable