Code
Kamel – Kotlin asynchronous media loading and caching library for Compose

Kamel is an asynchronous media loading library for Compose Multiplatform. It provides a simple, customizable and efficient way to load, cache, decode and display images in your application. By default, it uses Ktor client for loading resources.
Kamel Setup
Kamel is published on Maven Central:
repositories {
mavenCentral()
// ...
}
Multi-platform
Add the dependency to the common source-set:
kotlin {
sourceSets {
commonMain {
dependencies {
implementation("media.kamel:kamel-image:0.5.0")
// ...
}
}
}
}
Single-platform
Add the dependency to the dependencies block:
dependencies {
implementation("media.kamel:kamel-image:0.5.0")
// ...
}
Ktor HttpClient Engine
Make sure to add a dependency for Ktor HttpClient
engine for your platforms using this link.
Kamel Usage
Loading an image resource
To load an image asynchronously, you can use lazyPainterResource
composable, it can load images from different data sources:
// String
lazyPainterResource(data = "https://www.example.com/image.jpg")
// Ktor Url
lazyPainterResource(data = Url("https://www.example.com/image.jpg"))
// URI
lazyPainterResource(data = URI("https://www.example.com/image.png"))
// File (JVM, Native)
lazyPainterResource(data = File("/path/to/image.png"))
// File (JS)
lazyPainterResource(data = File(org.w3c.files.File(arrayOf(blob), "/path/to/image.png")))
// URL
lazyPainterResource(data = URL("https://www.example.com/image.jpg"))
// and more...
lazyPainterResource
can be used to load SVG, XML, JPEG, and PNG by default depending on the platform implementation.
lazyPainterResource
returns a Resource<Painter>
object which can be used to display the image using KamelImage
composable.
Platform specific implementations
Since there isn’t any shared resource system between Android and Desktop, some implementations (e.g. fetchers and mappers) are only available for a specific platform:
Desktop only implementations
To load an image file from desktop application resources, you have to add resourcesFetcher
to the KamelConfig
:
val desktopConfig = KamelConfig {
takeFrom(KamelConfig.Default)
// Available only on Desktop.
resourcesFetcher()
}
Assuming there’s an image.png
file in the /resources
directory in the project:
CompositionLocalProvider(LocalKamelConfig provides desktopConfig) {
lazyPainterResoursce("image.png")
}
Android only implementations
To load an image file from android application resources, you have to add resourcesFetcher
and resourcesIdMapper
to the KamelConfig
:
val context: Context = LocalContext.current
val androidConfig = KamelConfig {
takeFrom(KamelConfig.Default)
// Available only on Android.
resourcesFetcher(context)
// Available only on Android.
resourcesIdMapper(context)
}
Assuming there’s an image.png
file in the /res/raw
directory in the project:
CompositionLocalProvider(LocalKamelConfig provides androidConfig) {
lazyPainterResource(R.raw.image)
}
Configuring an image resource
lazyPainterResource
supports configuration using a trailing lambda:
val painterResource: Resource<Painter> = lazyPainterResource("https://www.example.com/image.jpg") {
// CoroutineContext to be used while loading the image.
coroutineContext = Job() + Dispatcher.IO
// Customizes HTTP request
requestBuilder { // this: HttpRequestBuilder
header("Key", "Value")
parameter("Key", "Value")
cacheControl(CacheControl.MAX_AGE)
}
}
Displaying an image resource
KamelImage
is a composable function that takes a Resource<Painter>
object, display it and provide extra functionality:
KamelImage(
resource = painterResource,
contentDescription = "Profile",
)
KamelImage
can also be used to get the exception
using onFailure
, and progress
using onLoading
parameters, to display a snackbar or a progress indicator, depending on the case:
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
Box {
KamelImage(
resource = painterResource,
contentDescription = "Profile",
onLoading = { progress -> CircularProgressIndicator(progress) },
onFailure = { exception ->
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = exception.message.toString(),
actionLabel = "Hide",
duration = SnackbarDuration.Short
)
}
}
)
SnackbarHost(hostState = snackbarHostState, modifier = Modifier.padding(16.dp))
}
You can also provide your own custom implementation using a simple when expression:
when (val resource = lazyPainterResource("https://www.example.com/image.jpg")) {
is Resource.Loading -> {
Text("Loading...")
}
is Resource.Success -> {
val painter: Painter = resource.value
Image(painter, contentDescription = "Profile")
}
is Resource.Failure -> {
log(resource.exception)
val fallbackPainter = painterResource("/path/to/fallbackImage.jpg")
Image(fallbackPainter, contentDescription = "Profile")
}
}
Crossfade animation
You can enable, disable or customize crossfade (fade-in) animation through the animationSpec
parameter. Setting animationSpec
to null
will disable the animation:
KamelImage(
resource = imageResource,
contentDescription = "Profile",
// null by default
animationSpec = tween(),
)
Configuring Kamel
The default implementation is KamelConfig.Default
. If you wish to configure it, you can do it the following way:
val customKamelConfig = KamelConfig {
// Copies the default implementation if needed
takeFrom(KamelConfig.Default)
// Sets the number of images to cache
imageBitmapCacheSize = DefaultCacheSize
// adds an ImageBitmapDecoder
imageBitmapDecoder()
// adds a FileFetcher
fileFetcher()
// Configures Ktor HttpClient
httpFetcher {
defaultRequest {
url("https://www.example.com/")
cacheControl(CacheControl.MaxAge(maxAgeSeconds = 10000))
}
install(HttpRequestRetry) {
maxRetries = 3
retryIf { httpRequest, httpResponse ->
!httpResponse.status.isSuccess()
}
}
// Requires adding "io.ktor:ktor-client-logging:$ktor_version"
Logging {
level = LogLevel.INFO
logger = Logger.SIMPLE
}
}
// more functionality available.
}
Cache size (number of entries to cache)
Kamel provides a generic Cache<K,V>
interface, the default implementation uses LRU memory cache mechanism backed by LinkedHashMap
. You can provide a number of entries to cache for each type like so:
KamelConfig {
// 100 by default
imageBitmapCacheSize = 500
// 100 by default
imageVectorCacheSize = 300
// 100 by default
svgCacheSize = 200
}
Applying Kamel configuration
You can use LocalKamelConfig
to apply your custom configuration:
CompositionLocalProvider(LocalKamelConfig provides customKamelConfig) {
lazyPainterResource("image.jpg")
}
Contributions
Contributions are always welcome!. If you’d like to contribute, please feel free to create a PR or open an issue.
