firecoil icon indicating copy to clipboard operation
firecoil copied to clipboard

Add support for diskcache

Open ludovicroland opened this issue 3 years ago • 2 comments

Hello,

For a project, I was trying to do the same thing as you but using Jetpack compose and the Composable AsyncImage from the Coil library.

Using your StorageReferenceFetcher and StorageReferenceKeyer classes and a custom ImageLoader is working correctly but your StorageReferenceFetcher does not support diskCache.

Reading the source code of the HttpUriFetcher class from the Coil library, in order to add diskcache support, we need to read and write into the diskcache into the fetch method.

But I cannot find a way to implement a working way to do that. Did you already did that? Thank your for your help :)

ludovicroland avatar May 13 '22 20:05 ludovicroland

@ludovicroland Oh, thanks for bringing this up. I thought diskCache was enabled by default. I'll try to implement it on the StorageReferenceFetcher and will share an update here once it's done :)

thatfiredev avatar May 17 '22 17:05 thatfiredev

@thatfiredev : On my side, I did a first implementation based on the source code I found into the HttpUriFetcher. It is probably not very optimized since it's the first time I use the okio library.

But it may help you to implement the diskcache.

Here my StorageReferenceFetcher:

import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.disk.DiskCache
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.request.Options
import com.google.firebase.storage.StorageReference
import kotlinx.coroutines.tasks.await
import okio.buffer
import okio.source
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream

class StorageReferenceFetcher(private val storage: StorageReference,
                       private val options: Options,
                       private val diskCache: DiskCache?)
  : Fetcher
{

  class Factory
    : Fetcher.Factory<StorageReference>
  {

    override fun create(storage: StorageReference, options: Options, imageLoader: ImageLoader): Fetcher =
        StorageReferenceFetcher(storage, options, imageLoader.diskCache)

  }

  override suspend fun fetch(): FetchResult
  {

    val snapshot = readFromDiskCache()

    try
    {
      if (snapshot != null)
      {
        // Return the candidate from the cache if it is eligible.
        return SourceResult(source = ImageSource(snapshot.data, diskCache!!.fileSystem, storage.path, snapshot), mimeType = "application/jpg", dataSource = DataSource.DISK)
      }

      // Slow path: fetch the image from the network.
      val response = storage.stream.await()

      try
      {
        // Write the response to the disk cache then open a new snapshot.
        val byteOutputStream = ByteArrayOutputStream()
        response.stream.use { input ->
          byteOutputStream.use { output ->
            input.copyTo(output)
          }
        }

        val byteInputStream = ByteArrayInputStream(byteOutputStream.toByteArray())
        val byteInputStream2 = ByteArrayInputStream(byteOutputStream.toByteArray())

        writeToDiskCache(byteInputStream2)

        return SourceResult(dataSource = DataSource.NETWORK, source = ImageSource(byteInputStream.source().buffer(), options.context), mimeType = "application/jpg")
      }
      catch (exception: Exception)
      {
        Timber.w(exception)
        throw exception
      }
    }
    catch (exception: Exception)
    {
      Timber.w(exception)
      throw exception
    }
  }

  private fun readFromDiskCache(): DiskCache.Snapshot? =
      diskCache?.get(storage.path)

  private fun writeToDiskCache(source: ByteArrayInputStream)
  {
    // Return `null` if we're unable to write to this entry.
    val editor = diskCache?.edit(storage.path)

    if (editor != null)
    {
      try
      {
        diskCache?.fileSystem?.write(editor?.data) {
          this.writeAll(source.source())
        }

        editor?.commitAndGet()
      }
      catch (exception: Exception)
      {
        Timber.d(exception)

        editor?.abort()
        throw exception
      }
    }
  }

}

And here my ImageLoader singleton implementation:

ImageLoader.Builder(this).apply {
      components {
        add(FirestoreKeyer())
        add(FirestoreFetcher.Factory())
      }
      allowRgb565(true)
      memoryCache {
        MemoryCache.Builder(this@Application).apply {
          allowRgb565(true)
        }.build()
      }
      diskCache {
        DiskCache.Builder().apply {
          directory([email protected]("image_cache"))
          allowRgb565(true)
        }.build()
      }
    }.build()

ludovicroland avatar May 17 '22 20:05 ludovicroland