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/
Note: After editing the 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 valueVisual result
BoxFit.coverScales to completely fill the box, cropping any excess — no distortion, but edges may be cut off. The most common choice for photo thumbnails.
BoxFit.containScales to fit entirely within the box, preserving aspect ratio — the whole image is visible, but may leave empty space (letterboxing) on two sides.
BoxFit.fillStretches 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.

Next up: With images covered, let's fetch real data from the internet in HTTP Requests & APIs.