cachet_image

A modern, isolate-friendly cached network image package for Flutter — built with Dart 3 sealed classes, stream-based load states, and a real LRU disk cache.

Why not cached_network_image?

cachet_image targets the same problem with a more current Dart 3 approach:

  • Sealed state, not booleans. CachetState is a sealed class (CachetLoading / CachetLoaded / CachetError) so UI code uses exhaustive switch expressions instead of juggling nullable flags.
  • Structured failures. CachetFailure splits into CachetNetworkFailure, CachetDecodeFailure, and CachetCacheFailure so you can branch on why something failed.
  • Stream-based progress. CachetLoader.load() returns a Stream<CachetState>, so download progress, cache hits, and errors are all just events on one stream — easy to wire into Riverpod/Bloc.
  • Real LRU eviction. The cache manager tracks size + last-access time per entry and evicts oldest-first once you exceed maxCacheBytes, instead of relying on the OS to clean up a temp folder.
  • Background decoding. decodeImageOffMainIsolate is exposed separately so you can decode large images without blocking the UI thread, independent of the fetch/cache pipeline.

Install

dependencies:
  cachet_image: ^0.1.0

Usage

import 'package:cachet_image/cachet_image.dart';

CachetImage(
  url: 'https://example.com/photo.jpg',
  fit: BoxFit.cover,
  placeholderBuilder: (context, progress) => Center(
    child: CircularProgressIndicator(value: progress),
  ),
  errorBuilder: (context, failure) => switch (failure) {
    CachetNetworkFailure() => const Icon(Icons.wifi_off),
    CachetDecodeFailure() => const Icon(Icons.broken_image),
    CachetCacheFailure() => const Icon(Icons.storage),
  },
)

Configuring the cache

Call this once at startup, before any CachetImage widgets build, if you want non-default limits:

void main() {
  CachetCacheManager.configure(
    maxCacheBytes: 100 * 1024 * 1024, // 100 MB
    maxAge: const Duration(days: 14),
  );
  runApp(const MyApp());
}

Clearing the cache

await CachetCacheManager.instance.clearAll();
await CachetCacheManager.instance.evictUrl('https://example.com/photo.jpg');
final stats = await CachetCacheManager.instance.stats();
print('${stats.entryCount} entries, ${stats.totalBytes} bytes');

Using the loader directly

If you want the raw bytes/stream without the widget (e.g. to pre-warm the cache, or feed a custom ImageProvider):

final loader = CachetLoader();
await for (final state in loader.load('https://example.com/photo.jpg')) {
  switch (state) {
    case CachetLoading(:final progress):
      print('loading: $progress');
    case CachetLoaded(:final bytes, :final fromCache):
      print('loaded ${bytes.length} bytes (fromCache: $fromCache)');
    case CachetError(:final failure):
      print('error: ${failure.message}');
  }
}

Project layout

lib/
  cachet_image.dart        # public API barrel
  src/
    models.dart             # CachetState / CachetFailure sealed classes
    cache_manager.dart       # disk cache + LRU eviction
    loader.dart               # fetch + cache-check stream
    cachet_image_widget.dart  # the CachetImage widget
example/
  main.dart                 # grid demo

Roadmap ideas

  • AVIF/WebP-aware format negotiation with fallback
  • BlurHash / ThumbHash placeholder decoding built in
  • Priority-queue prefetching for list scrolling
  • Pluggable cache backend (e.g. sqlite3 for large caches with querying)

License

MIT

Libraries

cachet_image
A modern, isolate-friendly cached network image package for Flutter.