I was recently working on showing thumbnails for files, and found that the image loading code was surprisingly complex.

I load thumbnails from my backend service via an SDK, which means I don’t have a simple URI that I can provide to an image loading library to load images. Added to that is the fact that I usually pass around a lightweight file id to represent files within my app, but my backend also requires a revision to generate a thumbnail, even though I always simply want the latest revision.

All of this added together meant that I was using a custom built image loading library that knew how to interact with my SDK, but lacked the features and battle testing of industry standard libraries like Coil.

I knew from past projects that I could extend Coil to use my custom data types and data sources, which would allow me to simplify my codebase and benefit from a modern, open source tool.

Custom data sources

To start with, I can create a custom Fetcher, which enables Coil to fetch thumbnails from my custom data source, my SDK, using my custom data class, Metadata. A simple implementation would use an injected Fetcher.Factory to create Fetcher instances for image requests that use Metadata as a data type.

class MetadataFetcher(
  private val sdk: Sdk,
  private val metadata: Metadata,
  private val options: Options,
) : Fetcher {

  override suspend fun fetch(): FetchResult {
    val response = sdk.files().thumbnail(metadata.id, metadata.revision)
    return SourceFetchResult(
      source = ImageSource(response.body.source(), options.context),
      mimeType = response.mimeType,
      dataSource = DataSource.NETWORK,
    )
  }

  class Factory @Inject constructor(
    private val sdk: Sdk,
  ) : Fetcher.Factory<Metadata> {
    override fun create(data: Metadata, options: Options) =
        MetadataFetcher(sdk, data, options)
  }
}

Since this Fetcher.Factory is constructor injected, I can easily provide it in my module function that creates my shared ImageLoader.

@Module
class ImageLoadingModule {
  @Provides
  fun provideImageLoader(
    @ApplicationContext context: Context,
    metadataFetcherFactory: MetadataFetcher.Factory,
  ): ImageLoader = ImageLoader.Builder(context)
      .components {
        add(metadataFetcherFactory)
      }
      .build()
}

With this in place, I can now pass my Metadata objects directly to Coil, which can use them to load file thumbnails using my SDK.

AsyncImage(
  model = uiState.fileMetadata,
  contentDescription = uiState.filename,
)

Custom data types

With the custom mapper I still have to load a full Metadata object for each file for which I want to show a thumbnail. This is a bit cumbersome because, as I mentioned earlier, I generally pass around a simple FileId data type, because I only ever reference the latest revision of a file.

I could implement another Fetcher with a few more dependencies so that it can fetch that data as needed, but Coil also supports a Mapper for just this case.

Mappers recognize that many different data types can be used to represent the same underlying data for a Fetcher, and allows you to map between them. They can also be used to augment data by accessing other dependencies.

In my case, since my MetadataFetcher requires a full Metadata object but I usually only use a FileId to represent a file, I can create a Mapper that is able to map that FileId into a full Metadata object.

class FileIdMapper @Inject constructor(
  metadataRepo: MetadataRepository,
) : Mapper<FileId, Metadata> {
  override fun map(data: FileId, options: Options): Metadata = 
      metadataRepo.getMetadataSync(data)
}

Again, since the FileIdMapper is constructor injected it’s easy to add it to my shared ImageLoader.

  @Module
  class ImageLoadingModule {
    @Provides
    fun provideImageLoader(
      @ApplicationContext context: Context,
      metadataFetcherFactory: MetadataFetcher.Factory,
+     fileIdMapper: FileIdMapper,
    ): ImageLoader = ImageLoader.Builder(context)
        .components {
          add(metadataFetcherFactory)
+         add(fileIdMapper)
        }
        .build()
  }

Now, in addition to passing in a full Metadata object, I can simply pass Coil my FileId, removing some overhead from my view models.

AsyncImage(
  model = uiState.fileId,
  contentDescription = uiState.filename,
)

Thanks to the extensibility of libraries like Coil, using industry standard open source libraries, even with your own custom data sources and data types, is often possible. This can quickly make day-to-day development easier for you and your team, and can help you focus on the important code in your app.