I'm building a simple app in Jetpack Compose. In the Home section of the app, I display a list of items. Nothing to fancy.
I do this using a custom card inside a LazyColumn
.
When the app loads, the list is shown and then the items are shown for the first time. Then I scroll down and all the items are shown as expected in the custom card
.
The issue comes up when I do scroll up and then scroll down again, basically recomposing/rerendering the items.
Then the image
of the custom card ignores its contentScale
and its modifier
completely and breaks the layout. Something extremely odd.
custom card
with columns
and rows
. ConstraintLayout
. remember
the modidier
. custom card
where it has a defined height
(180.dp) and a fillMaxWidth
.image
should be placed on the start
of the custom card
, its height should be fixed and the width as well (e.g. width: 124.dp, height: 180.dp).@Composable
fun ProductCard(
data: ProductDisplayData,
onClick: (ProductDisplayData) -> Unit = {}
) {
val constraints = ConstraintSet {
val imageSection = createRefFor("imageSection")
val textSection = createRefFor("textSection")
constrain(imageSection) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(textSection.start)
width = Dimension.value(128.dp)
height = Dimension.value(180.dp)
}
constrain(textSection) {
top.linkTo(parent.top)
start.linkTo(imageSection.end)
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
}
ConstraintLayout(constraints, modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(vertical = 8.dp)
.clickable { onClick(data) }
) {
if (data.image != null) {
Box(
modifier = Modifier
.layoutId("imageSection")
.height(180.dp)
.width(128.dp)
.clip(RoundedCornerShape(8.dp))
.clipToBounds()
) {
Image(
bitmap = data.image.asImageBitmap(),
contentDescription = "Product Image",
modifier = Modifier
.fillMaxSize()
)
}
}
Column(
modifier = Modifier
.layoutId("textSection")
.fillMaxWidth()
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = data.name,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Text(text = "${data.price}$" )
}
Text(text = data.brand, fontWeight = FontWeight.Bold, fontSize = 14.sp)
Text(text = data.size, modifier = Modifier, fontWeight = FontWeight.SemiBold, fontSize = 12.sp, fontStyle = FontStyle.Italic)
Text(text = data.category, fontWeight = FontWeight.SemiBold, fontSize = 12.sp)
LazyRow {
items(data.tags) { tag ->
Chip(
name = tag,
isSelected = false,
onSelectionChanged = {}
)
}
}
Text(
text = data.description,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Light,
fontSize = 12.sp
)
}
}
}
This is the second time, basically scrolled up and down again. In this case the contentScale has completely gone away.
Also look at the corner radius (.clip) of the image at the first time the item is shown.
The corner radius (.clip) is gone on the second time the item is shown, so basically also the modifiers are gone.
The only peculiarity of the image
is that it comes in the response as a base64
string, and it is converted to a bitmap
from it. That's why I'm using Image
and not coil (AsyncImage)
to display the image.
I can not place the base64 string for this MRE due to its length.
In order to test it, please, convert an image to base64 string.
The ProductCard
code is posted above.
data class ProductDisplayData(
val id: Int,
val name: String,
val description: String,
val size: String,
val ownerName: String,
val ownerID: Int,
val price: String,
val category: String,
val brand: String,
val tags: List<String>,
val image: Bitmap?
)
val products = listOf(
ProductDisplayData(
id = 0,
name = "Polo",
description = "Nice slim fit polo shirt, perfect for casual wear, available in multiple colors",
size = "Small",
price = "120",
image = bitmapFromBase64String,
brand = "Boss",
category = "Shirts",
tags = listOf("Shirts", "Boss", "Slim fit", "Casual", "Polo", "Small"),
ownerName = "John Doe",
ownerID = 0
)
//repeat it until the list overflows...
)
@Composable
fun HomeScreen() {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(vertical = 16.dp),
) {
items(products) { product ->
ProductCard(
data = product
) { productSelected ->
// do action... irrelevant for this issue.
}
}
}
}
Very interesting detail is that, if I test it using coil (AsyncImage)
applying a hardcoded url, this issue doesn't happens and it works correctly. Am I in front of a androidx.compose.foundation.Image
Bug?
I was 2 days into this ultra odd issue. I fixed it using the Coil
function rememberAsyncImagePainter
. I'm not 100% sure if this is the more appropriate approach but it works as it should.
I will be happy to read thoughts :), anyways thanks @Leviathan for your time.
This is how the Image
code looks like:
Image(
contentScale = ContentScale.Crop,
painter = rememberAsyncImagePainter(it),
contentDescription = "Product Image",
modifier = Modifier
.clipToBounds()
.height(180.dp)
.width(128.dp)
.layoutId("imageSection")
.clip(RoundedCornerShape(8.dp))
)