How to preload Coil image in Compose?
Asked Answered
U

3

14

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.

Underlay answered 16/10, 2021 at 17:29 Comment(4)
As per my experience, coil would cache the image so well that even after app-destruction and re-creation, the cached image exists, I do not even need to be connected to the internet to have it rendered.Niblick
Just post your implementation, preferably some levels of parent containers so that we can dig the problem out.Niblick
@MARSK I added a sample of my code to reproduce the problem. But the problem is clearly that the Image() of other page isn't showed so the request of the coil painter isn't madeUnderlay
@MARSK also the problem isn't about the cache, image I load can change a lot and are randomly showed here. From what I saw the cache work perfectly.Underlay
V
19

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.

Vincenty answered 17/10, 2021 at 8:10 Comment(5)
And then what am I supposed to do to actually load the prefetched image?Particularism
@Particularism use any of Coil ways to load image in Compose, check out documentation.Vincenty
I still can't figure it out. I have the LaunchedEffect block you mentioned and then my code is like this: item.photos.forEachIndexed { index, url -> if (index == photoIndex) { AsyncImage( model = url, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier .fillMaxSize() .clip(RoundedCornerShape(10.dp)) ) } } And it will just make a new request, instead of using the image from the cached/prefetched from the launchedeffect blockParticularism
@Particularism LaunchedEffect is just a sample of how it can be used in Compose: to make it work LaunchedEffect should be launched before AsyncImage appears so it have time to pre-load. You can also do same request in a view model.Vincenty
@PhilDukhov, yep, the LauchedEffect thing is confusing:)Riddle
I
0

Here is full code for accompanist pager with coil preloading and indicator with video example

Infraction answered 14/12, 2022 at 12:45 Comment(1)
what is the point of such preload? You preload an image and you use it the next momentRiddle
M
0

I would have an instance of this class in a ViewModel when you request for data over the network you can prefetch as you're parsing data. Hope this helps :)

class ImageCache
@Inject constructor(
    @ApplicationContext private val context: Context
) {
    private val cache = DiskCache.Builder()
        .directory(context.cacheDir.resolve("image_cache"))
        .maxSizePercent(0.02)
        .build()

    private val imageLoader = ImageLoader.Builder(context)
        .diskCache { cache }
        .build()

    fun cacheImage(url: String): Deferred<ImageResult> {
        Timber.d("cacheImage: %s", url)
        val request = ImageRequest.Builder(context)
            .data(url)
            .diskCacheKey(url)
            .diskCachePolicy(CachePolicy.ENABLED)
            .build()
        return imageLoader.enqueue(request).job
    }

    @OptIn(ExperimentalCoilApi::class)
    suspend fun getImage(imageUri: String, mimeType: String): File? {
        Timber.d("getImage: %s", imageUri)
        val cachedFile = withContext(Dispatchers.IO) {
            imageLoader.diskCache?.openSnapshot(imageUri)?.data?.toFile()
        }

        cachedFile?.let { file ->
            val bitmap = BitmapFactory.decodeFile(file.path)
            val outputDir = context.cacheDir.resolve("image_cache_converted")
            if (!outputDir.exists()) outputDir.mkdirs()
            val fileName = "${System.currentTimeMillis()}." + when (mimeType) {
                "image/jpeg" -> "jpg"
                "image/png" -> "png"
                else -> "jpg"
            }
            val outputFile = File(
                outputDir,
                fileName
            )

            withContext(Dispatchers.IO) {
                FileOutputStream(outputFile).use { out ->
                    bitmap.compress(
                        when (mimeType) {
                            "image/jpeg" -> Bitmap.CompressFormat.JPEG
                            "image/png" -> Bitmap.CompressFormat.PNG
                            else -> Bitmap.CompressFormat.JPEG
                        }, 100, out
                    )
                }
            }
            return outputFile
        }

        return null

    }
}
Maleate answered 11/6 at 16:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.