I'm migrating my app and for the grid view I used LazyVerticalGrid that loads images using coilPainter but the scrolling stutters a lot and it feels laggy. Does anyone have a solution or a better implementation for this?
val photos: List<Photo>? by photosViewModel.photosLiveData.observeAsState(null)
Surface(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier.fillMaxSize()
) {
LazyVerticalGrid(
cells = GridCells.Fixed(3)
) {
photos?.let { photosList ->
items(photosList) { photo ->
Image(
modifier = Modifier.padding(2.dp).clickable {
photosViewModel.onPhotoClicked(photo)
},
painter = rememberCoilPainter(photo.url),
contentDescription = stringResource(R.string.cd_details_photo),
contentScale = ContentScale.Fit,
alignment = Alignment.Center,
)
}
}
}
}
}
Update
Trying to move rememberCoilPainter above Image like val painter = rememberCoilPainter(photo.url) and use the painter inside the Image did not work
items takes an additional parameter key, assign this parameter to a unique value like the index of each photo of your list, If you don't know how does that relate to scrolling jank? Or what the heck am I talking about? check out the following documentation for a detailed explanation:
https://developer.android.com/jetpack/compose/lifecycle#add-info-smart-recomposition
After updating my compose image loading library to coil-compose I was forced to set a size to either the request builder or the image itself. The problem of the stuttering was caused by allowing the photo to load the original size and thus recomposing was done multiple times and loading the image had to wait for the picture to load. By setting e fixed height to the Image fixed the issue because the Images() were created and the recomposition was made only for the picture inside not for the Image itself with different height.
Related
I am trying to show a list of installed apps and a checkbox next to each app. The scrolling is terrible, it's noticably laggy. It looks like i am doing something wrong but i can't figure it out. how can I improve it?
#Composable
fun AppList(infoList: MutableList<android.content.pm.ResolveInfo>) {
val ctx = LocalContext.current
LazyColumn {
items(infoList) { info ->
var isChecked by remember { mutableStateOf(false) }
var icon by remember { mutableStateOf(info.loadIcon(ctx.packageManager)) }
var label by remember { mutableStateOf( info.loadLabel(ctx.packageManager).toString()) }
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()
) {
Image(
painter = rememberImagePainterMine(icon),
contentDescription = label,
contentScale = ContentScale.Fit,
modifier = Modifier.padding(16.dp)
)
// Display the app name
Text(
text =label,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f)
)
// Display the checkbox on the right
Box(modifier = Modifier.clickable { isChecked = !isChecked }) {
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it },
modifier = Modifier.padding(16.dp)
)
}
}
}
}
}
#Composable
fun rememberImagePainterMine(drawable: Drawable): Painter = remember(drawable) {
object : Painter() {
override val intrinsicSize: Size
get() = Size(drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat())
override fun DrawScope.onDraw() {
drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
drawable.draw(this.drawContext.canvas.nativeCanvas)
}
}
}
Are you running your app in release mode? It makes a big difference as to how smooth Jetpack Compose will be (due to the amount of debug code under the hood).
Google themselves explain it as follows:
If you're finding performance issues, make sure to try running your app in release mode. Debug mode is useful for spotting lots of problems, but it imposes a significant performance cost, and can make it hard to spot other code issues that might be hurting performance. You should also use the R8 compiler to remove unnecessary code from your app. By default, building in release mode automatically uses the R8 compiler.
Additionally, you should consider using keys with your LazyColumn to help Compose figure out if your data moves around or changes completely.
Without your help, Compose doesn't realize that unchanged items are just being moved in the list. Instead, Compose thinks the old "item 2" was deleted and a new one was created, and so on for item 3, item 4, and all the way down. The result is that Compose recomposes every item on the list, even though only one of them actually changed.
The solution here is to provide item keys. Providing a stable key for each item lets Compose avoid unnecessary recompositions. In this case, Compose can see that the item now at spot 3 is the same item that used to be at spot 2. Since none of the data for that item has changed, Compose doesn't have to recompose it.
My current goal is to achieve view that can represent a table, but this table can contain pretty much elements in its columns and rows. My current idea is to populate LazyColumn with LazyRows.
#Composable
internal fun Table() {
LazyColumn {
items(count = 100) {
LazyRow {
items(10) {
Box(
modifier = Modifier
.size(100.dp)
.border(width = 1.dp, color = Color.Black)
)
}
}
}
}
}
But there is a huge problem: i don't want the rows to be able to scroll by themselves, i want any scrolling to be shared. Unfortunately, i can't figure out gow to achieve that. As far as i know lazy grid can't help me either.
I also tried to use same instance of LazyListState, but it doesn't work.
In a new version appered new layout called "LazyLayout" - it's somewhat of your custom lazy layout builder. This should solve my problem, although i haven't tried it yet
I have a pager (Accompanist) with image that are get from web with Coil in Compose.
The rememberPainter() seem to only call the request when the Image composable is shown for the first time.
So when I slide page in the pager, the Image show only at that moment and so we have to wait a moment.
Any way to force the rememberPainter (Coil) to preload ?
Edit 1 :
Here is a simple version of my implementation (with lots of stuff around removed but that had no impact on the result) :
#Composable
private fun Foo(imageList: List<ImageHolder>) {
if (imageList.isEmpty())
return
val painterList = imageList.map {
rememberImagePainter(
data = it.getDataForPainter(),
builder = {
crossfade(true)
})
}
ImageLayout(imageList, painterList)
}
#Composable
fun ImageLayout(imageList: List<ImageHolder>, painterList: List<Painter>) {
HorizontalPager(
count = imageList.size,
modifier = Modifier.fillMaxWidth(),
) { page ->
Image(
painter = painterList[page],
"",
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(
imageList[page].colorAverage
),
contentScale = ContentScale.Crop
)
}
}
I tried with having directly the rememberImagePainter at the Image too just in case.
But the problem is clearly that Image() of page 2, 3, 4 isn't rendered so Image don't do the call to the painter. I tried to look how it work inside but couldn't find.
Edit 2 : I found a workaround but it's not clean
for (painter in painterList)
Image(painter = painter, contentDescription = "", modifier = Modifier.size(0.001.dp))
It force coil to load image and with a size really small like 0.001.dp (with 0 it don't load).
Also the problem is that in the builder you need to force a size or it will just load one pixel, so I force the full size of image as I don't know what size would have been available for the image.
In the Coil documentation there is a section about preloading. Depending on your architecture, you can do this in different places, the easiest is to use LaunchedEffect:
val context = LocalContext.current
LaunchedEffect(Unit) {
val request = ImageRequest.Builder(context)
.data("https://www.example.com/image.jpg")
// Optional, but setting a ViewSizeResolver will conserve memory by limiting the size the image should be preloaded into memory at.
// For example you can set static size, or size resolved with Modifier.onSizeChanged
// .size(coil.size.PixelSize(width = 100, height = 100))
// or resolve display size if your images are full screen
// .size(DisplaySizeResolver(context))
.build()
context.imageLoader.enqueue(request)
}
Before version 2.0.0 use LocalImageLoader.current instead of context.imageLoader to get the image loader.
Here is full code for accompanist pager with coil preloading and indicator with video example
I'm migrating from XML layouts to Jetpack Compose and noticed that the Image composable does not scale an image as good as ImageView when the image gets cropped.
The image on the top is added via Jetpack Compose, the one at the bottom is an ImageView.
For comparison, I added them next to each other like this:
Column {
Image(
painter = painterResource(id = R.drawable.my_image),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(280.dp)
)
Spacer(
modifier = Modifier.height(4.dp)
)
AndroidView(
factory = { context ->
ImageView(context).apply {
scaleType = ImageView.ScaleType.CENTER_CROP
setImageResource(R.drawable.my_image)
}
},
modifier = Modifier.size(280.dp)
)
}
The effect is depending on the image which is used and I noticed it's mostly promiment on low-end devices, however I would expect Jetpack Compose to have the same or higher image quality, as it is the new framework for building UI on Android.
Is there something I'm missing which can give me the same quality as ImageView?
I would like to display an image with the following rules:
The image is remote and needs to load on runtime.
We don't know what the image size is until it's loaded.
The Image view should take the full width of the parent, and it should automatically adjust its height to match the remotely loaded image, so that no cropping/stretching occurs.
I tried using the Coil dependency and I have this code:
Image(
painter = rememberImagePainter(viewModel.fullImageUrl),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3.21428f) // TODO: Figure out how to set height/ratio based on image itself
)
When I remove the Image's contentScale and modifier, its height always seems to be zero. I'm not sure what I should do to make Image fill the max width of its parent while determining its own size after Coil loads the image file.
As you set flexible width with fillMaxWidth, you need to use ContentScale.FillWidth for your contentScale:
Image(
painter = rememberImagePainter(
"https://via.placeholder.com/300.png",
),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
)
The following will work fine if your image has enough priority. But in some cases coil won't start loading because it's gonna think that image is zero size so no need to display it. For such case you can add size(OriginalSize) parameter to the builder:
rememberImagePainter(
"https://via.placeholder.com/300.png",
builder = {
size(OriginalSize)
}
)
This parameter makes your view download and put into the memory full size of the image, without optimizing it for the current view. So use it carefully, only if you're sure the image won't be too big and you really can't add use size modifiers.
If image is getting loaded but still not takes enough space, you can set aspect ratio like this:
val painter = rememberImagePainter("https://via.placeholder.com/300.png")
Image(
painter = painter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxWidth()
.then(
(painter.state as? ImagePainter.State.Success)
?.painter
?.intrinsicSize
?.let { intrinsicSize ->
Modifier.aspectRatio(intrinsicSize.width / intrinsicSize.height)
} ?: Modifier
)
)
If none of these helped and painter.state is not changing from Empty, try hardcode height ?: Modifier.height(10.dp) to let image start loading