Skip to content
FlutterLearn

Beginner · Lesson 8 · 10 min read

Images, Assets, Icons and Fonts

Bundle images and fonts properly, load network images with loading and error states, and understand resolution-aware assets.

Updated August 1, 2026

What you will learn

  • Declare and load bundled assets from pubspec.yaml
  • Handle network image loading and failure states
  • Use resolution-aware asset variants
  • Add custom fonts and use icons correctly

Assets are any file you ship inside the app: images, fonts, JSON fixtures, sounds. Flutter will not find them unless you declare them in pubspec.yaml — the single most common cause of the "Unable to load asset" error.

Declaring assets

pubspec.yaml
flutter:
  uses-material-design: true

  assets:
    # A single file
    - assets/images/logo.png
    # An entire directory (not recursive — subfolders need their own line)
    - assets/images/
    - assets/images/icons/
    - assets/data/countries.json

Displaying images

Dart
// From the bundle
Image.asset('assets/images/logo.png', width: 120)

// From the network, with both states handled
Image.network(
  article.imageUrl,
  height: 200,
  width: double.infinity,
  fit: BoxFit.cover,
  loadingBuilder: (context, child, progress) {
    if (progress == null) return child;   // finished
    return SizedBox(
      height: 200,
      child: Center(
        child: CircularProgressIndicator(
          value: progress.expectedTotalBytes != null
              ? progress.cumulativeBytesLoaded / progress.expectedTotalBytes!
              : null,
        ),
      ),
    );
  },
  errorBuilder: (context, error, stackTrace) => Container(
    height: 200,
    color: Theme.of(context).colorScheme.surfaceContainerHighest,
    child: const Center(child: Icon(Icons.broken_image_outlined)),
  ),
)

// From a file on disk
Image.file(File(path))

Always supply an errorBuilder for network images. Without one, a dead URL throws a red error box in debug and an invisible broken layout in release.

BoxFit: how the image fills its box

ValueBehaviour
coverFills the box, cropping overflow — the usual choice for photos
containFits entirely inside, leaving empty space — good for logos
fillStretches to the box, distorting aspect ratio — rarely what you want
fitWidth / fitHeightMatches one dimension, overflows the other
noneOriginal size, cropped to the box

Resolution-aware assets

Screens differ in pixel density. Flutter picks the right variant automatically if you follow the folder convention — you still reference only the base path.

Text
assets/images/logo.png        # 1x — the base path you reference
assets/images/2.0x/logo.png   # 2x — most modern phones
assets/images/3.0x/logo.png   # 3x — high-density displays
Dart
// Reference the base path only; Flutter resolves the variant
Image.asset('assets/images/logo.png', width: 120)

Icons

Dart
// Built-in Material icons — thousands available, no asset needed
const Icon(Icons.favorite, size: 28, color: Colors.pink)

// Outlined and rounded variants exist for most icons
const Icon(Icons.favorite_border)
const Icon(Icons.favorite_rounded)

// Icons that trigger something must be an IconButton, not a bare Icon —
// this gives you a 48x48 tap target, a ripple, and a semantic label
IconButton(
  icon: const Icon(Icons.share),
  tooltip: 'Share this lesson',
  onPressed: _share,
)

Setting uses-material-design: true in pubspec is what makes the Material icon font available. Leave it out and every icon renders as an empty box.

Custom fonts

pubspec.yaml
flutter:
  fonts:
    - family: Poppins
      fonts:
        - asset: assets/fonts/Poppins-Regular.ttf
        - asset: assets/fonts/Poppins-Medium.ttf
          weight: 500
        - asset: assets/fonts/Poppins-SemiBold.ttf
          weight: 600
        - asset: assets/fonts/Poppins-Italic.ttf
          style: italic
Dart
// Apply app-wide through the theme — do not set fontFamily per widget
MaterialApp(
  theme: ThemeData(
    fontFamily: 'Poppins',
    colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF027DFD)),
  ),
  home: const HomeScreen(),
)

// Override in one place when you genuinely need to
Text('Code sample', style: TextStyle(fontFamily: 'JetBrainsMono'))

Loading other bundled files

Dart
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;

Future<List<Country>> loadCountries() async {
  final raw = await rootBundle.loadString('assets/data/countries.json');
  final decoded = jsonDecode(raw) as List<dynamic>;
  return decoded
      .map((item) => Country.fromJson(item as Map<String, dynamic>))
      .toList();
}

This is handy for seed data, country lists, or sample content while you build a feature before the API exists.

Key takeaways

  • Every bundled asset must be declared under flutter: assets: in pubspec, and directories are not recursive.
  • Always give network images a loadingBuilder and an errorBuilder.
  • Use 2.0x/3.0x folders for density variants and reference only the base path.
  • Declare each font weight you actually use, and apply the family through the theme.

Practice

A polished profile card

Build a profile card with a bundled placeholder avatar, a network avatar that falls back to the placeholder on error, a custom font applied through the theme, and an IconButton row for share and edit. Add 2x and 3x variants of the placeholder and confirm the right one loads.

Show hints
  • errorBuilder is where the fallback to Image.asset goes.
  • Run a full restart after editing pubspec — hot reload does not pick up new assets.