flutter_media_cache 3.0.0
flutter_media_cache: ^3.0.0 copied to clipboard
A powerful Flutter package for caching images and videos with customizable expiration time and automatic cache management.
flutter_media_cache #
Production-grade media caching for Flutter — images and videos — with a priority download queue, automatic LRU eviction, deduplication, exponential- backoff retry, per-download progress streams, and conditional HTTP GET.
Supports all Flutter platforms: Android · iOS · Web · macOS · Windows · Linux.
Features #
| Feature | Details |
|---|---|
| 🗂️ Two-tier cache | In-memory LRU + disk storage |
| 🚦 Priority queue | high / normal / low per request |
| 🔁 Deduplication | Same URL → one in-flight download |
| 🔄 Retry | Exponential backoff, configurable attempts |
| 📡 Progress stream | Stream<DownloadProgress> with byte counts |
| 🌐 Conditional GET | ETag / If-None-Match / Last-Modified |
| 🧹 LRU eviction | Separate limits for memory (items) and disk (bytes) |
| 🎬 Video support | CachedVideo widget with built-in controls |
| ⏸️ Cancellation | Cancel any queued or in-flight download |
| 📦 Preloading | preloadAll([url1, url2, ...]) |
| 📊 Analytics | Hit rate, miss count, total cache size |
| 🌍 All platforms | Android · iOS · Web · macOS · Windows · Linux |
Getting started #
dependencies:
flutter_media_cache: ^3.0.0
Initialize once #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await MediaCacheManager.initialize(
config: CacheConfig(
maxDiskBytes: 300 * 1024 * 1024, // 300 MB disk
maxMemoryItems: 200, // 200 items in RAM
maxAge: const Duration(days: 30),
maxConcurrentDownloads: 6,
maxRetries: 3,
),
);
runApp(const MyApp());
}
Alternatively, use CacheManagerProvider near the root of your widget tree
and skip the manual initialize call:
return CacheManagerProvider(
config: CacheConfig(maxDiskBytes: 300 * 1024 * 1024),
child: MaterialApp(home: HomePage()),
);
Usage #
Display a cached image #
CachedImage(
imageUrl: 'https://picsum.photos/400/300',
width: 400,
height: 300,
fit: BoxFit.cover,
borderRadius: BorderRadius.circular(12),
priority: DownloadPriority.high,
placeholder: const ShimmerBox(), // optional
errorWidget: const ErrorPlaceholder(), // optional
)
Custom progress indicator #
CachedImage(
imageUrl: 'https://example.com/large.jpg',
progressIndicatorBuilder: (context, url, progress) {
return CircularProgressIndicator(value: progress.progress);
},
)
Display a cached video #
CachedVideo uses a builder pattern — you provide any video player widget
via the builder callback. The CacheResult exposes url, filePath,
bytes, and isFromCache.
CachedVideo(
videoUrl: 'https://example.com/clip.mp4',
builder: (context, result) => AspectRatio(
aspectRatio: 16 / 9,
child: YourVideoPlayer(filePath: result.filePath),
),
)
With a custom loading state:
CachedVideo(
videoUrl: 'https://example.com/clip.mp4',
placeholder: const MyLoadingWidget(),
builder: (context, result) => VideoPlayerWidget(path: result.filePath),
)
Programmatic fetch (bytes) #
final result = await MediaCacheManager.instance.getMedia(
'https://example.com/image.jpg',
priority: DownloadPriority.high,
);
if (result.bytes != null) {
// Use result.bytes (Uint8List) or result.filePath (String? on non-web)
}
Fetch with progress stream #
final (:result, :progress) = MediaCacheManager.instance.getMediaWithProgress(
'https://example.com/video.mp4',
);
progress.listen((p) {
print('${p.bytesDownloaded} / ${p.totalBytes ?? "?"} '
'— ${p.status.name}');
});
final data = await result;
Custom progress UI with DownloadProgressBuilder #
DownloadProgressBuilder(
url: 'https://example.com/video.mp4',
builder: (context, progress, child) {
if (progress == null || progress.isCompleted) return child!;
return LinearProgressIndicator(value: progress.progress);
},
child: const Icon(Icons.check_circle, color: Colors.green),
)
Preloading #
// Fire-and-forget; uses low priority so it doesn't block visible content.
await MediaCacheManager.instance.preloadAll([
'https://example.com/next1.jpg',
'https://example.com/next2.jpg',
]);
Cache management #
final manager = MediaCacheManager.instance;
// Is this URL cached?
final cached = manager.isCached('https://example.com/image.jpg');
// Remove one entry.
await manager.removeEntry('https://example.com/image.jpg');
// Remove all expired entries.
await manager.clearExpired();
// Wipe everything.
await manager.clearAll();
// Analytics snapshot.
final stats = manager.stats;
print('Hit rate: ${(stats.hitRate * 100).toStringAsFixed(1)}%');
print('Disk used: ${stats.totalSizeBytes ~/ 1024} KB');
Configuration reference #
CacheConfig(
maxDiskBytes: 200 * 1024 * 1024, // 200 MB (default)
maxMemoryItems: 150, // LRU item cap (default)
maxAge: const Duration(days: 14), // TTL (default)
maxConcurrentDownloads: 4, // parallel limit (default)
maxRetries: 3, // retry attempts (default)
retryBaseDelay: Duration(seconds: 1), // backoff base (default)
downloadTimeout: Duration(seconds: 30),
connectTimeout: Duration(seconds: 10),
useMemoryCache: true,
useConditionalGet: true, // ETag / Last-Modified
subdirectoryName: 'flutter_media_cache',
customHeaders: {'Authorization': 'Bearer token'},
)
Architecture #
┌──────────────────────────────────────────────────────────┐
│ MediaCacheManager │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ LruMemory │ │ CacheIndex │ │ DownloadEngine │ │
│ │ Cache │ │ (disk meta) │ │ (queue+retry) │ │
│ └─────────────┘ └──────────────┘ └────────────────┘ │
└──────────────────────────────────────────────────────────┘
▲ ▲ ▲
Memory hit Disk hit Network miss
(< 1 ms) (I/O) (HTTP + retry)
Request flow:
- Check
LruMemoryCache(in-process, instant). - Check
CacheIndex→ read file from disk → promote to memory. - Enqueue in
DownloadEngine— deduplicated, priority-sorted. - On success: persist bytes to disk, update index, promote to memory.
- On failure: exponential-backoff retry up to
maxRetries.
Web platform notes #
On web, the package automatically switches to a memory-only caching strategy:
| Feature | Native (Android/iOS/desktop) | Web |
|---|---|---|
| Storage | Disk + memory | Memory only |
| Cache persistence | Survives app restart | Lost on page reload |
CachedImage |
Bytes from disk/network | Bytes from memory/network |
CachedVideo |
CacheResult.filePath available |
CacheResult.filePath is null |
No code changes are required — the same API works everywhere. On web, CacheResult.filePath is null and CacheResult.bytes always contains the image data.
Migration from v2 #
| v2 | v3 |
|---|---|
CachedVideo(videoUrl: url, autoPlay: false, showControls: true) |
CachedVideo(videoUrl: url, builder: (ctx, r) => YourPlayer(filePath: r.filePath)) |
Migration from v1 #
| v1 | v2 |
|---|---|
FlutterMediaCache() |
MediaCacheManager.initialize() |
CachedImage(url: ...) |
CachedImage(imageUrl: ...) |
FlutterMediaCache.clearCache() |
MediaCacheManager.instance.clearAll() |
| No progress support | getMediaWithProgress(url) |
| No video widget | CachedVideo(videoUrl: ...) |
Contributing #
PRs and issues are welcome at github.com/SwanFlutter/flutter_media_cache.
License #
MIT © SwanFlutter