Kotlin AsyncImage Coil not caching images properly, keeps reloading each time

Viewed 206

I'm using the Coil dependency, so I can harness AsyncImage.

This is how I'm displaying my images:

    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(img)
            .crossfade(true)
            .memoryCachePolicy(
                CachePolicy.ENABLED)
            .build(),
        placeholder = painterResource(R.drawable.default_profile_picture),
        contentDescription = "Profile Picture",
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .clip(RoundedCornerShape(100.dp))
            .size(100.dp)
    )

The problem I'm having is the caching. I expected that every time an image was downloaded, it was cached and upon app-open, it would simply appear!

However, every time I start the app, I see the placeholder for a short while and then the image is downloaded again. Am I calling AsyncImage incorrectly?

Update

I've updated how I can AsyncImage, using an imageLoader I in my ComponentActivity() class, with context being applicationContext:

override fun newImageLoader(): ImageLoader = ImageLoader.Builder(applicationContext)
        .diskCache {
            DiskCache.Builder()
                .directory(applicationContext.cacheDir.resolve("image_cache"))
                .maxSizePercent(0.25)
                .build()
        }
        .networkCachePolicy(CachePolicy.ENABLED)
        .diskCachePolicy(CachePolicy.ENABLED)
        .respectCacheHeaders(false)
        .addLastModifiedToFileCacheKey(true)
        .build()

And this is in my @Composable

         val request: ImageRequest = ImageRequest.Builder(LocalContext.current.applicationContext)
                .data(profile.value?.img)
                .crossfade(true)
                .diskCacheKey(profile.value?.img)
                .build()

            LocalContext.current.applicationContext.imageLoader.enqueue(request)

            AsyncImage(
                model = request,
                placeholder = painterResource(R.drawable.default_profile_picture),
                contentDescription = "Profile Picture",
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .clip(RoundedCornerShape(100.dp))
                    .size(50.dp)
            )

However, it seems that the images are actually taking longer to load? I even added the exact diskCacheKey I expect them to be saved at.

Could anyone please tell me where I'm doing something incorrect?

Update 2

This is my updated ImageLoader:

 override fun newImageLoader(): ImageLoader = ImageLoader.Builder(applicationContext)
        .diskCache {
            DiskCache.Builder()
                .directory(applicationContext.cacheDir.resolve("image_cache"))
                .maxSizePercent(0.25)
                .build()
        }
        .networkCachePolicy(CachePolicy.ENABLED)
        .diskCachePolicy(CachePolicy.ENABLED)
        .respectCacheHeaders(true)
        .build()

And this is how I display images:

val request: ImageRequest = ImageRequest.Builder(LocalContext.current.applicationContext)
   .data(space.img)
   .crossfade(true)
   .diskCacheKey(space.img)
   .diskCachePolicy(CachePolicy.ENABLED)
   .setHeader("Cache-Control", "max-age=31536000")
   .build()


AsyncImage(
   model = request,
   placeholder = painterResource(R.drawable.default_space_picture),
   contentDescription = "Space Picture",
   contentScale = ContentScale.Crop,
   modifier = Modifier
       .clip(RoundedCornerShape(100.dp))
       .size(50.dp)
)

And when I try to do:

SubcomposeAsyncImage(
    model = request,
    contentDescription = "Profile Picture",
    contentScale = ContentScale.Crop,
    modifier = Modifier
        .clip(RoundedCornerShape(100.dp))
        .size(50.dp)
) {
    val state = painter.state
    if (state is AsyncImagePainter.State.Success) {
        SubcomposeAsyncImageContent()
        LaunchedEffect(Unit) {
            Log.d("DATASOURCE", state.result.dataSource.toString())
            println(context.imageLoader.diskCache?.get(space.img)?.data)
        }
    }
}

I get this:

2022-08-06 14:02:30.928 5225-5225/com.app.app D/DATASOURCE: NETWORK
2022-08-06 14:02:30.929 5225-5225/com.app.app I/System.out: /data/user/0/com.billbandits.hero/cache/image_cache/746b90d410c17e9d5a5d705a69e69d45afe12b20b8f234972e7394a6052e8033.1

So my question now is, how can there be a disk cache key, and it still be requesting the image from Network?

1 Answers

You need disk cache instead of memory cache. But both are enabled by default.

The problem is that by default, coil decides whether to disk cache based on the Cache-Control http header field. If you need to ignore Cache-Control, you can set respectCacheHeaders for ImageLoader.

        /**
         * Enables support for network cache headers. If enabled, this image loader will respect the
         * cache headers returned by network responses when deciding if an image can be stored or
         * served from the disk cache. If disabled, images will always be served from the disk cache
         * (if present) and will only be evicted to stay under the maximum size.
         *
         * Default: true
         */
        fun respectCacheHeaders(enable: Boolean) = apply {
            this.options = this.options.copy(respectCacheHeaders = enable)
        }

To check what data source is used for this image, you can use SubcomposeAsyncImage. For example:

SubcomposeAsyncImage(
    model = request,
    contentDescription = "Profile Picture",
    contentScale = ContentScale.Crop,
    modifier = Modifier
        .clip(RoundedCornerShape(100.dp))
        .size(50.dp)
) {
    val state = painter.state
    if (state is AsyncImagePainter.State.Success) {
        SubcomposeAsyncImageContent()
        LaunchedEffect(Unit) { println(state.result.dataSource) }
    }
}
Related