Skip to content

Composition loaders pipelines and bitmap ownership - #48

Merged
SKProCH merged 18 commits into
masterfrom
composition
Aug 16, 2026
Merged

SKProCH merged 18 commits into
masterfrom
composition

Conversation

@SKProCH

@SKProCH SKProCH commented Aug 15, 2026 •

Copy link
Copy Markdown
Member

This is a complete rework of internals of the library:

  • Replaced inheritance-based image loaders with a composable loading pipeline
  • Added pipeline presets and a builder for customizing source resolution, transport, decoding, and caching
  • Introduced lease-based image ownership with proper control lifecycle handling
  • Kept the existing loaders as compatibility facades over the new pipeline

Instead current inheritance based loaders will be:

Image loading pipeline

ImageLoaderPipeline and ImageLoaderPipelineBuilder are the primary APIs for configuring image loading. The pipeline composes source resolution, external transport, encoded byte caching, bitmap decoding and decoded image retention. Start with the closest builder preset, then replace only the components your application needs to customize:

using AsyncImageLoader.Core;

var loader = ImageLoaderPipelineBuilder.RamCached(new MemoryImageCacheOptions {
    AbsoluteExpiration = TimeSpan.FromMinutes(10),
    SlidingExpiration = TimeSpan.FromMinutes(2)
})
    .UseHttpClient(new HttpClient { Timeout = TimeSpan.FromSeconds(30) })
    .UseDecoder(new MyBitmapDecoder())
    .Build();

ImageLoader.AsyncImageLoader = loader;

The available presets are:

  • Uncached() downloads and decodes each request without retaining the decoded image.
  • RamCached(...) shares decoded images and retains them in the lease-aware RAM cache.
  • DiskCached(...) adds a persistent encoded disk cache for HTTP and HTTPS sources.

All presets use the same default source resolvers, HTTP transport and bitmap decoder. They are starting configurations, not separate extension hierarchies.

Set the resulting pipeline globally through ImageLoader.AsyncImageLoader or ImageBrushLoader.AsyncImageLoader, or assign it to the Loader property of an individual AdvancedImage. Dispose the previous global loader when replacing it.

Pipeline components

  • ImageLoadRequest carries the source string and optional Avalonia context (BaseUri and IStorageProvider) through the pipeline.
  • IImageSourceResolver handles non-network sources. The default CompositeImageSourceResolver tries FileImageSourceResolver, StorageImageSourceResolver and AvaloniaAssetSourceResolver in order.
  • IImageTransport retrieves external encoded data. The default HttpImageTransport handles absolute HTTP and HTTPS sources using HttpClient.
  • IImageByteCache stores encoded image data before decoding. DiskImageByteCache persists HTTP responses under hashed keys and is enabled by the DiskCached(...) preset.
  • IBitmapDecoder converts an encoded stream into an Avalonia Bitmap. The default BitmapDecoder reads non-seekable streams asynchronously before constructing the bitmap.
  • IImageMemoryCache coordinates concurrent requests and returns independent consumer leases. TransientImageCache performs no retention; MemoryImageCache provides RAM retention with absolute and sliding expiration.
  • IImageLease represents one consumer's ownership of an image. UI integrations release their lease when a source is replaced or detached, while the memory cache controls how long its own reference is retained.
  • ImageLoaderPipeline orchestrates these components and implements IAsyncImageLoader.

The builder methods replace individual components:

  • UseSourceResolver(...)
  • UseTransport(...)
  • UseDecoder(...)
  • UseMemoryCache(...)
  • UseByteCache(...)
  • UseHttpClient(...)

The built pipeline owns and disposes its configured memory cache. A supplied HttpClient remains caller-owned unless UseHttpClient(client, disposeHttpClient: true) is used. A builder can build only one pipeline because ownership of its cache is transferred during Build().

Compatibility loaders

The original ready-made loaders remain available as compatibility and convenience facades:

These types delegate to the same pipeline presets. They are useful for existing applications and simple configurations, but new customization should use ImageLoaderPipelineBuilder instead of inheriting from a loader. On mobile, WASM and other restricted platforms, provide a valid writable cache path before using disk caching.

Custom loaders

You can implement every component of the pipeline individually.

Or implement IAsyncImageLoader directly only when the complete built-in pipeline is not appropriate. LoadAsync receives an ImageLoadRequest and returns an IImageLease; such an implementation replaces source resolution, transport, decoding and caching rather than customizing one pipeline stage.

Use ImageLease.Owned, ImageLease.NonOwning or ImageLease.Create to make ownership explicit when implementing a custom loader.

RAM retention

RAM retention can be configured when creating a loader. Expiration releases the loader's strong reference;
if the UI still uses the bitmap, it can be reused through a weak reference:

ImageLoader.AsyncImageLoader = ImageLoaderPipelineBuilder.RamCached(new MemoryImageCacheOptions {
    AbsoluteExpiration = TimeSpan.FromMinutes(10),
    SlidingExpiration = TimeSpan.FromMinutes(2)
}).Build();

@SKProCH SKProCH self-assigned this Aug 15, 2026
Comment thread AsyncImageLoader.Avalonia/Core/Caching/DiskImageByteCache.cs
Comment thread AsyncImageLoader.Avalonia.Tests/TestTimeProvider.cs Outdated
@SKProCH
SKProCH merged commit f0babf3 into master Aug 16, 2026
@SKProCH
SKProCH deleted the composition branch August 16, 2026 22:37
@SKProCH

SKProCH commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

Migration Guide

This release replaces the inheritance-based loader API with a composable pipeline.


Use the pipeline builder

Replace the built-in loader facades with the corresponding presets:

Old / Deprecated New Preset Builder
new BaseWebImageLoader() ImageLoaderPipelineBuilder.Uncached().Build()
new RamCachedWebImageLoader(options) ImageLoaderPipelineBuilder.RamCached(options).Build()
new DiskCachedWebImageLoader(cacheFolder) ImageLoaderPipelineBuilder.DiskCached(cacheFolder).Build()

Note: The old facades remain available but are now marked as obsolete.


Configure individual components

Instead of inheriting from a loader and overriding methods, replace individual pipeline components:

var loader = ImageLoaderPipelineBuilder.RamCached()
    .UseSourceResolver(sourceResolver)
    .UseTransport(transport)
    .UseDecoder(decoder)
    .UseMemoryCache(memoryCache)
    .UseByteCache(byteCache)
    .Build();

Update custom loaders

IAsyncImageLoader.LoadAsync now receives an ImageLoadRequest and returns an IImageLease:

Task<IImageLease?> LoadAsync(
    ImageLoadRequest request,
    CancellationToken cancellationToken = default);

Use ImageLease.Owned, ImageLease.NonOwning, or ImageLease.Create to define ownership.


Update cache options

RamCacheOptions was removed. Use MemoryImageCacheOptions instead:

new MemoryImageCacheOptions 
{
    AbsoluteExpiration = TimeSpan.FromMinutes(10),
    SlidingExpiration = TimeSpan.FromMinutes(2)
}

More info in README.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants