Extending Coil
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.