placeholder draws while the request runs, error when it fails, and fallback when the model is null. All three are painters scaled by the image's contentScale, which is why the 24 dp book icon below is blown up and cropped. App-wide settings belong in one ImageLoader, which the Application class provides:
class BookNestApp : Application(), SingletonImageLoader.Factory {
override fun newImageLoader(context: PlatformContext): ImageLoader =
ImageLoader.Builder(context)
.crossfade(true) // 200 ms fade-in
.memoryCache { MemoryCache.Builder().maxSizePercent(context, 0.20).build() }
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache").toOkioPath())
.maxSizeBytes(50L * 1024 * 1024) // 50 MB
.build()
}
.build()
}To photograph the placeholders, the test server (a few-line SimpleHTTPRequestHandler subclass) waits three seconds per response. The demo asks for three covers through Coil 11,909 , three through Glide 35,023 , and a missing 99.png:

The listener shows the cache at work. The first launch after pm clear downloaded everything:
D/BookNestImg: coil 99: coil3.network.HttpException: HTTP 404 D/BookNestImg: coil 2: NETWORK D/BookNestImg: coil 1: NETWORK D/BookNestImg: coil 3: NETWORK
Rotating the phone recreates the activity in the same process, and the three lines then ended in MEMORY_CACHE; after am force-stop and a relaunch, a new process, they ended in DISK. The memory cache holds decoded bitmaps and dies with the process; the disk cache holds the downloaded bytes (132 KB here) and survives it. The 404 is not cached and came back on every launch.