Images & Assets
Nearly every app ships with some images bundled directly inside it — a logo, icons, illustrations —
and many also load images from the network at runtime, such as user avatars or product photos. Flutter
treats both cases as first-class citizens: bundled files are called assets, declared in
pubspec.yaml (introduced back in Packages & pubspec.yaml),
while remote images are fetched over HTTP and rendered once they arrive.
Declaring Assets in pubspec.yaml
Before you can use a bundled image, Flutter needs to know it exists — any file not explicitly listed
under the flutter: assets: section of pubspec.yaml will not be packaged into
your app at all. A common convention is to keep images in an assets/images/ folder at the
project root.
# pubspec.yaml
flutter:
uses-material-design: true
assets:
- assets/images/logo.png
- assets/images/banner.jpg
# or include an entire folder at once:
- assets/images/
assets: list you need to stop and restart the app (a hot reload isn't enough for asset manifest changes) so Flutter regenerates its internal asset bundle.Displaying a Bundled Asset Image
Once declared, load the image with Image.asset, passing the same path you listed in
pubspec.yaml.
Image.asset('assets/images/logo.png', width: 120, height: 120)
Displaying a Network Image With Loading and Error States
Image.network fetches an image from a URL. Because that fetch happens over the network, it
can be slow or can fail entirely — a bad connection, an expired link, a 404. Handling both cases
explicitly, with loadingBuilder and errorBuilder, is important for a
production-quality app; without them, a slow or failed image just leaves a blank gap where a spinner or
a friendly fallback should be.
Image.network(
'https://example.com/photos/profile.jpg',
width: 150,
height: 150,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return child; // fully loaded
}
return const Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48, color: Colors.grey);
},
)
BoxFit: Controlling How an Image Fills Its Space
When an image's own dimensions don't match the box you've given it, fit (a
BoxFit value) decides how it's scaled and cropped to fill that space.
| BoxFit value | Visual result |
|---|---|
BoxFit.cover | Scales to completely fill the box, cropping any excess — no distortion, but edges may be cut off. The most common choice for photo thumbnails. |
BoxFit.contain | Scales to fit entirely within the box, preserving aspect ratio — the whole image is visible, but may leave empty space (letterboxing) on two sides. |
BoxFit.fill | Stretches the image to exactly fill the box in both dimensions, ignoring aspect ratio — no cropping or empty space, but the image can look squashed or stretched. |
Image.network(
'https://example.com/photos/banner.jpg',
fit: BoxFit.cover,
)
Using Custom Fonts
Custom fonts follow the same declaration pattern as images — you place the font files in a folder
(commonly assets/fonts/) and register them under a fonts: section in
pubspec.yaml, giving each one a family name you'll reference from your theme or a
TextStyle.
# pubspec.yaml
flutter:
fonts:
- family: Poppins
fonts:
- asset: assets/fonts/Poppins-Regular.ttf
- asset: assets/fonts/Poppins-Bold.ttf
weight: 700
Text('Styled with a custom font', style: TextStyle(fontFamily: 'Poppins'))
Asset Variants for Different Pixel Densities
Phones and tablets ship with wildly different screen pixel densities. Rather than making you juggle
this manually, Flutter supports density-specific subfolders next to your base asset —
2.0x/ and 3.0x/ — each holding a higher-resolution version of the same file
name. At runtime, Flutter automatically picks whichever variant best matches the device's actual pixel
density, so your Image.asset('assets/images/logo.png') call never needs to change.
assets/images/logo.png // base resolution (1.0x)
assets/images/2.0x/logo.png // for higher-density screens
assets/images/3.0x/logo.png // for the highest-density screens
A Real-World Upgrade: cached_network_image
Plain Image.network re-downloads the image every single time the widget is rebuilt or the
app restarts, with no built-in disk caching. For real apps showing lots of network images — a feed, a
gallery — the community package cached_network_image is the common upgrade: it
transparently caches images on disk after the first download, so scrolling back to a previously seen
image is instant and doesn't re-hit the network. It's a drop-in-style replacement with its own
placeholder and error-widget parameters, conceptually very similar to the loadingBuilder/
errorBuilder pattern shown above.