🌻 Landscapist is a highly optimized, pluggable Jetpack Compose and Kotlin Multiplatform image loading solution that fetches and displays network images, and compatibles with Glide , Coil , and Fresco. This library supports tracing image loading states, composing custom implementations, and some valuable animations, such as crossfades, blur transformation, and circular reveals. You can also configure and attach…
🌻 Landscapist is a highly optimized, pluggable Jetpack Compose and Kotlin Multiplatform image loading solution that fetches and displays network images, and compatibles with Glide, Coil, and Fresco. This library supports tracing image loading states, composing custom implementations, and some valuable animations, such as crossfades, blur transformation, and circular reveals. You can also configure and attach image-loading behaviors easily and fast with image plugins.
Open the playground to use Landscapist without
installing anything. It is landscapist-image compiled to WebAssembly, running in the page.
Every image size, content scale and plugin combination is a switch. The panel under the image is
what the loader reported back, and the panel below that is the Kotlin for whatever you land on,
with a button to copy it into your project.
For comprehensive information about Landscapist, please refer to the official documentation.
Technical Content
For more details of the history, performance, customizability, and how to load network images and implement placeholders, animations, and transformations with Landscapist, check out Optimized Image Loading for Compose and Kotlin Multiplatform.
💝 Sponsors
Demo projects
You can see the use cases of this library in the repositories below:
google/modernstorage: ModernStorage is a group of libraries that provide an abstraction layer over storage on Android to simplify its interactions.
GetStream/WhatsApp-Clone-Compose: 📱 WhatsApp clone project demonstrates modern Android development built with Jetpack Compose and Stream Chat SDK for Compose.
android/storage-samples: Multiple samples showing the best practices in storage APIs on Android.
skydoves/DisneyCompose: 🧸 A demo Disney app using Jetpack Compose and Hilt based on modern Android tech-stacks and MVVM architecture.
skydoves/MovieCompose: 🎞 A demo movie app using Jetpack Compose and Hilt based on modern Android tech stacks.
Landscapist Image
The landscapist-image module provides a Compose Multiplatform UI component built on top of landscapist-core. It integrates seamlessly with the Landscapist plugin ecosystem and works across all Compose Multiplatform targets (Android, iOS, Desktop, Web).
Setup
Add the dependency below to your module’s build.gradle file:
var currentState by remember { mutableStateOf<LandscapistImageState>(LandscapistImageState.None) }
LandscapistImage(
imageModel = { imageUrl },
state ->
currentState = state
}
)
when (currentState) {
is LandscapistImageState.Loading -> { /* loading */ }
is LandscapistImageState.Success -> { /* success */ }
is LandscapistImageState.Failure -> { /* failure */ }
else -> { /* none */ }
}
Drawing the image yourself
A LandscapistImage with no slot and no plugin is a single layout node, which draws the image
itself. Add a loading, success or failure slot or an ImagePlugin and it becomes a container,
and it composes a child inside that container when something actually has to go there. A
CrossfadePlugin does not, since the fade happens inside the painter, and neither does a
PainterPlugin such as BlurTransformationPlugin, since the container draws through it.
When all you want is the image, rememberImagePainter gives you the painter on its own
and you keep the node:
It reads the memory cache while it composes, so an already loaded image is drawn in the first frame,
and it takes the size to decode at from the first draw. There are no loading or failure slots here,
no ImagePlugin and no crossfade, since each of those needs something composed around the image.
Supported Image Sources
LandscapistImage supports various image sources including network URLs, local files, drawable resources, and more. See the Landscapist Image documentation for a complete list of supported image sources per platform.
Landscapist Core & Image
Landscapist now provides two foundational modules designed for Kotlin Multiplatform and Compose Multiplatform from the scratch, giving you full control over image loading across all platforms:
landscapist-core: A standalone, Kotlin Multiplatform image loading engine with built-in memory/disk caching, progressive loading, and network fetching via Ktor. Works on Android, iOS, Desktop, and Web without any UI dependencies.
landscapist-image: A Compose Multiplatform image component built on top of landscapist-core with full plugin support. Seamlessly works across all Compose Multiplatform targets.
landscapist-svg: SVG decoding for landscapist-core, rasterized with AndroidSVG on Android and Skia everywhere else. See the Landscapist SVG documentation.
These modules are perfect if you want a lightweight, customizable image loader without depending on Glide, Coil, or Fresco, with first-class support for all Kotlin Multiplatform and Compose Multiplatform targets.
Landscapist Core
The landscapist-core module is a complete, Kotlin Multiplatform image loading solution that works standalone without any UI dependencies. It provides:
Network image loading via Ktor HTTP client.
Memory caching with LRU eviction and weak references.
Disk caching with size limits and automatic cleanup.
Progressive loading for better perceived performance.
Priority-based scheduling for optimized resource usage.
Kotlin Multiplatform support (Android, iOS, Desktop, Web).
Image transformations and custom decoders.
Event listeners for monitoring load states.
Why Choose Landscapist Core?
Landscapist Core is exceptionally lightweight compared to other image loading libraries, making it the ideal choice for SDK and library developers who need to minimize their dependency footprint.
Core engine AAR size (release build), for the versions this repo depends on:
Library
Module
Release AAR
vs landscapist-core
landscapist-core
landscapist-core
371 KiB
baseline
Coil3
coil-core 3.6.2
469 KiB
+26%
Glide
glide 5.0.9
701 KiB
+89%
Fresco
core pipeline artifacts
1.11 MiB
3.06x
Reproduce with ./gradlew :landscapist-core:assembleRelease && ls -l landscapist-core/build/outputs/aar/landscapist-core-release.aar (379,990 bytes = 371 KiB). This is a single module’s AAR, not the full transitive footprint.
Load-time and memory numbers are not published here because they depend on device, OS, and network. Run the included benchmarks (ImageLibraryBenchmark instrumentation test and the :benchmark-landscapist macrobenchmark) on your own hardware. See the performance comparison for methodology.
Setup
Add the dependency below to your module’s build.gradle file:
All platform-specific Ktor engines are included automatically based on your target platforms.
Using Landscapist Core for Network Loading
You can use landscapist-core as a standalone image loader for fetching and caching network images without any UI dependencies. This is useful for pre-loading images, implementing custom image components, or using images in non-Compose contexts.
Android Example
This example demonstrates creating a Landscapist instance with custom cache sizes and loading an image from a URL. The result is delivered as a Flow, allowing you to handle loading, success, and failure states reactively.
import com.skydoves.landscapist.core.Landscapist
import com.skydoves.landscapist.core.LandscapistConfig
import com.skydoves.landscapist.core.ImageRequest
import kotlinx.coroutines.flow.collect
// Create a Landscapist instance (typically once in your app)
val landscapist = Landscapist.builder(context)
.config(
LandscapistConfig(
memoryCacheSize = 64 * 1024 * 1024L, // 64MB
diskCacheSize = 100 * 1024 * 1024L, // 100MB
)
)
.build()
// Load an image
lifecycleScope.launch {
val request = ImageRequest.builder()
.model("https://example.com/image.jpg")
.size(width = 800, height = 600)
.build()
landscapist.load(request).collect { result ->
when (result) {
is ImageResult.Loading -> {
// Show loading state
}
is ImageResult.Success -> {
val imageBitmap = result.data
// Use the loaded ImageBitmap
}
is ImageResult.Failure -> {
// Handle error
}
}
}
}
Kotlin Multiplatform Example
For non-Android platforms (iOS, Desktop, Web), use the singleton instance which comes pre-configured with sensible defaults. This example shows a simple suspend function that loads an image and returns the ImageBitmap, suitable for use in shared Kotlin Multiplatform code.
import com.skydoves.landscapist.core.Landscapist
import com.skydoves.landscapist.core.ImageRequest
// Get the default instance (works on all platforms)
val landscapist = Landscapist.getInstance()
suspend fun loadImage(url: String): ImageBitmap? {
val request = ImageRequest.builder()
.model(url)
.build()
var bitmap: ImageBitmap? = null
landscapist.load(request).collect { result ->
if (result is ImageResult.Success) {
bitmap = result.data
}
}
return bitmap
}
Advanced Configuration
Customize the Landscapist instance with advanced options including network timeouts, memory optimizations, and event listeners. This example shows how to configure various aspects of the image loader to match your app’s specific requirements.
Leaving the disk cache unset falls through to the default one on disk. Use noDiskCache() for a
loader that writes nothing to disk, and reads nothing back from it:
val landscapist = Landscapist.builder(context)
.noDiskCache()
.build()
The disk cache is keyed by the URL, so one download answers every size an image is drawn at. Any
headers the request carries scope both that key and the memory key, so two requests for one URL with
different headers do not share an entry.
Glide
Add the codes below to your rootbuild.gradle file (not your module-level build.gradle file):
allprojects {
repositories {
mavenCentral()
}
}
Next, add the dependency below to your module’s build.gradle file:
Note: Landscapist-Glide includes version 5.0.9 of Glide internally. So please make sure your project is using the same Glide version or exclude the Glide dependency to adapt yours. Also, please make sure the Jetpack Compose version on the release page.
GlideImage
You can load images simply by using GlideImage composable function as the following example below:
GlideImage(
imageModel = { imageUrl }, // loading a network image using an URL.
imageOptions = ImageOptions(
contentScale = ContentScale.Crop,
alignment = Alignment.Center
)
)
More Details for GlideImage
👉 Read further for more details
Custom RequestOptions and TransitionOptions
You can customize your request-options with your own RequestOptions and TransitionOptions for applying caching strategies, loading transformations like below:
You can request image with your own RequestBuilder, which is the backbone of the request in Glide and is responsible for bringing your options together with your requested url or model to start a new load.
If you’re targeting on Kotlin Multiplatform, add the dependency below to your module’s build.gradle.kts file:
sourceSets {
val commonMain by getting {
dependencies {
implementation("com.github.skydoves:landscapist-coil3:$version")
}
}
}
The coil3-landscapist package functions identically to the coil-landscapist package, with the key distinction being its focus on Kotlin Multiplatform. This enables the use of Coil3 across various platforms, including Android, iOS, and Desktop (JVM), facilitating a unified image loading experience across different environments.
Note: Please make sure your project uses the same Jetpack Compose version on the release page.
CoilImage
You can load images by using the CoilImage composable function as the following example below:
CoilImage(
imageModel = { imageUrl }, // loading a network image or local resource using an URL.
imageOptions = ImageOptions(
contentScale = ContentScale.Crop,
alignment = Alignment.Center
)
)
More Details for CoilImage
👉 Read further for more details
Custom ImageRequest and ImageLoader
You can load images with your own ImageRequest and ImageLoader, which provides all the necessary information for loading images like caching strategies and transformations.
You can pass the same instance of your ImageLoader down through the Composition in your composable hierarchy as following the example below:
val imageLoader = ImageLoader.Builder(context).build()
CompositionLocalProvider(LocalCoilImageLoader provides imageLoader) {
// This will automatically use the value of current imageLoader in the hierarchy.
CoilImage(
imageModel = ...
)
}
Animated Image Supports (GIF, Webp)
You can load animated GIFs and WebP Images with your ImageLoader.
Note: Landscapist-Fresco includes version 3.1.0 of Fresco. So please make sure your project is using the same Fresco version or exclude the Fresco dependency to adapt yours. Also, please make sure the Jetpack Compose version on the release page.
Setup
To get started, you should set up Fresco with ImagePipelineConfig in your Application class. Generally, it’s recommended initializing with OkHttpImagePipelineConfigFactory. Also, you can customize caching, networking, and thread pool strategies with your own ImagePipelineConfig. For more details, you can check out Using Other Network Layers.
class App : Application() {
override fun onCreate() {
super.onCreate()
val pipelineConfig =
OkHttpImagePipelineConfigFactory
.newBuilder(this, OkHttpClient.Builder().build())
.setDiskCacheEnabled(true)
.setDownsampleEnabled(true)
.setResizeAndRotateEnabledForNetwork(true)
.build()
Fresco.initialize(this, pipelineConfig)
}
}
FrescoImage
You can load images by using the FrescoImage composable function as the following example below:
FrescoImage(
imageUrl = stringImageUrl, // loading a network image using an URL.
imageOptions = ImageOptions(
contentScale = ContentScale.Crop,
alignment = Alignment.Center
)
)
More Details for FrescoImage
👉 Read further for more details
Custom ImageRequest
You can load images with your own ImageRequest, which provides some necessary information for loading images like decoding strategies and resizing.
You can pass the same instance of your imageRequestBuilder down through the Composition in your composable hierarchy as following the example below:
// customize the ImageRequest as needed
val imageRequestBuilder = ImageRequestBuilder
.newBuilderWithSource(uri)
.setImageDecodeOptions(decodeOptions)
.setLocalThumbnailPreviewsEnabled(true)
.setLowestPermittedRequestLevel(RequestLevel.FULL_FETCH)
.setProgressiveRenderingEnabled(false)
.setResizeOptions(ResizeOptions(width, height))
CompositionLocalProvider(LocalFrescoImageRequest provides imageRequestBuilder) {
// This will automatically use the value of current ImageRequest in the hierarchy.
FrescoImage(
imageUrl = ...
)
}
Fresco Animated Image Support (GIF, Webp)
Add the below dependency to your module’s build.gradle file.
You can load animated GIFs and WebP Images with FrescoWebImage composable function. You should pass the AbstractDraweeController like the following example below:
For more details, check out DraweeController, and Supported URIs for setting URI addresses. Also, you can load general images (jpeg, png, etc) which can be loaded with FrescoImage by using FrescoWebImage and your custom controller.
ImageOptions
You can give image options to your image composable functions by passing ImageOptions instance like the below:
Note: You can also use the custom Composables for CoilImage and FrescoImage.
Preview on Android Studio
Landscapist supports preview mode for each image library; Glide, Coil, and Fresco. You can show the preview image on your editor with a previewPlaceholder parameter as following:
Note: You can also use the the previewPlaceholder parameter for CoilImage and FrescoImage.
ImageComponent and ImagePlugin
You can compose supported image plugins by Landscapist or you can create your own image plugin that will be composed following the image loading state.
ImagePlugin is a pluggable compose interface that will be executed for loading images. ImagePlugin provides following types below:
PainterPlugin: A pinter plugin interface to be composed with the given Painter.
LoadingStatePlugin: A pluggable state plugin that will be composed while the state is ImageLoadState.Loading.
SuccessStatePlugin: A pluggable state plugin that will be composed when the state is ImageLoadState.Success.
FailureStatePlugin: A pluggable state plugin that will be composed when the state is ImageLoadState.Failure.
ComposablePlugin: A plugin that wraps the image in composable content of its own, such as ZoomablePlugin.
For example, you can implement your own LoadingStatePlugin that will be composed while loading an image like the below:
Note the data class. A plugin set is compared to decide whether the component an image was handed
has changed, and images skip when it has not, so a plugin the runtime cannot compare is a new value
on every composition and costs every image carrying it its skipping. Make a custom plugin a data
class, or give it equals and hashCode over whatever configures it.
Next, you can compose plugins by adding them in the rememberImageComponent like the below:
The landscapist-placeholder package provides useful image plugins, such as loading & failure placeholder supports and shimmering animation.
To use placeholder supports, add the dependency below:
Note: The source should be one of ImageBitmap, ImageVector, or Painter.
ThumbnailPlugin
Landscapist supports the thumbnail feature, which pre-loads and displays small sizes of images while loading the original image. So you can make users feel images loading faster and give images a nature loading effect while displaying an original image.
To show thumbnail, add the image plugin into your image component like the example below:
Note: It’s highly recommended to use a small size of the request size on the thumbnail plugin to load the pre-load images process faster.
Animation
The landscapist-animation package provides useful image plugins related to animations, such as crossfade and circular reveal animation.
To use animation supports, add the dependency below:
Note: You can also use the Circular Reveal animation for CoilImage and FrescoImage.
Transformation
The landscapist-transformation package provides useful image transformation plugins, such as the blur effect.
To use transformation supports, add the dependency below:
You can implement the blur effect with BlurTransformationPlugin as the following:
GlideImage( // CoilImage, FrescoImage also can be used.
imageModel = { poster.image },
component = rememberImageComponent {
+BlurTransformationPlugin(radius = 10) // between 0 to Int.MAX_VALUE.
}
)
Note: Landscapist’s blur transformation falls back onto a CPU-based implementation to support older API levels. So you don’t need to worry about API compatibilities and performance issues.
Palette
The landscapist-palette package provides useful image plugins related to palette, such as extracting primary color sets.
To use palette supports, add the dependency below:
You can extract primary (theme) color profiles with PalettePlugin. You can check out Extract color profiles to see what kinds of colors can be extracted.
var palette by rememberPaletteState(null)
GlideImage( // CoilImage, FrescoImage also can be used.
imageModel = { poster.image },
component = rememberImageComponent {
+PalettePlugin { palette = it }
}
)
Crossfade(
targetState = palette,
modifier = Modifier
.padding(horizontal = 8.dp)
.size(45.dp)
) {
Box(
modifier = Modifier
.background(color = Color(it?.lightVibrantSwatch?.rgb ?: 0))
.fillMaxSize()
)
}
Also, you can customize attributes of PalettePlugin like the example below:
var palette by remember { mutableStateOf<Palette?>(null) }
GlideImage( // CoilImage, FrescoImage also can be used.
imageModel = { poster.image },
component = rememberImageComponent {
+PalettePlugin(
imageModel = poster.image,
useCache = true, // use cache strategies for the same image model.
interceptor = {
it.addFilter { rgb, hsl ->
// here edit to add the filter colors.
false
}
},
paletteLoadedListener = {
palette = it
}
)
}
)
Note: You can also use the Palette for CoilImage and FrescoImage.
Zoomable
The landscapist-zoomable package provides a ZoomablePlugin that enables zoom and pan gestures for images. This plugin supports both Android and Kotlin Multiplatform (iOS, Desktop).
To use zoomable supports, add the dependency below:
Note: Sub-sampling is supported on Android (Glide, Coil3) and Kotlin Multiplatform (Coil3, targeting Android, iOS/macOS, and Destkop).
Image Gallery
The landscapist-image-gallery package provides high level, Compose Multiplatform UI components for building photo gallery experiences on top of Landscapist:
ImageGallery: a thumbnail grid built on LazyVerticalGrid with optional multi select, header and footer slots, and a custom content slot.
ImageViewer: a full screen pager with pinch to zoom, swipe to dismiss, and top bar, bottom bar, and page indicator overlays.
ImageSharedTransitionConfig: drop in shared element transition between gallery thumbnails and viewer pages.
By default, both components render their images using LandscapistImage, so they work on Android, iOS, Desktop, and Web without extra setup. You can still swap in Glide, Coil, Fresco, or any custom loader through the content slot.
To use the gallery components, add the dependency below:
ImageGallery displays a list of image models in a LazyVerticalGrid. The items can be anything accepted by Landscapist (URL strings, Uri, File, ByteArray, drawable resources, and so on).
Because each tile is rendered by LandscapistImage under the hood, the full plugin ecosystem (Shimmer, Crossfade, Palette, and so on) is available through component. Use the content lambda to render tiles with another loader (Glide, Coil, Fresco).
Selection mode
Enable multi select by flipping selectable and tracking the selected indices yourself. Long pressing any tile enters selection mode, tapping tiles while in selection mode toggles them, and onImageClick is not fired while a selection is active.
ImageViewer is a full screen pager for viewing images one at a time. It includes horizontal paging, pinch and double tap zoom via ZoomablePlugin, swipe to dismiss, and slots for a top bar, bottom bar, and page indicator.
Swipe to dismiss is automatically disabled while the current page is zoomed, so zoom and pan gestures are not interrupted. Use the content slot to render pages with another loader while keeping zoom, paging, and dismiss behavior intact.
Shared element transition
Pass the sameImageSharedTransitionConfig to both ImageGallery and ImageViewer to animate a tapped thumbnail into the full screen viewer and back on dismiss.
The same pattern works with NavHost. Use the AnimatedContentScope from each composable { ... } destination as the animatedContentScope. Override keyProvider when gallery indices don’t match viewer pages, when the same model appears multiple times, or when you need to namespace keys across multiple galleries.
For the complete API reference (state holders, defaults, Kotlin Multiplatform targets, all overlay slots), see the Image Gallery documentation.
BOM
The landscapist Bill of Materials (BOM) lets you manage all of your landscapist library versions by specifying only the BOM’s version.
The BOM settles versions; it does not save you from writing each coordinate out.
landscapist-version-catalog publishes every Landscapist artifact as a Gradle version catalog, so
they arrive as aliases with completion in the IDE.
The two are not alternatives. The catalog carries coordinates for the lines you write; the BOM
constrains versions across the whole resolution, including a module that arrives through another
library. See the documentation for the full list.
Taking Snapshot Images With Paparazzi
Paparazzi allows you to take snapshot images of your Composable functions without running them on physical devices. You can take proper snapshots images about your images with Paparazzi like the below:
Accompanist is a group of libraries that contains some utilities which I’ve found myself copying around projects which use Jetpack Compose. Currently, it contains image loading and insets. You can get more variety and recent systems from the library maintained by Google.
Find this repository useful? :heart:
Support it by joining stargazers for this repository. :star:
Also follow me for my next creations! 🤩
License
Designed and developed by 2020 skydoves (Jaewoong Eum)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
评论