Image loading has always been a fundamental challenge in mobile and cross-platform development. While Android developers have relied on battle-tested libraries like Glide, Coil, and Fresco for years, the rise of Kotlin Multiplatform has created a gap: how do you load images consistently across Android, iOS, Desktop, and Wasm without duplicating code or depending on platform-specific solutions? This is precisely the problem Landscapist Core solves.
In this article, you’ll explore the architecture behind Landscapist Core, a standalone Kotlin Multiplatform image loading engine, and Landscapist Image, its Compose Multiplatform UI companion. You’ll examine the design decisions that make this library exceptionally lightweight, understand how it achieves cross-platform consistency, and learn practical patterns for integrating it into your applications. Whether you’re building a consumer app or a library/SDK that needs to minimize its footprint, understanding Landscapist’s approach offers valuable insights into modern image loading architecture.
If you want to deeply explore the how and why behind Kotlin, from core language fundamentals and internal mechanisms to API design, check out Practical Kotlin Deep Dive, a new Kotlin book by this author.
Practical Kotlin Deep Dive | Master Kotlin, Coroutines, Flow & Multiplatform
The problem with existing solutions
Before diving into Landscapist Core, it’s worth understanding why a new image loading library is needed when mature solutions already exist.
When Compose Multiplatform emerged as a serious cross-platform UI framework, the image loading story remained fragmented. Developers cobbled together solutions: Glide/Coil on Android, platform-specific loaders elsewhere, or custom implementations that duplicated effort. This has been a long-standing item on my TODO list since the initial launch of the Landscapist library in 2020. Over time, I found myself looking for:
- A unified solution: consistent APIs and behavior across all Compose Multiplatform targets.
- Just lightweight: I’ve been working on a ton of third-party solutions, such as libraries and SDKs, so I wanted to make everything lightweight and focus only on core capabilities such as network fetching, memory and disk caching, decoding and downsampling, and progressive loading.
- Extremely optimized performance for Jetpack Compose.
For SDK developers, every kilobyte matters. When you ship a library that includes image loading capabilities, your users inherit your dependencies. At ~312 KB, Landscapist Core is remarkably small compared to Coil3 (~460 KB, +47% larger), Glide (~689 KB, +121% larger), and Fresco (~1 MB, +228% larger). This isn’t accidental; it’s the result of deliberate architectural choices that focus on essential functionality while keeping the design simple.
Then, is Landscapist the best for every case? No, Coil3 is probably a very safe choice in many cases due to its maturity. Landscapist Core’s main value is:
- Highly optimized for Compose, reducing unnecessary recompositions and improving startup time with Baseline Profiles.
- SDK/library developers who need every KB savings.
- Beyond the core engine, Landscapist offers a rich plugin ecosystem including placeholder animations (shimmer, fade, resonate), image transformations (blur), transition animations (crossfade, circular reveal), palette extraction, and advanced zoomable support with subsampling for high-resolution images.
- Projects already using Landscapist’s plugin ecosystem.
So let’s explore more about the Landscapist.
Landscapist Core: The foundation
Landscapist Core is a complete, standalone image loading engine designed for Kotlin Multiplatform from day one. It provides everything needed to fetch, cache, decode, and deliver images, without any UI dependencies.
Core architecture
The architecture follows a pipeline pattern where images flow through distinct stages: from the network or source, through fetching, decoding, transformation, caching, and finally to the result. Each stage is pluggable, allowing customization without modifying core behavior. The design enables several important properties: stages can be skipped when appropriate (cache hits bypass fetching), failures at any stage propagate cleanly, and the entire pipeline is suspendable for efficient coroutine integration.
On Android, you can create an instance using the builder pattern with a Context:
https://medium.com/media/3e3e2adabef82dd4316c80194165f2a4/href
This approach automatically configures disk caching in the app’s cache directory and enables Android-specific image sources like Uri, Drawable resources, and content providers.
Alternatively, you can use the context-free builder and getInstance(), which works across all platforms, including Android:
https://medium.com/media/ef3f46d738b84d3dd01446d501717226/href
Loading images with Flow
Image loading is inherently asynchronous, and Landscapist embraces Kotlin’s Flow for delivering results:
https://medium.com/media/cef3fad183ce09a5541caf2a24c157cd/href
The Flow-based API provides several advantages over callback-based alternatives. State changes are delivered sequentially and can be collected multiple times. The loading state is explicit rather than implicit. Cancellation happens automatically when the collecting coroutine is cancelled.
Memory and disk caching strategy
Caching is where image loaders spend most of their complexity budget, and Landscapist’s approach balances simplicity with effectiveness.
Two-tier cache architecture
The caching strategy uses two tiers: a fast in-memory LRU cache and a persistent disk cache.
https://medium.com/media/a4c56713ec71642458d142c9b6e7f36a/href
The memory cache uses Least Recently Used eviction, automatically removing the oldest entries when the cache exceeds its size limit. The weakReferencesEnabled option is particularly clever: when entries are evicted from the LRU cache, they’re kept as weak references. If the bitmap hasn’t been garbage collected when re-requested, it’s promoted back to the LRU cache without any I/O.
Different images have different caching requirements. A user’s profile picture should be cached aggressively. A one-time verification image might skip caching entirely. Landscapist provides granular control through cache policies:
https://medium.com/media/6df039bb513e2e76ba7748b35f8fb3de/href
Automatic downsampling
Large images are a common source of OutOfMemoryErrors. Landscapist automatically downsamples images based on the target size specified in the request:
https://medium.com/media/ff43bd8051eafc55b54e83ed65d9134e/href
The decoder uses the target dimensions to calculate an appropriate sample size, loading a ~400×400 bitmap instead of the full 4000×4000 image. This single optimization often makes the difference between a smooth scrolling list and an app that crashes under memory pressure.
Landscapist Image: Compose Multiplatform integration
While Landscapist Core handles the image loading pipeline, Landscapist Image provides the Compose Multiplatform UI integration. It’s built on top of the core module and provides seamless integration with Compose’s composition and layout systems.
Basic usage
The simplest usage requires just an image model and a size:
https://medium.com/media/2c6f78d973c9edcadf5d0424a26775c7/href
Behind this simple API, several things happen automatically: the size constraints from the modifier are measured during composition, the appropriate cache is checked, the image is fetched if needed, decoded at the target resolution, and rendered.
Loading state composables
Real applications need to handle loading and error states gracefully. Landscapist Image provides composable slots for each state:
https://medium.com/media/8cd87f9046cb96b145643f6a43cc6d2c/href
The composable slots receive relevant state information. The success slot provides both the LandscapistImageState.Success with metadata (data source, original dimensions) and a Painter ready for rendering. The failure slot provides the LandscapistImageState.Failure with the exception that caused the failure.
State change callbacks
For analytics, logging, or coordinating UI state, you can observe state changes:
https://medium.com/media/b729c5ea63d452d2831ddbf04d57ff4a/href
The plugin ecosystem
One of Landscapist’s most graceful features is its plugin architecture. Plugins are modular, composable components that extend functionality without modifying core behavior.
Plugins are added through the component parameter using a DSL:
https://medium.com/media/0fbc27c1d95d05ef09324e8d6e618d20/href
The + operator adds plugins to the component. Plugins are applied in order, allowing you to layer effects. The rememberImageComponent ensures the component configuration survives recomposition.
The ecosystem includes several production-ready plugins.
- ShimmerPlugin displays an animated shimmer effect during loading, providing visual feedback that content is loading with customizable colors, duration, and animation parameters.
- CrossfadePlugin smoothly crossfades between placeholder and loaded image, essential for polished user experience.
- CircularRevealPlugin reveals the image with an expanding circular animation from the center.
- BlurTransformationPlugin applies a blur effect to loaded images.
- PalettePlugin extracts dominant colors from the loaded image, useful for creating dynamic UI that responds to image content.
- ZoomablePlugin enables pinch-to-zoom and pan gestures on images, and you can apply sub-sampling for Kotlin Multiplatform.
Plugins can be combined to create sophisticated loading experiences:
https://medium.com/media/9f2e750e7344076f54f9f65e512dac96/href
You can even create your own plugin with the ImageComponent and ImagePlugin, and everything makes it more flexible for your Jetpack Compose project.
Performance characteristics
The landscapist’s performance wasn’t an afterthought; it was designed from the beginning with specific performance targets.
Performance testing against established libraries shows competitive results. LandscapistImage achieves an average load time of 1,245ms with 4,520KB memory usage, while GlideImage takes 1,312ms (+5%) with 5,124KB (+13%), CoilImage takes 1,389ms (+12%) with 4,876KB (+8%), and FrescoImage takes 1,467ms (+18%) with 5,342KB (+18%). These numbers come from standardized benchmark conditions: loading the same set of images from network on identical hardware. Real-world performance varies based on network conditions, cache state, and image characteristics.
The test methodology; the performance numbers averaged across 5 rounds of instrumented tests on Android 16 emulator. Each test loads a fresh 200KB JPEG from network (GitHub CDN, no cache) at 300dp size. All caches cleared between runs. Measurements from setContent to fully decoded bitmap. You can see ComprehensivePerformanceTest.kt for full implementation.
Landscapist’s composable functions are designed to be Restartable and Skippable according to Compose compiler metrics. Being Restartable means the function can be re-entered at the point of a state change without re-executing the entire function body. Being Skippable means if inputs haven’t changed, the function can be skipped entirely during recomposition. These properties significantly reduce recomposition overhead in lists and complex UIs where images are common.
The library also includes Baseline Profiles that pre-compile critical code paths during app installation. This reduces cold-start latency and improves overall responsiveness, particularly noticeable on lower-end devices.
Cross-platform considerations
Writing truly cross-platform code requires understanding where platforms differ and designing abstractions that hide those differences.
Landscapist uses Kotlin’s expect/actual mechanism to provide platform-specific implementations:
https://medium.com/media/003798b993048edada88bb0ce9f4bf63/href
The common code works with the abstraction; platforms provide concrete implementations.
Different platforms support different image sources. Android supports network URLs, Content URIs, file paths, Drawable resources, Bitmaps, and byte arrays. iOS and Desktop support network URLs and file paths. Web supports network URLs only. The ImageRequest.builder().model() accepts Any?, and the platform-specific fetcher resolves the appropriate loading strategy.
Ktor provides the cross-platform HTTP client, with automatic engine selection per platform: OkHttp on Android, Darwin on iOS/macOS, CIO on Desktop, and JS on Web. Ktor engines are bundled automatically based on your target platforms, so you don’t need to add Ktor dependencies manually.
Migration from existing Landscapist libraries
Migrating to Landscapist Core from existing Landscapist libraries is designed to be straightforward. If you’re already using Landscapist’s other components (GlideImage, CoilImage, FrescoImage), migration is a near drop-in replacement:
https://medium.com/media/159ffaf3be17638fb8c39e170574ee08/href
The API is intentionally similar. The same plugins work with both. The primary difference is that LandscapistImage uses the standalone core engine instead of delegating to Glide, Coil, or Fresco.
The concepts from other Compose image loading solutions also map reasonably well:
https://medium.com/media/a2f3146d3d041241ec67c8c17a3f5f0e/href
When to choose Landscapist
Landscapist Core and Landscapist Image are particularly good for specific scenarios. If you’re building an SDK that needs image loading capabilities, Landscapist’s minimal footprint is compelling. At ~312 KB, it adds minimal weight to your users’ APKs. Compare this to bundling Glide (~689 KB) or Fresco (~1 MB), and the difference compounds across your SDK’s install base.
For Compose Multiplatform applications targeting Android, iOS, Desktop, or Wasm, Landscapist provides a unified solution. Write your image loading code once in common source sets, and it works identically across all platforms. If you’re starting a new project and don’t need library-specific features (Glide’s resource management, Coil’s extension ecosystem, Fresco’s streaming architecture), Landscapist offers a simpler mental model with fewer concepts to learn.
Landscapist may not be the best choice if you need Glide’s extensive transformation library or resource lifecycle integration, you’re heavily invested in Coil’s interceptor and extension ecosystem, you require Fresco’s progressive streaming for very large images, or your project is Android-only and you need proven library-specific optimizations. So, it very depends on your choice, even though we can’t say ‘A’ is the best in every single case.
Conclusion
Landscapist Core represents a thoughtful approach to the image loading problem in the Kotlin Multiplatform era. By starting fresh rather than wrapping existing libraries, it achieves a remarkably small footprint (~312 KB) while providing production-ready functionality across Android, iOS, Desktop, and Wasm.
The architecture separates concerns cleanly: Landscapist Core handles the image loading pipeline without UI dependencies, while Landscapist Image provides Compose Multiplatform integration with a graceful plugin ecosystem. This separation enables use cases from headless image pre-loading to sophisticated UI with shimmer effects, crossfades, and color extraction.
Performance is competitive with established solutions, composable functions are optimized for Compose’s recomposition system, and Baseline Profiles ensure smooth cold-start behavior. For SDK developers, the minimal footprint means you can add image loading capabilities without significantly impacting your users’ app sizes.
Whether you’re building a Compose Multiplatform application, developing an SDK that needs to stay lean, or simply want a unified image loading solution across platforms, Landscapist provides a solid foundation. The library continues to evolve with the Kotlin Multiplatform ecosystem, and its architecture positions it well for future platform expansions.
Announcing Landscapist Core: A New Image Loading Library for Android & Compose Multiplatform was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.




This Post Has 0 Comments