Image & Icon Widgets
Image Widget Overview
Flutter provides several constructors for the Image widget, each designed for loading images from different sources. Understanding which constructor to use and how to handle loading states and errors is essential for building polished apps.
Image.asset — Bundled Images
Use Image.asset to display images bundled with your app. These are images stored in your project’s assets folder and declared in pubspec.yaml.
Loading Asset Images
// pubspec.yaml:
// flutter:
// assets:
// - assets/images/
// Basic asset image
Image.asset(
'assets/images/logo.png',
width: 200,
height: 200,
)
// With fit and alignment
Image.asset(
'assets/images/banner.jpg',
width: double.infinity,
height: 200,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
)
// Resolution-aware assets
// Place images in folders:
// assets/images/logo.png (1x)
// assets/images/2.0x/logo.png (2x)
// assets/images/3.0x/logo.png (3x)
// Flutter automatically picks the right resolution
pubspec.yaml. If you forget, Flutter will throw an Unable to load asset error at runtime. Use directory-level declarations like assets/images/ to include all files in a folder.Image.network — Remote Images
Image.network loads images from a URL. It handles the HTTP request and decoding automatically. For production apps, consider using cached network images for better performance.
Loading Network Images
// Basic network image
Image.network(
'https://picsum.photos/400/300',
width: 400,
height: 300,
fit: BoxFit.cover,
)
// With loading indicator and error handling
Image.network(
'https://example.com/photo.jpg',
width: double.infinity,
height: 250,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
width: double.infinity,
height: 250,
color: Colors.grey.shade200,
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.broken_image, size: 48, color: Colors.grey),
SizedBox(height: 8),
Text('Failed to load image',
style: TextStyle(color: Colors.grey)),
],
),
);
},
)
Image.file & Image.memory
Image.file loads images from the device file system (useful after taking a photo or picking a file). Image.memory loads from raw bytes in memory.
File and Memory Images
import 'dart:io';
import 'dart:typed_data';
// From a file on the device
Image.file(
File('/path/to/photo.jpg'),
width: 300,
height: 300,
fit: BoxFit.cover,
)
// From bytes in memory (e.g., decoded from base64)
Image.memory(
Uint8List.fromList(imageBytes),
width: 200,
height: 200,
fit: BoxFit.contain,
)
// Practical: Display image from camera
class CameraPreview extends StatelessWidget {
final File? imageFile;
const CameraPreview({super.key, this.imageFile});
@override
Widget build(BuildContext context) {
if (imageFile == null) {
return Container(
width: 300,
height: 300,
color: Colors.grey.shade100,
child: const Icon(Icons.camera_alt, size: 64, color: Colors.grey),
);
}
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
imageFile!,
width: 300,
height: 300,
fit: BoxFit.cover,
),
);
}
}
BoxFit — Image Sizing
The fit property controls how the image is inscribed into the allocated space. Understanding each BoxFit value is crucial for proper image display.
BoxFit Examples
// BoxFit.cover - Fills the box, may crop
// Best for: backgrounds, cards, avatars
Image.network(url, fit: BoxFit.cover)
// BoxFit.contain - Fits entire image, may have letterboxing
// Best for: product images, logos
Image.network(url, fit: BoxFit.contain)
// BoxFit.fill - Stretches to fill exactly, may distort
// Best for: backgrounds where distortion is acceptable
Image.network(url, fit: BoxFit.fill)
// BoxFit.fitWidth - Fits width, may overflow height
Image.network(url, fit: BoxFit.fitWidth)
// BoxFit.fitHeight - Fits height, may overflow width
Image.network(url, fit: BoxFit.fitHeight)
// BoxFit.none - No scaling, centered
// Best for: pixel-perfect images at native resolution
Image.network(url, fit: BoxFit.none)
// BoxFit.scaleDown - Like contain but never scales up
// Best for: small images that should not be enlarged
Image.network(url, fit: BoxFit.scaleDown)
// Visual comparison widget
class BoxFitShowcase extends StatelessWidget {
const BoxFitShowcase({super.key});
@override
Widget build(BuildContext context) {
const fits = [
BoxFit.cover, BoxFit.contain, BoxFit.fill,
BoxFit.fitWidth, BoxFit.fitHeight, BoxFit.scaleDown,
];
return Wrap(
spacing: 12,
runSpacing: 12,
children: fits.map((fit) => Column(
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
),
child: Image.network(
'https://picsum.photos/200/300',
fit: fit,
),
),
const SizedBox(height: 4),
Text(fit.toString().split('.').last,
style: const TextStyle(fontSize: 12)),
],
)).toList(),
);
}
}
CachedNetworkImage
For production apps, use the cached_network_image package. It caches downloaded images on disk, reducing bandwidth usage and improving load times for repeated views.
Using CachedNetworkImage
// pubspec.yaml:
// dependencies:
// cached_network_image: ^3.3.1
import 'package:cached_network_image/cached_network_image.dart';
// Basic cached image
CachedNetworkImage(
imageUrl: 'https://example.com/photo.jpg',
width: double.infinity,
height: 200,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
)
// Advanced with fade animation
CachedNetworkImage(
imageUrl: 'https://example.com/photo.jpg',
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
),
placeholder: (context, url) => Container(
color: Colors.grey.shade200,
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
),
errorWidget: (context, url, error) => Container(
color: Colors.grey.shade100,
child: const Icon(Icons.broken_image, color: Colors.grey),
),
fadeInDuration: const Duration(milliseconds: 300),
fadeOutDuration: const Duration(milliseconds: 300),
)
CachedNetworkImage uses flutter_cache_manager under the hood and stores images on device storage. This is especially valuable for list views and grids where images are scrolled in and out frequently.Icon Widget
The Icon widget displays a glyph from a font-based icon set. Flutter comes with the Material Icons set built in. Icons are vector-based, so they scale cleanly at any size.
Using Icons
// Basic icons
const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.home, size: 32),
Icon(Icons.favorite, size: 32, color: Colors.red),
Icon(Icons.settings, size: 32, color: Colors.grey),
Icon(Icons.search, size: 32, color: Colors.blue),
],
)
// Icon sizes
const Column(
children: [
Icon(Icons.star, size: 16), // Small
Icon(Icons.star, size: 24), // Default
Icon(Icons.star, size: 32), // Medium
Icon(Icons.star, size: 48), // Large
Icon(Icons.star, size: 64), // Extra large
],
)
// Outlined vs filled variants
const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.bookmark), // Filled (default)
Icon(Icons.bookmark_outline), // Outlined
Icon(Icons.bookmark_border), // Border variant
Icon(Icons.bookmark_add), // Add variant
],
)
IconButton Widget
IconButton wraps an icon in a tappable area with an ink splash effect. It is the standard way to create interactive icons in Material Design apps.
IconButton Examples
class IconButtonDemo extends StatefulWidget {
const IconButtonDemo({super.key});
@override
State<IconButtonDemo> createState() => _IconButtonDemoState();
}
class _IconButtonDemoState extends State<IconButtonDemo> {
bool _isFavorite = false;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Toggle favorite
IconButton(
icon: Icon(
_isFavorite ? Icons.favorite : Icons.favorite_border,
color: _isFavorite ? Colors.red : Colors.grey,
),
iconSize: 32,
onPressed: () => setState(() => _isFavorite = !_isFavorite),
tooltip: 'Toggle Favorite',
),
// Filled icon button (Material 3)
IconButton.filled(
icon: const Icon(Icons.add),
onPressed: () {},
),
// Outlined icon button
IconButton.outlined(
icon: const Icon(Icons.share),
onPressed: () {},
),
// Disabled icon button
const IconButton(
icon: Icon(Icons.delete),
onPressed: null, // null makes it disabled
),
],
);
}
}
ImageIcon & Custom Icons
ImageIcon creates an icon from an image asset, which is useful when the Material Icons set does not have what you need. For custom icon fonts, use packages like flutter_launcher_icons.
Custom Icon Approaches
// ImageIcon from asset
const ImageIcon(
AssetImage('assets/icons/custom_icon.png'),
size: 32,
color: Colors.blue,
)
// Using a custom icon font
// After generating your font with FlutterIcon or similar:
class CustomIcons {
static const IconData myCustomIcon = IconData(
0xe900,
fontFamily: 'CustomIcons',
fontPackage: null,
);
}
// Usage
const Icon(CustomIcons.myCustomIcon, size: 24)
// Cupertino icons (iOS style)
import 'package:flutter/cupertino.dart';
const Row(
children: [
Icon(CupertinoIcons.heart, size: 28),
SizedBox(width: 16),
Icon(CupertinoIcons.gear, size: 28),
SizedBox(width: 16),
Icon(CupertinoIcons.search, size: 28),
],
)
Practical Example: Photo Gallery Grid
Let’s build a responsive photo gallery that demonstrates image loading with placeholders and error handling:
Photo Gallery Widget
class PhotoGallery extends StatelessWidget {
final List<String> imageUrls;
final int crossAxisCount;
const PhotoGallery({
super.key,
required this.imageUrls,
this.crossAxisCount = 3,
});
@override
Widget build(BuildContext context) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: imageUrls.length,
itemBuilder: (context, index) => GalleryTile(
imageUrl: imageUrls[index],
index: index,
),
);
}
}
class GalleryTile extends StatelessWidget {
final String imageUrl;
final int index;
const GalleryTile({
super.key,
required this.imageUrl,
required this.index,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey.shade100,
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (context, error, stackTrace) => Container(
color: Colors.grey.shade200,
child: const Icon(Icons.broken_image, color: Colors.grey),
),
),
);
}
}
// Usage
PhotoGallery(
imageUrls: List.generate(
9,
(i) => 'https://picsum.photos/seed/\${i + 1}/400/400',
),
)
Practical Example: Avatar with Fallback
A common pattern in apps is showing a user avatar with graceful fallbacks when the image is unavailable:
Avatar Widget with Fallback
class UserAvatar extends StatelessWidget {
final String? imageUrl;
final String name;
final double size;
final Color? backgroundColor;
const UserAvatar({
super.key,
this.imageUrl,
required this.name,
this.size = 48,
this.backgroundColor,
});
String get _initials {
final parts = name.trim().split(RegExp(r'\s+'));
if (parts.length >= 2) {
return '\${parts.first[0]}\${parts.last[0]}'.toUpperCase();
}
return name.isNotEmpty ? name[0].toUpperCase() : '?';
}
Color get _fallbackColor {
final hash = name.hashCode;
final colors = [
Colors.blue, Colors.red, Colors.green, Colors.purple,
Colors.orange, Colors.teal, Colors.pink, Colors.indigo,
];
return backgroundColor ?? colors[hash.abs() % colors.length];
}
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _fallbackColor,
),
clipBehavior: Clip.antiAlias,
child: imageUrl != null && imageUrl!.isNotEmpty
? Image.network(
imageUrl!,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildInitials(),
)
: _buildInitials(),
);
}
Widget _buildInitials() {
return Center(
child: Text(
_initials,
style: TextStyle(
color: Colors.white,
fontSize: size * 0.38,
fontWeight: FontWeight.w600,
),
),
);
}
}
// Usage examples
const Column(
children: [
UserAvatar(name: 'John Doe', imageUrl: 'https://example.com/photo.jpg'),
SizedBox(height: 8),
UserAvatar(name: 'Ahmed Ali', size: 64), // Shows initials "AA"
SizedBox(height: 8),
UserAvatar(name: 'Sara', size: 40), // Shows initial "S"
],
)
Summary
In this lesson, you learned:
- Image.asset loads bundled images; Image.network loads remote images with HTTP
- Image.file loads from device storage; Image.memory loads from raw bytes
- BoxFit controls how images scale: cover, contain, fill, fitWidth, fitHeight, scaleDown, none
- loadingBuilder and errorBuilder provide loading indicators and error fallbacks
- CachedNetworkImage caches images on disk for better performance
- Icon displays Material icons; IconButton makes them interactive
- ImageIcon and custom icon fonts extend beyond the built-in icon set