Landscapist Documentation

repository·main·Indexed 25 days ago

https://github.com/skydoves/landscapist

A highly optimized, pluggable image loading solution for Jetpack Compose and Kotlin Multiplatform. It features a standalone KMP engine (landscapist-core) for network fetching and caching via Ktor, and a Compose Multiplatform UI layer (landscapist-image). Landscapist also provides integration wrappers for popular image loading engines including Glide, Coil (including Coil3 for KMP), and Fresco.

Tokens
45.3K
Snippets
134
Records
154
Agent score
82%

What's inside Landscapist

  1. Overview of Landscapist

    main

    Landscapist is a highly optimized, pluggable image loading solution designed specifically for Jetpack Compose. It provides seamless network image fetching and display capabilities by leveraging established image loading engines: Glide, Coil, or Fresco.

    Key features include:

    • Pluggable Architecture: Easily switch between or use different image loading engines.
    • State Tracing: Support for monitoring image loading states to build custom UI implementations.
    • Animations & Transformations: Built-in support for crossfades, blur transformations, and circular reveals.
    • Customization: Use ImagePlugin to attach custom behaviors and ImageOptions for fine-grained control.
    • Performance: Optimized for Jetpack Compose with Restartable and Skippable composables, and support for Baseline Profiles to improve startup and runtime performance.
  2. Key benefits of Landscapist

    main

    Landscapist provides several architectural advantages for modern application development:

    • Cross-Platform Support: LandscapistImage works identically across all Compose Multiplatform targets, allowing you to share image loading logic, caching configuration, and UI components without platform-specific conditional code.
    • Pipeline Control: Unlike wrapper libraries, Landscapist exposes the entire image loading pipeline. You can configure network timeouts, cache policies, image transformations, and loading priorities at both global and per-request levels.
    • Performance Optimizations:
      • Android: Uses two-pass inSampleSize for downsampling at decode time, hardware bitmaps for opaque images (API 26+), and an Okio-based disk cache with a byte-bounded LRU memory cache.
      • Efficiency: Concurrent loads of the same image are coalesced into a single fetch/decode operation. Memory cache can trim on system memory pressure via an Android memory-pressure handler.
    • Plugin Ecosystem: Supports seamless integration with plugins for shimmer placeholders, crossfade animations, blur transformations, palette extraction, and zoomable images.
  3. Related components for ImageGallery

    main

    When working with ImageGallery, you may need the following related concepts or components:

    • Landscapist Image: The default renderer used by both ImageGallery and ImageViewer.
    • Zoomable: Provides ZoomableConfig options that are used within ImageViewer to control zoom behavior.
    • Image Component and Plugin: The underlying plugin system used to extend image loading and processing.
    • Placeholder: Support for shimmer and fallback plugins specifically for gallery tiles.
  4. How plugins work in Landscapist

    main

    Plugins are modular components that extend LandscapistImage by hooking into the image loading lifecycle. They allow you to add effects like placeholders, animations, or transformations.

    Key concepts:

    • Composition: Plugins are added using the component parameter with rememberImageComponent.
    • Ordering: Multiple plugins can be combined using the + operator. They are applied in the order they are added.
    • Lifecycle: Plugins can act during the loading phase (placeholders), the transition phase (animations), or the final rendering phase (transformations/palette extraction).
    LandscapistImage(
        imageModel = { imageUrl },
        component = rememberImageComponent {
            +ShimmerPlugin()
            +CrossfadePlugin(duration = 550)
            +PalettePlugin { palette -> /* ... */ }
            +CircularRevealPlugin()
        }
    )
  5. Landscapist Image limitations

    main

    Be aware of the following constraints:

    • Progressive Loading: Only supported for progressive JPEG images from network sources. Does not work with PNG, WebP, or local images.
    • Animated Images: Support is platform-dependent. Android uses native decoders; other platforms may only show the first frame. WebP animation support varies by OS/platform.
    • Platform-Specific Features:
      • Content URIs: Android only.
      • Drawable resources: Android only.
      • SubSampling: Best support on Android.
    • Memory Limits: Very large images (>4096x4096) may fail due to GPU texture size limits or memory constraints.
  6. Manage Memory and Disk Caching

    main

    Landscapist provides direct access to its caching layers for manual management.

    Memory Cache

    Uses an LRU (Least Recently Used) policy. You can clear it, trim it to a specific size, or inspect its current usage.

    Disk Cache

    Provides persistent storage for offline access. You can clear the entire disk cache via landscapist.config.diskCache?.clear().

    Cache Policies

    Control caching behavior on a per-request basis using CachePolicy:

    • CachePolicy.ENABLED: Read and write to both caches.
    • CachePolicy.READ_ONLY: Only read from cache, never write.
    • CachePolicy.WRITE_ONLY: Only write to cache, never read.
    • CachePolicy.DISABLED: Disable caching completely.
    // Memory Cache management
    val memoryCache = landscapist.config.memoryCache
    memoryCache?.clear()
    memoryCache?.trimToSize(32 * 1024 * 1024L)
    
    // Disk Cache management
    val diskCache = landscapist.config.diskCache
    diskCache?.clear()
    
    // Per-request policy
    val request = ImageRequest.builder()
        .model(url)
        .memoryCachePolicy(CachePolicy.ENABLED)
        .diskCachePolicy(CachePolicy.READ_ONLY)
        .build()
  7. How ImageComponent and ImagePlugin work together

    main

    Landscapist uses a plugin-based architecture to manage image loading states and visual effects:

    • ImageComponent: A container that holds a collection of ImagePlugin instances. It manages how different plugins are composed during the image lifecycle.
    • ImagePlugin: An executable Compose interface triggered by specific image states. You can use built-in plugins or implement custom ones to tailor behavior.

    Available Plugin Types

    • PainterPlugin: Composes with a given Painter.
    • LoadingStatePlugin: Executed while the state is ImageLoadState.Loading.
    • SuccessStatePlugin: Executed when the state is ImageLoadState.Success.
    • FailureStatePlugin: Executed when the state is ImageLoadState.Failure.

    By combining these, you can define exactly what happens (e.g., showing a shimmer, a placeholder, or a specific transition) during each phase of the loading process.

    // Example of a custom LoadingStatePlugin
    data class LoadingPlugin(val source: Any?) : ImagePlugin.LoadingStatePlugin {
      @Composable
      override fun compose(
        modifier: Modifier,
        imageOptions: ImageOptions,
        executor: @Composable (IntSize) -> Unit,
      ): ImagePlugin = apply {
        if (source != null && imageOptions != null) {
          ImageBySource(
            source = source,
            modifier = modifier,
            alignment = imageOptions.alignment,
            contentDescription = imageOptions.contentDescription,
            contentScale = imageOptions.contentScale,
            colorFilter = imageOptions.colorFilter,
            alpha = imageOptions.alpha
          )
        }
      }
    }
  8. Understand and handle Image States

    main

    Image states represent the lifecycle of an image loading process, including loading from a network, successful rendering, or failure. You can react to these state changes by providing an onImageStateChanged lambda to your image composable functions (e.g., GlideImage, CoilImage, or FrescoImage).

    Common states across different engines include:

    • None: No state currently active.
    • Loading: The image is currently being fetched or processed.
    • Success: The image loaded successfully. This state often contains the resulting image data.
    • Failure: An error occurred during the loading process.
    // Example using Glide
    GlideImage(
      onImageStateChanged = {
        when (it) {
           GlideImageState.None -> ..
           GlideImageState.Loading -> ..
           is GlideImageState.Success -> ..
           is GlideImageState.Failure -> ..
        }
      },
      ..
    )
  9. Why choose Landscapist Core for SDK development

    main

    Landscapist Core is designed to be exceptionally lightweight, making it suitable for developers building SDKs or libraries where minimizing the dependency footprint is critical to preventing APK bloat.

    Compared to other major image loading libraries, Landscapist Core has a significantly smaller release AAR size:

    • landscapist-core: ~313 KiB (baseline)
    • Coil3 (coil-core 3.5.0): ~468 KiB (+50%)
    • Glide (5.0.7): ~693 KiB (+121%)
    • Fresco: ~1.0 MiB (~3.3x)

    Note: The actual impact on your APK depends on R8 shrinking and the specific features used. Landscapist-core also includes transitive dependencies like Ktor, Okio, coroutines, and atomicfu.

  10. How sizing and modifier propagation works in Landscapist

    main

    A critical concept in Landscapist is that the modifier passed to GlideImage, CoilImage, or FrescoImage is applied to a root BoxWithConstraints container, but it is not automatically forwarded to the loading, success, or failure slots.

    The Mental Model

    1. The outer modifier (e.g., Modifier.size(200.dp)) establishes the layout box.
    2. The state slots (loading, success, failure) are invoked inside that box as BoxScope lambdas.
    3. Because they are inside a BoxScope, you must explicitly opt-in to the parent's size using Modifier.matchParentSize() or Modifier.fillMaxSize() if you want your custom content to fill the requested area.

    If you do not apply a size modifier inside the slot, the content will default to its intrinsic size, which often results in small images or icons appearing in the corner of a large empty box.

  11. How ImagePlugin and ImageComponent work together

    main

    Landscapist uses a plugin system to handle image loading states. An ImagePlugin is a pluggable interface that provides specific logic for different states:

    • PainterPlugin: Composes with a Painter.
    • LoadingStatePlugin: Executed during ImageLoadState.Loading.
    • SuccessStatePlugin: Executed during ImageLoadState.Success.
    • FailureStatePlugin: Executed during ImageLoadState.Failure.

    You can compose multiple plugins into an ImageComponent using rememberImageComponent and the add() method or the + operator.

    data class LoadingPlugin(val source: Any?) : ImagePlugin.LoadingStatePlugin {
    
      @Composable
      override fun compose(
        modifier: Modifier,
        imageOptions: ImageOptions?
      ): ImagePlugin = apply {
        if (source != null && imageOptions != null) {
          ImageBySource(
            source = source,
            modifier = modifier,
            alignment = imageOptions.alignment,
            contentDescription = imageOptions.contentDescription,
            contentScale = imageOptions.contentScale,
            colorFilter = imageOptions.colorFilter,
            alpha = imageOptions.alpha
          )
        }
      }
    }
    
    // Using plugins with add()
    GlideImage(
      imageModel = { poster.image },
      component = rememberImageComponent {
        add(CircularRevealPlugin())
        add(LoadingPlugin(source))
      },
    )
    
    // Using plugins with the + operator
    GlideImage(
      imageModel = { poster.image },
      component = rememberImageComponent {
        +CircularRevealPlugin()
        +LoadingPlugin(source)
      },
    )
  12. Implement shared element transitions between Gallery and Viewer

    main

    You can animate a tapped thumbnail from ImageGallery into the ImageViewer using ImageSharedTransitionConfig.

    Requirements:

    1. Wrap both components in a SharedTransitionLayout (or use NavHost and provide the AnimatedContentScope).
    2. Pass the exact same ImageSharedTransitionConfig instance to both ImageGallery and ImageViewer.
    3. The sharedTransitionScope should be the scope provided by the SharedTransitionLayout or NavHost.
    4. The animatedContentScope should be the scope provided by the AnimatedContent or composable destination.
    SharedTransitionLayout {
      AnimatedContent(
        targetState = showViewer,
        transitionSpec = { fadeIn() togetherWith fadeOut() },
        label = "gallery-viewer",
      ) {\ viewerVisible ->
        val animatedContentScope = this
        if (!viewerVisible) {
          ImageGallery(
            images = imageUrls,
            onImageClick = { index, _ ->
              selectedPage = index
              showViewer = true
            },
            sharedTransition = ImageSharedTransitionConfig(
              sharedTransitionScope = this@SharedTransitionLayout,
              animatedContentScope = animatedContentScope,
            ),
          )
        } else {
          ImageViewer(
            images = imageUrls,
            state = rememberImageViewerState(
              initialPage = selectedPage,
              pageCount = { imageUrls.size },
            ),
            onDismiss = { showViewer = false },
            sharedTransition = ImageSharedTransitionConfig(
              sharedTransitionScope = this@SharedTransitionLayout,
              animatedContentScope = animatedContentScope,
            ),
          )
        }
      }
    }