4

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?

Sam Chahine
  • 530
  • 1
  • 17
  • 52

1 Answers1

2

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) }
    }
}
FishHawk
  • 414
  • 4
  • 11
  • Hi @FishHawk! Thanks for this, I've updated my question to include setting up an `imageLoader` and `enqeue`ing each request before displaying AsyncImage, however I'm still running into problems where it seems the images are taking a long time to load, could you please have another look and tell me what you think? – Sam Chahine Aug 04 '22 at 08:07
  • We need to check if the image is cached first. You can use `LaunchedEffect(painter.state) { println((painter.state as? AsyncImagePainter.State.Success)?.result?.dataSource) }` to check it. – FishHawk Aug 04 '22 at 13:05
  • I'm sorry I don't quite understand where this would fit into my code that I included in the question though? Doesn't it simply know if it's in the cache by checking the URL in the `dataCacheKey`? Is there a working example online somewhere? – Sam Chahine Aug 04 '22 at 13:09
  • I have updated my answer. I only know that glide has key, I don't know if coil has. – FishHawk Aug 04 '22 at 13:45
  • I don't use `painter` anywhere though, so where should I assume this value from? – Sam Chahine Aug 04 '22 at 14:23
  • Sorry, I forgot that coil2.0's AsyncImage no longer uses painter as parameter. I've updated my answer. – FishHawk Aug 05 '22 at 06:56
  • All good! It worked and the result says "Network". So it's safe to say it's definitely not coming from the cache. – Sam Chahine Aug 05 '22 at 07:29
  • In that case, you can check if there is a key inside `context.imageLoader.diskCache`, after the image is successfully loaded from the network. If it's still not there, I suggest setting respectCacheHeaders to true and using `addHeader` to modify the http header. see https://stackoverflow.com/questions/72981927/coil-image-caching-not-working-with-jetpack-compose/72988170#72988170. By the way, you dont need `enqueue`, which is usually used for preloading. – FishHawk Aug 05 '22 at 08:57
  • I've applied all of this and updated my question in **Update 2**, I included the disk cache source and image key being there – Sam Chahine Aug 06 '22 at 04:04
  • It's weird. You should also check the disk cache before loading. And try opening the same image again after it is cached and disable memory cache and network to see if you can load it from the disk. – FishHawk Aug 06 '22 at 04:37
  • @SamChahine did you fix this? I am having same issue. It keeps reloading (that is until I open a place where I load in a noncompose view. When it flickers -> in compose I get always in Subcompose: as source: MEMORY_CACHE Then loading in a normal view with coil, first time after I get "NETWORK" in compose. and from there on, compose will always take MEMORY_CACHE from there, but just doesn't flicker anymore – rosu alin Aug 01 '23 at 12:05