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
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.jsonDisplaying images
// 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
| Value | Behaviour |
|---|---|
cover | Fills the box, cropping overflow — the usual choice for photos |
contain | Fits entirely inside, leaving empty space — good for logos |
fill | Stretches to the box, distorting aspect ratio — rarely what you want |
fitWidth / fitHeight | Matches one dimension, overflows the other |
none | Original 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.
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// Reference the base path only; Flutter resolves the variant
Image.asset('assets/images/logo.png', width: 120)Icons
// 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
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// 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
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
loadingBuilderand anerrorBuilder. - Use
2.0x/3.0xfolders 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
errorBuilderis where the fallback toImage.assetgoes.- Run a full restart after editing pubspec — hot reload does not pick up new assets.