# Kamel: asynchronous image loading that works across Compose targets

> Kamel is a Kotlin library that loads, decodes, caches and displays images in Compose Multiplatform, using Ktor for fetching and shipping a decoder module per media type. It suits multiplatform teams that want one image pipeline instead of a per platform one, and the granular dependency setup keeps the transitive graph small.

**Kamel-Media/Kamel** — Kotlin asynchronous media loading and caching library for Compose. 

- Repository: https://github.com/Kamel-Media/Kamel
- Stars: 795 · Forks: 29
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/kamel-media-kamel

## One image pipeline for every Compose target

Kamel is an asynchronous media loading library for Compose Multiplatform, and it provides a simple, customizable and efficient way to load, cache, decode and display images in an application. By default it uses the Ktor client for loading resources.

The problem it addresses shows up the moment a project has more than one target. An Android application can use the platform image loaders, a desktop application has a different filesystem and URL story, and a web or Wasm build has a third. A team that shares business logic across those targets ends up with a per platform image pipeline, or with a common abstraction that leaks platform details.

Kamel's answer is to make the shared layer do the work. The common code declares what it wants to display, and the fetcher, the decoder and the mapper are supplied per platform. That is the same expectation the library has for the rendering side, since Compose Multiplatform is what it is built for.

The current toolchain targets are visible in the badges: Kotlin 2.3.0 and Compose Multiplatform 1.10.0, with the library published to Maven Central under the media.kamel group and snapshots on a Sonatype repository. The topic list names the platforms it covers, including Android, iOS, desktop, web, wasmjs, kotlin-js and kotlin-native.

## The default dependency and the granular module set

There are two ways to install Kamel, and the difference is about how much of the graph you take.

The default setup adds one artifact to the common source set:

```kotlin
                implementation("media.kamel:kamel-image-default:1.0.9")
```

The comment in the README notes that there is no need to specify Ktor engines here, because one is included for each target. That is the convenience trade: one line, and every target has a working engine.

The granular setup exists to limit transitive dependencies, and it is a real list of choices rather than a single flag. The core module kamel-image is required, and the README warns that when you use kamel-image a Ktor engine is not included, so you must add your own for each target if you want to fetch remote images. Optional decoder modules then cover the media types: kamel-decoder-image-bitmap, kamel-decoder-image-bitmap-resizing, which is Android only right now, kamel-decoder-image-vector, kamel-decoder-svg-batik, kamel-decoder-svg-std, and kamel-decoder-animated-image for GIF support. Platform specific resource fetchers live in the jvm and android source sets as kamel-fetcher-resources-jvm and kamel-fetcher-resources-android.

The two SVG decoders are the interesting pair, because having both batik and the standard library variant in the list is a sign that the SVG story is not settled in one implementation.

## asyncPainterResource and the shapes of data it accepts

The entry point is the asyncPainterResource composable, which loads images from different data sources, and it accepts a wide range of input types:

```kotlin
asyncPainterResource(data = "https://www.example.com/image.jpg")
```

The documented list covers a plain String, a file URL, a Ktor Url, a URI, a java.io.File for the JVM and Native targets, the org.w3c.files.File variant for JS, and a java.net.URL, and then and more.

The function returns a Resource<Painter> object, which is what you display with the KamelImage composable. That split is the standard Compose resource pattern, where loading state and failure are part of the value rather than something you handle with callbacks, and it is why the composable is named for the resource it returns.

What you can decode by default depends on the platform implementation, and the README names SVG, XML, JPEG and PNG in that context. The animated image decoder module is the one to add for GIFs, and the resizing decoder is the one that is currently Android only, which is the kind of asymmetry that matters when you are sizing images differently per platform.

## Resource fetchers differ between Desktop and Android

There is no shared resource system between Android and Desktop, and that single fact explains most of the platform specific code in the library. Some implementations, fetchers and mappers among them, are only available for one platform.

On the desktop you add the resources fetcher and, if you want it, the Batik SVG decoder to a KamelConfig:

```kotlin
val desktopConfig = KamelConfig {
    takeFrom(KamelConfig.Default)
    // Available only on Desktop.
    resourcesFetcher()
    // Available only on Desktop.
    // An alternative svg decoder
    batikSvgDecoder()
}
```

That configuration is then provided through a CompositionLocal, so that a composable can load an image from the project resources by its plain name:

```kotlin
CompositionLocalProvider(LocalKamelConfig provides desktopConfig) {
    asyncPainterResource("image.png")
}
```

On Android the equivalent configuration takes the Context, and adds the resourcesFetcher and the resourcesIdMapper, since Android resources are referenced by identifier rather than by path. The two configurations are near mirrors of each other, and a multiplatform application usually keeps both, one per source set, with the shared code depending on nothing but the composable.

## Cache sizing and the crossfade animation

The documentation table of contents lists the configuration surface, and two entries are the ones teams actually tune.

The first is memory cache size, expressed as the number of entries to cache, and the second is disk cache size, expressed in bytes. That asymmetry is worth noting: the memory cache is counted in items, which is the right unit for decoded images of roughly similar size, while the disk cache is counted in bytes, which is the right unit for compressed data of varying size. The config is applied through a KamelConfig, and the Default configuration is the starting point that takeFrom copies and then modifies.

