A tiny, dependency-free Flutter utility that pre-warms PaintingBinding.instance.imageCache from any non-widget code — controllers, services, repositories — without needing a BuildContext. Pairs naturally with Image.network, cached_network_image, or any other image widget: they all read the same global imageCache.
- Context-free — call from controllers, repositories, services, anywhere. No
BuildContextrequired. - String shortcut + generic escape hatch —
Overture.warm(urls)for the URL case,Overture.warmWith(builder, inputs)for anyImageProvider(CNI, custom headers, file-backed, etc.). - Drop-in cache hit — uses the exact same key the widget will compute, so the next render paints synchronously from cache.
- Forgiving — null/empty entries skipped, duplicates dropped, per-item errors swallowed.
- Bounded — total wait is capped by
timeout(default5s); never blocks the caller forever. - Zero dependencies —
flutteronly. No transitive surprises.
- Why
- Installation
- Quick Start
- API
- How it works
- Pairing with cached_network_image
- Caveats
- Example App
- Why "overture"?
- License
Flutter's built-in precacheImage(provider, context) requires a BuildContext. That's a poor fit for clean-architecture apps where the data layer (controllers, services, repositories) shouldn't depend on Flutter's widget tree — and it's awkward to call from a route guard, a background fetch, a stream listener, or anywhere else outside build(). Overture.warm does the same job without the context: pass it the URLs as soon as they arrive (right after a JSON fetch is the canonical spot) and the bitmaps are decoded into the global imageCache while the rest of your app is still composing the UI.
The payoff is perceived UI quality. When the widget tree finally builds, every Image.network(url) finds the bitmap already in cache and paints synchronously on the first frame — no progress spinners, no fade-in, no layout shift, no flash of placeholder. For galleries, lists with thumbnails, hero transitions, and any flow where the user notices the difference between "blank → loading → image" and "image, instantly", warming the cache up-front turns a janky reveal into a clean one.
flutter pub add overtureOr in pubspec.yaml:
dependencies:
overture: ^0.1.0Warm the cache from any non-widget code, then build your widgets normally — the first frame finds the bitmaps already decoded.
import 'package:overture/overture.dart';
class GalleryController {
GalleryState state = const GalleryState.idle();
Future<void> load() async {
final List<Photo> photos = await api.fetchPhotos();
// Pre-warm the image cache before flipping to the "ready" state.
await Overture.warm(photos.map((Photo p) => p.thumbnailUrl));
state = GalleryState.ready(photos);
}
}// Later, in a widget — no flicker, no spinner on the first frame.
GridView.count(
crossAxisCount: 2,
children: <Widget>[
for (final Photo p in photos) Image.network(p.thumbnailUrl),
],
)Need something other than NetworkImage from a URL? Reach for warmWith — it accepts any builder that returns an ImageProvider:
// Disk persistence via cached_network_image.
await Overture.warmWith(CachedNetworkImageProvider.new, urls);
// Authenticated requests with a custom header builder.
await Overture.warmWith(
(String url) => NetworkImage(url, headers: <String, String>{
'Authorization': 'Bearer $token',
}),
urls,
);
// File-backed images (e.g. local cache, generated thumbnails).
await Overture.warmWith(FileImage.new, files);
// Bundled assets — read the AssetImage caveat below before relying on this
// for DPR-variant assets (1.5x, 2x, 3x).
await Overture.warmWith(AssetImage.new, <String>[
'assets/onboarding/hero.png',
'assets/icons/logo.png',
]);
// Cross-package assets — microapp / multi-package setups where each image
// lives in a different Flutter package. The closure captures `package` so
// every entry resolves under `packages/<pkg>/...` automatically.
await Overture.warmWith(
(String name) => AssetImage(name, package: 'design_system'),
<String>['assets/icons/logo.png', 'assets/icons/badge.png'],
);warm and warmWith both return a Future<void> that completes when every entry has loaded into the cache (or failed individually), or when the timeout elapses — whichever comes first. Neither ever throws.
sealed class Overture {
static Future<void> warm(
Iterable<String?> urls, {
Duration timeout = const Duration(seconds: 5),
});
static Future<void> warmWith<T extends Object>(
ImageProvider Function(T) builder,
Iterable<T?> inputs, {
Duration timeout = const Duration(seconds: 5),
});
}| Method | When to use |
|---|---|
warm(urls) |
Default. URL strings + plain NetworkImage. The 80% case. |
warmWith(builder, inputs) |
When you need a different ImageProvider: CachedNetworkImageProvider for disk persistence, NetworkImage with custom headers, FileImage, anything custom. |
warm(urls) is a thin wrapper over warmWith<String>(NetworkImage.new, urls) (with empty strings normalized to null). The two paths share the same dedup / listener / timeout pipeline, so behavior is identical except for the input shape.
Behavior shared by both methods:
nullentries are skipped (and empty strings forwarm) — pass nullable iterables straight from a model without filtering first.- Duplicates within the same call are dropped —
warmdedups by URL string,warmWithdedups by==onT. - Per-item errors are swallowed. Whether a URL 404s, hits a
SocketException, or the bytes don't decode, the future for that single item completes silently and the rest keep going. Warming never throws. - The total wait is capped by
timeout— a slow CDN or hung connection cannot block the caller indefinitely. Items still in flight when the timeout fires are not cancelled (they may eventually populate the cache anyway), but the future returns.
Flutter's image cache is a global ImageCache instance on PaintingBinding, keyed by ImageProvider. NetworkImage.obtainKey() returns SynchronousFuture(this) with == over (url, scale, headers); CachedNetworkImageProvider is keyed similarly. overture resolves each provider against ImageConfiguration.empty, which lands on the same key the widget will compute later when there's no DPR-dependent variant in play. The result: a guaranteed cache hit when the widget renders, and the first frame paints the bitmap synchronously from RAM.
overture only warms RAM. For cross-session offline rendering, pair it with cached_network_image:
import 'package:cached_network_image/cached_network_image.dart';
import 'package:overture/overture.dart';
class GalleryController {
Future<void> load() async {
final List<Photo> photos = await api.fetchPhotos();
// Warm Flutter's imageCache via CNI's provider — populates RAM
// *and* CNI's disk cache in one pass.
await Overture.warmWith(
CachedNetworkImageProvider.new,
photos.map((Photo p) => p.url),
);
state = GalleryState.ready(photos);
}
}
// Render with the same provider — cache hit on first frame.
CachedNetworkImage(imageUrl: photo.url);The pair gives you:
- First load —
Overture.warmWithruns the GETs in parallel during your spinner; first render is synchronous from RAM. - Cross-session — CNI persists bytes to disk. App reopen →
warmWithreads from disk (~10–50 ms per image), no network.
The two libraries cooperate via the global imageCache. Both prefetch and render use CachedNetworkImageProvider(url) as the key, so they hit the same cache entry.
overtureresolves providers withImageConfiguration.empty. That's fine for providers whose cache key is configuration-independent (NetworkImage,CachedNetworkImageProvider,FileImage). ForAssetImage, the resolution depends on the asset shape:- Single-resolution assets (one file per logical asset, no
1.5x/,2x/,3x/variants inpubspec.yaml) — the cache key is stable andOverture.warmWith(AssetImage.new, ...)lands on the same key the widget will compute. Works fine. - DPR-variant assets —
AssetImage.obtainKeypicks the variant fromconfiguration.devicePixelRatio. WithImageConfiguration.emptythe DPR isnulland the resolved key may differ from the widget's at render time, leading to a cache miss. First-class asset support with explicit DPR plumbing is on the roadmap.
- Single-resolution assets (one file per logical asset, no
- Items still in flight when
timeoutfires are not cancelled. They may eventually populate the cache anyway, but the future returns.
A demo app lives in example/. It shows two columns side-by-side: the left renders cold (Image.network straight to the network), the right was pre-warmed with Overture.warm before render. Tap "Load 6 images" to see the difference: the right column paints instantly from cache while the left shows a spinner per tile. Run it with:
cd example
flutter create . # generate platform folders the first time
flutter runIn an opera, the overture is the orchestral piece that plays before the curtain rises — it sets the mood while the audience settles in, so the moment the stage lights come up the show starts clean. This package does the same for your image cache: it warms the bitmaps in RAM while the app is still composing the UI, so the first frame paints without flicker.
MIT — see LICENSE.
