In Flutter, how do an Image's loadingBuilder, frameBuilder and errorBuilder differ, and how would you use them for a listing photo?
answer
- loadingProgress null means done
- expectedTotalBytes may be null
- frame null until the first frame
- wasSynchronouslyLoaded skips the fade
- errorBuilder or FlutterError.onError
basics
~20 sloadingBuilder reports download progress and is called with null once loading completes; frameBuilder learns when the first frame is ready, for placeholders and fade-ins; errorBuilder replaces the image when loading fails. When both are set, loadingBuilder's child is frameBuilder's result.
solid answer
~40 s`loadingBuilder(context, child, loadingProgress)` receives an `ImageChunkEvent` while bytes arrive, with `cumulativeBytesLoaded` and a nullable `expectedTotalBytes`, and `null` once the image is complete; the `Image` rebuilds on every frame while it loads, so it is for real progress indicators. `frameBuilder(context, child, frame, wasSynchronouslyLoaded)` gets `frame == null` until the first frame is available and a flag telling whether it came synchronously from the cache, which is how a fade-in skips cached images. `errorBuilder(context, error, stackTrace)` returns a replacement widget; without one, the error goes to `FlutterError.onError`. For a listing photo I use `frameBuilder` for a fade-in, `loadingBuilder` only on the full-screen viewer, and `errorBuilder` for a "photo unavailable" tile.
code
dart · 20 linesimport 'package:flutter/material.dart';
Widget listingThumbnail(String url) {
return Image.network(
url,
fit: BoxFit.cover,
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) return child;
return AnimatedOpacity(
opacity: frame == null ? 0 : 1,
duration: const Duration(milliseconds: 250),
child: child,
);
},
errorBuilder: (context, error, stackTrace) => const ColoredBox(
color: Colors.black12,
child: Center(child: Text('Photo unavailable')),
),
);
}go deeper
Know which builder shows progress, which shows a fallback on error, and that loadingProgress is null once the image has loaded.
Explain frame and wasSynchronouslyLoaded, nullable expectedTotalBytes, how the builders chain, and where errors go without an errorBuilder.
Pick builders per screen for cost and feel: no per-frame rebuilds in grids, no fade on cached images, and retries that really refetch.
Define shared image widgets so loading and failure states look and behave the same across every photo surface in the app.
## Three hooks, three moments An `Image` goes through three observable moments: bytes arriving, the first frame becoming available, and possibly failure. Each has its own builder: | Builder | Signature | Called with | |---|---|---| | `loadingBuilder` | `(context, child, ImageChunkEvent? loadingProgress)` | progress events while loading, then `null` when done | | `frameBuilder` | `(context, child, int? frame, bool wasSynchronouslyLoaded)` | `frame == null` before the first frame, then 0, 1, 2 for animated images | | `errorBuilder` | `(context, Object error, StackTrace? stackTrace)` | the failure, once | For the first two, `child` is the widget that paints the image, so the builder wraps it, replaces it, or returns it unchanged; `errorBuilder` has no `child` and returns the replacement outright. ## loadingBuilder: real progress `ImageChunkEvent` carries: - `cumulativeBytesLoaded`, the bytes received so far; - `expectedTotalBytes`, **nullable**, because the server may not send a content length. A determinate progress bar uses `cumulativeBytesLoaded / expectedTotalBytes!` only when the total is known and falls back to an indeterminate indicator otherwise. When loading finishes, the builder is called with **`null`**, and it should then return `child`. Performance matters here: with a `loadingBuilder`, the framework documents that the `Image` is likely to rebuild on **every frame** until the image has loaded. For a grid of forty listing thumbnails, that is forty widgets rebuilding each frame during loading. Use it where progress is worth showing, such as a full-screen photo viewer on a slow connection. ## frameBuilder: placeholders and fade-ins `frameBuilder` is cheaper: the `Image` subscribes to download-progress events only when a `loadingBuilder` is set, so a `frameBuilder` alone does not rebuild for every chunk of bytes. Two arguments do the work: 1. `frame` is `null` until the first image frame is available, so a builder can show a neutral placeholder until then. 2. `wasSynchronouslyLoaded` is `true` when the image was already in the `ImageCache` and appeared in the same frame. Animating a fade-in in that case makes cached photos flicker, so the usual pattern returns `child` directly when it is true. ## errorBuilder: failure without a red box When the network fails, the server returns 404 or the file cannot be decoded, `errorBuilder` returns a replacement, for example a tile reading "Photo unavailable" with a retry button. Without it, the exception is reported to **`FlutterError.onError`**, which in debug builds prints a large error. `NetworkImage` evicts a failed key from the cache, so rebuilding the image after a retry really goes back to the network. ## Retrying after an error A "Tap to retry" tile inside `errorBuilder` needs the image to resolve again. Rebuilding the same `Image` with an equal provider does **not** re-resolve, because the widget only resolves when its provider changes or its dependencies change. Two ways work: 1. Remount the image: keep a retry counter in `State` and give the `Image` a `ValueKey(attempt)`, so a fresh `State` resolves from scratch. 2. Change the provider, for example by adding a harmless query parameter, at the cost of a new cache key. Because `NetworkImage` evicted the failed key, the remounted image goes back to the network instead of replaying the cached failure. ## Chaining When both `frameBuilder` and `loadingBuilder` are set, they are chained: the `child` passed to `loadingBuilder` is the **result of `frameBuilder`**. A fade-in from `frameBuilder` can therefore sit inside a progress overlay from `loadingBuilder`. ## Alternatives - **`FadeInImage`** (for example `FadeInImage.assetNetwork`) cross-fades from a placeholder image to the target and covers the most common case in one widget. - A plain `Stack` with a placeholder behind the image works when the image's own background can stay transparent while loading. ## Choosing for a real-estate app 1. **Grid thumbnails**: `frameBuilder` fade-in plus `errorBuilder`; no `loadingBuilder`. 2. **Full-screen viewer**: `loadingBuilder` with determinate progress when `expectedTotalBytes` is known, plus `errorBuilder` with retry. 3. **Agent avatars from the bundle**: none; assets load almost instantly and rarely fail.
- Why does a fade-in written with frameBuilder flicker for cached photos unless it checks wasSynchronouslyLoaded?A cached image is available in the very first frame, so `wasSynchronouslyLoaded` is true. If the builder still starts at opacity 0 and animates to 1, a photo the user already saw fades in again every time the tile rebuilds or scrolls back into view. Returning `child` directly in that case shows cached photos instantly.
- Why prefer frameBuilder over loadingBuilder for a grid of thumbnails?With a `loadingBuilder`, each `Image` listens to progress events and is likely to rebuild on every frame until it has loaded, which multiplies across a grid. Without one, the `Image` ignores progress chunks, so a `frameBuilder` can show a placeholder and a fade-in without those rebuilds. Keep `loadingBuilder` for places where progress really matters.
saying these in an interview costs you the question
- loadingBuilder is called only once, when loading starts.
- expectedTotalBytes is always known for network images.
- Without errorBuilder, a failed image silently shows nothing and logs nothing.
- Adding only a frameBuilder makes the Image rebuild for every chunk of downloaded bytes.
- When both builders are set, only loadingBuilder is used.