piksel
A type-safe, extensible image loading & caching pipeline for Flutter.
Network, SVG, AVIF, GIF, video frames and blurhash — one request model, one
widget, one cache. The parts Flutter's engine already gives you (off-thread
decode, ImageProvider) are kept; the parts it lacks (disk cache, format
unification, transforms, retry, animation control) are built.
PikselImage.network('https://example.com/photo.jpg')
Why piksel
- Type-safe by construction.
LoadStateandImageResultare sealed — the compiler makes you handle loading, success and error. No stringly-typed options, no silent fallthrough. - Fast where it counts. A warm memory hit renders in the widget's first build — no placeholder flash, no async hop, no extra rebuild. Cold loads run through bounded fetch/decode pools with in-flight dedup (20 identical grid cells = 1 fetch + 1 decode). Measured head-to-head in BENCHMARK.md.
- Three cache tiers. Decoded images in memory (LRU, clone-on-read), decoded+transformed results on disk, raw downloads on disk. On web, disk tiers persist in IndexedDB.
- Every format through one pipeline. GIF and animated WebP play out of the box; SVG, AVIF and video frames are one-line plugin registrations.
- Resilient. Exponential-backoff retry for transient failures (connection errors, HTTP 408/429/5xx); permanent failures fail fast. Cooperative cancellation stops off-screen downloads mid-stream.
- Scroll-aware prefetch.
PikselPrefetcherwarms the next tiles in the direction of travel from youritemBuilder— bounded so it never starves visible loads, cancelled the moment the scroll direction reverses.
Quick start
// 1. Common case — one line.
PikselImage.network('https://…/photo.jpg', width: 120, height: 120)
// 2. Full control: transforms, placeholder, typed exhaustive state.
PikselImage(
ImageRequest(
source: NetworkSource(url, headers: auth),
transformations: const [RoundedCorners(16), Blur(4)],
placeholder: BlurhashStateImage(hash),
error: IconStateImage(Icons.broken_image),
),
builder: (context, state) => switch (state) {
LoadLoading(:final progress) => LinearProgressIndicator(value: progress),
LoadSuccess(:final result) => RawImage(image: result.image),
LoadError() => const Icon(Icons.error),
},
)
// 3. Animated sources play automatically; pin the first frame if you want.
PikselImage.network('https://…/animation.gif', animate: false)
// 4. Drops into ANY Flutter widget via ImageProvider interop.
final provider = request.toImageProvider();
Image(image: provider);
BoxDecoration(image: DecorationImage(image: provider));
// 5. Imperative — prefetch / non-widget.
await Piksel.instance.prefetch(request);
final result = await Piksel.instance.execute(request);
Configure once (no forced singleton — hold your own instance for DI):
Piksel.configure(Piksel(
memoryCache: MemoryCache.lru(maxSizeBytes: 128 << 20),
retryPolicy: const RetryPolicy(maxAttempts: 3),
components: const ComponentRegistry(
decoders: [SvgDecoder(), AvifDecoder()], // from plugin packages
),
));
Architecture
Widget / DX PikselImage · request.toImageProvider()
Engine Piksel · RequestInterceptor chain (retry) · dedup · pools
Components Fetcher · Decoder (+AnimatedDecoder) · Transformation
Cache MemoryCache (decoded) · ResultCache (disk) · DownloadCache (disk)
Data ImageSource · DataSource · Fetch/DecodeResult
Platform dart:io files (native/desktop) · IndexedDB (web)
Type-safety
sealed LoadState/sealed ImageResult— the compiler forces you to handle loading, success and error.ImageSourcevariants (Network/Asset/File/Bytes) — fetchers dispatch on the type; plugins add their own sources (VideoFrameSource).enum CachePolicywithreadAllowed/writeAllowed;Precision,Scale.Extra<T>— typed request metadata instead ofMap<String, Object?>.
Concurrency (correctness under image-heavy grids)
- Bounded parallelism — separate
Semaphorepools for fetch vs decode, so a 200-cell grid can't open 200 sockets or decode 200 bitmaps at once. - In-flight dedup — N widgets requesting the same URL share one fetch+decode.
ui.Imageref-safety — the memory cache owns a private clone and hands every caller its own clone; a consumer disposing its handle can never free pixels still on screen.- Cooperative cancellation —
CancelTokenchecked at every stage boundary; widget disposal cancels in-flight work. - Size-aware decode — images are down-sampled to the box they'll paint
into (kills OOM-on-grids without manual
cacheWidth).
Extensibility
Three seams, each one interface:
class SvgDecoder implements Decoder {
bool handles(FetchResult f) => f.mimeType == 'image/svg+xml';
Future<DecodeResult> decode(ctx, f) async { /* … */ }
}
Fetcher— teach piksel a new source.Decoder— teach it a new format; sniff bytes viaDataSource.magic. ImplementAnimatedDecoderto provide multi-frame playback.Transformation— pixel operations that participate in cache keys.
Interceptors (RequestInterceptor / DecodeInterceptor) wrap the chain for
cross-cutting concerns — retry and the result cache are themselves
interceptors.
Format plugins
| Package | Adds |
|---|---|
piksel_svg |
SVG rendering through the same widget + cache |
piksel_avif |
AVIF stills and animation |
piksel_video_frame |
Frame extraction from video URLs/files |
piksel_blurhash |
Decoded blurhash placeholders |
Each is a separate package; the core depends on none of them.
Performance
Measured, not claimed: BENCHMARK.md has a reproducible head-to-head against the ecosystem — load latency, scroll frame timings, methodology, and honest caveats (including what's inside measurement noise).
Libraries
- piksel
- piksel — a type-safe, extensible image loading and caching pipeline for Flutter. Unifies network, SVG, AVIF, GIF, video-frame and blurhash behind one request/decode/transform pipeline with a three-tier cache.