disk_cached_image
A Flutter widget and cache service that downloads a network image once and serves it from disk on later builds and app launches.
Features
DiskCachedImagewidget that renders a cached fileDiskImageCacheservice withfetch,evict,clear,sizeandentries- TTL refresh through
maxAge - Atomic writes: the cache writes a temporary file, then renames it
- Single-flight downloads, so concurrent fetches for one key share a request
- Cache key validation against path traversal and Windows device names
- Gapless refresh: a stale image is replaced without a blank frame
- Optional byte limit with least-recently-used eviction
- Optional memory tier for recently decoded images
- Download progress through
onProgress
Install
flutter pub add disk_cached_image
Usage
final cache = DiskImageCache();
DiskCachedImage(
url: 'https://picsum.photos/seed/one/200',
cacheKey: 'coin-one',
width: 64,
height: 64,
maxAge: const Duration(days: 7),
cache: cache,
placeholder: const Center(child: CircularProgressIndicator()),
errorBuilder: (context, error, stackTrace) => const Icon(Icons.broken_image),
);
The cacheKey names the file in the cache, and it's required. Share one
DiskImageCache between images so they reuse its HTTP client.
Fetch a file without the widget:
final DiskImageCacheResult result = await cache.fetch(
url: Uri.parse('https://picsum.photos/seed/one/200'),
cacheKey: 'coin-one',
maxAge: const Duration(days: 7),
);
print(result.file.path); // path of the cached file
print(result.downloaded); // true when this call downloaded the file
await cache.evict('coin-one');
final int bytes = await cache.size();
await cache.clear();
Notes:
- The cache identifies files by
cacheKeyonly, not by URL. When the URL changes, use a new key or callevict. - A
nullmaxAgekeeps a file until you evict or clear it. fetchthrows anArgumentErrorfor an invalid key, anHttpExceptionfor a non-200 response, and aTimeoutExceptionwhen the cache timeout runs out (30 seconds by default).DiskImageCachetakes aclientand adirectoryProviderfor tests.
Revalidation
The cache records the ETag and Last-Modified response headers in a JSON
sidecar under .metadata/. Once an entry is stale, the next request carries
If-None-Match or If-Modified-Since when those values exist. A 304
response keeps the file, refreshes its timestamp and skips downloading the
bytes again.
Stale-while-revalidate
Set staleWhileRevalidate on the widget to show the stale file right away and
refresh it in the background:
DiskCachedImage(
url: 'https://picsum.photos/seed/one/200',
cacheKey: 'coin-one',
maxAge: const Duration(hours: 1),
staleWhileRevalidate: true,
);
When the refresh finishes, the widget redraws with the new bytes. A failed
refresh leaves the stale image in place. The service exposes the same
behaviour through result.refresh.
Precache
await cache.precache(
url: Uri.parse('https://picsum.photos/seed/one/200'),
cacheKey: 'coin-one',
);
await cache.precacheAll([
(url: Uri.parse('https://picsum.photos/seed/two/200'), cacheKey: 'coin-two'),
(url: Uri.parse('https://picsum.photos/seed/three/200'), cacheKey: 'coin-three'),
]);
Clear by pattern
final int removed = await cache.clearByPattern('https://picsum.photos/seed/');
final int matched = await cache.clearByPattern(RegExp(r'picsum\.photos'));
A String pattern matches URLs that start with it. Any other pattern matches anywhere in the URL. Entries without metadata stay put.
Size limit
Set maxBytes to keep the cache directory bounded. After each download, the
cache deletes the least recently used files until the total fits. Each entry
keeps its access time in the metadata sidecar; files without metadata fall
back to their modification time.
final cache = DiskImageCache(maxBytes: 50 << 20); // 50 MiB
await cache.prune(); // enforces the limit now
One file larger than the limit stays in the cache.
Inspect and expire entries
entries() lists the files with their size, modification time and last access
time, oldest access first. clearOlderThan deletes the files that the cache
has not served or written for a while, for example to expire images that a user
has not looked at recently:
for (final entry in await cache.entries()) {
print('${entry.cacheKey}: ${entry.sizeBytes} bytes');
}
final int expired = await cache.clearOlderThan(const Duration(days: 30));
Temporary files from interrupted downloads are never listed. An entry without metadata reports its modification time as the last access. A non-positive duration deletes nothing.
Memory tier
MemoryImageCache holds recently decoded image bytes in memory. Pass one
shared instance to the widgets to skip disk reads on rebuilds:
final memoryCache = MemoryImageCache(maxBytes: 16 << 20);
DiskCachedImage(
url: 'https://picsum.photos/seed/one/200',
cacheKey: 'coin-one',
cache: cache,
memoryCache: memoryCache,
);
The store drops the least recently used images when the byte limit would be
exceeded. DiskCachedImage renders from memory when the bytes are there and
fills the store after a load.
Download progress
Pass onProgress to the cache or the widget to follow a download:
DiskCachedImage(
url: 'https://picsum.photos/seed/one/200',
cacheKey: 'coin-one',
onProgress: (progress) => print('${(progress * 100).round()} %'),
);
Progress only arrives during a real download, and only when the server sends the content length.
Platforms
The package reads and writes files with dart:io and path_provider. It
supports Android, iOS, macOS, Windows and Linux. Web is not supported.
Example
The example/ app renders two cached images and clears the cache.
Licence
See LICENSE.
Libraries
- disk_cached_image
- A Flutter widget and cache service that downloads network images to disk once, serves them from disk, and supports TTL, eviction, clearing, a size limit, and an optional memory tier.