The third entry is the crossfade animation, which sits under displaying an image resource rather than under configuration. A crossfade is the standard way to avoid the flash of empty space while a remote image loads, and having it in the library means the composable can treat loading as a state of the resource rather than something each call site animates by hand.

Taken together, these are the settings that determine the behaviour your users see: how much decoded image data stays in memory, how much compressed data is kept on disk, and what happens in the moment between requesting an image and having it.

## The module layout and the release line

The repository is a Gradle multi-project, and the directory names map directly onto the dependency names you saw in the setup instructions. There is kamel-core/, kamel-image/ for the core image module, kamel-image-default/ for the batteries included default, kamel-decoder/ for the decoders, kamel-fetcher/ for the fetchers, and kamel-mapper/ for the mappers. On top of that sit kamel-samples/, kamel-sample-ios/, a kotlin-js-store/ directory for the JavaScript dependency lock, build.gradle.kts, settings.gradle.kts, gradle.properties and a .fleet/ directory.

The release history is short and the versions are low. The recent tags are v1.0.8 dated 2025-09-19, v1.0.9 dated 2026-01-22, and v1.0.10_avif, a snapshot published on 2026-01-27 that carries the AVIF work in its name. A snapshot published under a version tag rather than a release is worth a second look before you depend on it, because the name is the only signal that the artifact is not a stable one.

The last push to the repository was on 2026-10-15, so work is continuing, and the Apache-2.0 licence means the library can be used inside commercial applications under its terms. The licence badge in the README points at the Apache 2.0 text, and the LICENSE file in the tree is the version to read for your own review.

## Where Kamel is the wrong tool

Three cases point elsewhere.

The first is a single platform target. If you are building an Android only application, the platform image loaders and the lifecycle aware image components already handle caching, cancellation and request coalescing, and a multiplatform library adds an abstraction and a dependency for a problem you do not have.

The second is a project that needs a format the decoders do not cover, and the module list makes this concrete rather than theoretical. A video frame loader, a HEIF decoder or a font renderer is not in the list, and adding a module is the supported route, so the question is whether someone has written one for your format. The avif snapshot in the release list suggests that coverage is being extended, which is encouraging and also a sign that the format list moves.

The third is an application with a hard requirement on memory. The memory cache is configured as a number of entries, and decoded images at Compose resolution are large, so a cache tuned for thumbnails will behave very differently from one tuned for full screen bitmaps. Getting that wrong is a memory problem, not a bug in the library.

One operational note as well. The default setup includes a Ktor engine per target and the granular setup does not, and that difference is easy to miss when moving a project from one to the other, because the failure appears at runtime as images that never load rather than as a build error.

## Conclusion

Adopt Kamel if you ship a Compose Multiplatform application and want one asynchronous image pipeline shared by the Android, desktop, iOS, web and Wasm targets, with caching and crossfade animation handled for you. Do not adopt it if you have a single platform target and a working image loader already, since the library earns its keep on the seams between platforms. Verify three things first: which media formats you actually need, because the decoders are separate modules and animated image support is only as good as the module you include; whether you need a Ktor engine, because the granular setup deliberately does not include one and you must add it per target; and which platform specific fetchers your resources require, since there is no shared resource system between Android and Desktop. The licence is Apache-2.0, the last push was on 2026-10-15, and the newest published release is v1.0.9 dated 2026-01-22.

## FAQ

### What is Kamel and what does it do?

Kamel is an asynchronous media loading library for Compose Multiplatform that provides a way to load, cache, decode and display images, using the Ktor client for loading resources by default. It covers Android, iOS, desktop, web and Wasm targets.

### How do I add Kamel to a Kotlin Multiplatform project?

Kamel is published on Maven Central. The default setup adds implementation("media.kamel:kamel-image-default:1.0.9") to the common source set, and a Ktor engine is included for each target. The granular setup instead uses kamel-image plus the decoder, fetcher and mapper modules you actually need.

### What data types can asyncPainterResource load?

The documented list covers a String, a file URL, a Ktor Url, a URI, a File on the JVM and Native targets, the org.w3c.files.File variant on JS, and a URL. The function returns a Resource<Painter> that you display with the KamelImage composable.

### How do I load an image from application resources?

Resources are platform specific. On desktop you add resourcesFetcher and optionally batikSvgDecoder to a KamelConfig and provide it through CompositionLocalProvider with LocalKamelConfig. On Android you add resourcesFetcher with the context and the resourcesIdMapper, since there is no shared resource system between the two platforms.

### Which media formats does Kamel decode?

By default the platform implementations handle SVG, XML, JPEG and PNG. Other formats come from separate decoder modules, such as kamel-decoder-animated-image for GIF support and kamel-decoder-image-bitmap-resizing, which is Android only right now. Using kamel-image directly means no Ktor engine is included and you must add one per target.

## Sources

- [Issues](https://github.com/Kamel-Media/Kamel/issues)
- [Kamel-Media/Kamel on GitHub](https://github.com/Kamel-Media/Kamel)
- [License: Apache-2.0](https://github.com/Kamel-Media/Kamel/blob/main/LICENSE)
- [README](https://github.com/Kamel-Media/Kamel/blob/main/README.md)
- [Releases](https://github.com/Kamel-Media/Kamel/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/kamel-media-kamel
