StatelessWidget in Depth
What Is a StatelessWidget?
In Flutter, everything you see on screen is a widget. A StatelessWidget is a widget that does not change once it is built. It has no mutable state — meaning its appearance and behavior are determined entirely by the configuration passed to it through its constructor. Once Flutter calls the build() method, the widget renders and remains the same until the parent rebuilds it with new parameters.
Think of a StatelessWidget as a pure function of its inputs. Given the same constructor arguments, it always produces the same UI output. This makes it predictable, easy to test, and highly efficient.
The build() Method
Every StatelessWidget must override the build() method. This method receives a BuildContext and returns a widget tree that describes what should appear on screen.
Basic StatelessWidget
import 'package:flutter/material.dart';
class WelcomeMessage extends StatelessWidget {
const WelcomeMessage({super.key});
@override
Widget build(BuildContext context) {
return const Text(
'Welcome to Flutter!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
);
}
}
The BuildContext parameter gives the widget access to its position in the widget tree. You can use it to look up inherited widgets, themes, media queries, and more. We will explore this in later lessons.
Immutability — Why It Matters
All fields in a StatelessWidget must be final. This is enforced by the Dart analyzer. Because the widget is immutable, Flutter can safely cache and reuse it, which improves performance.
Immutable Fields
class UserBadge extends StatelessWidget {
final String username;
final int level;
const UserBadge({
super.key,
required this.username,
required this.level,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.person, size: 20),
const SizedBox(width: 8),
Text('\$username (Lv. \$level)'),
],
);
}
}
When to Use StatelessWidget
Use a StatelessWidget when your widget:
- Displays static content that depends only on constructor parameters
- Does not need to track user interactions internally
- Does not need lifecycle callbacks like
initStateordispose - Does not call
setState
Common real-world examples include: labels, icons, static cards, display-only list items, dividers, decorative elements, and layout wrappers.
Constructor Parameters & Named Parameters
Flutter widgets use Dart’s named parameters extensively. The required keyword ensures callers provide essential values, while optional parameters can have defaults.
Required and Optional Parameters
class ProductCard extends StatelessWidget {
final String name;
final double price;
final String? imageUrl;
final bool showBorder;
const ProductCard({
super.key,
required this.name,
required this.price,
this.imageUrl,
this.showBorder = true,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: showBorder
? Border.all(color: Colors.grey.shade300)
: null,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (imageUrl != null)
Image.network(imageUrl!, height: 120, fit: BoxFit.cover),
const SizedBox(height: 8),
Text(name, style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600,
)),
Text('\\$\${price.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 16, color: Colors.green.shade700,
),
),
],
),
);
}
}
const when possible. This allows Flutter to optimize widget rebuilds by reusing identical widget instances. The super.key pattern (introduced in Dart 2.17) is the modern way to forward the key parameter.const Constructors & Performance
When all fields in a widget are compile-time constants, you can declare the constructor as const. This tells Dart to create the widget at compile time rather than runtime, which has two key benefits:
- Memory efficiency: Identical const widgets share the same instance in memory
- Rebuild optimization: Flutter skips rebuilding const widgets because it knows they haven’t changed
Using const Constructors
class AppLogo extends StatelessWidget {
const AppLogo({super.key});
@override
Widget build(BuildContext context) {
return const Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.flutter_dash, size: 64, color: Colors.blue),
SizedBox(height: 8),
Text(
'My Flutter App',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
);
}
}
// Usage - both are identical instances in memory
const logo1 = AppLogo();
const logo2 = AppLogo(); // Same instance as logo1
Widget Composition
Flutter encourages building complex UIs by composing small, focused widgets together rather than creating one large widget. This is called widget composition, and it is one of Flutter’s most powerful patterns.
Composing Widgets Together
class InfoRow extends StatelessWidget {
final IconData icon;
final String label;
final String value;
const InfoRow({
super.key,
required this.icon,
required this.label,
required this.value,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Icon(icon, size: 20, color: Colors.blue),
const SizedBox(width: 12),
Text(label, style: const TextStyle(
fontWeight: FontWeight.w500, color: Colors.grey,
)),
const Spacer(),
Text(value, style: const TextStyle(
fontWeight: FontWeight.w600,
)),
],
),
);
}
}
class UserProfile extends StatelessWidget {
final String name;
final String email;
final String phone;
final String location;
const UserProfile({
super.key,
required this.name,
required this.email,
required this.phone,
required this.location,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: const TextStyle(
fontSize: 22, fontWeight: FontWeight.bold,
)),
const Divider(height: 24),
InfoRow(icon: Icons.email, label: 'Email', value: email),
InfoRow(icon: Icons.phone, label: 'Phone', value: phone),
InfoRow(icon: Icons.location_on, label: 'Location', value: location),
],
),
),
);
}
}
build() method is longer than ~50 lines, consider extracting parts into separate widgets. This improves readability, reusability, and performance (Flutter can skip rebuilding unchanged sub-widgets).Practical Example: Greeting Card Widget
Let’s build a complete, reusable greeting card widget that demonstrates all the concepts we have covered.
Greeting Card Widget
class GreetingCard extends StatelessWidget {
final String recipientName;
final String message;
final Color backgroundColor;
final bool showIcon;
const GreetingCard({
super.key,
required this.recipientName,
required this.message,
this.backgroundColor = Colors.white,
this.showIcon = true,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showIcon)
const Icon(Icons.celebration, size: 40, color: Colors.amber),
if (showIcon)
const SizedBox(height: 12),
Text(
'Hello, \$recipientName!',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
message,
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade700,
height: 1.5,
),
),
],
),
);
}
}
// Usage
GreetingCard(
recipientName: 'Ahmed',
message: 'Wishing you a wonderful day!',
backgroundColor: Colors.blue.shade50,
)
Practical Example: Reusable Label Widget
Labels and tags are common UI elements. Here is a flexible, reusable label widget:
Reusable Label Widget
class StatusLabel extends StatelessWidget {
final String text;
final Color color;
final IconData? icon;
final double fontSize;
const StatusLabel({
super.key,
required this.text,
this.color = Colors.blue,
this.icon,
this.fontSize = 12,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: fontSize + 2, color: color),
const SizedBox(width: 4),
],
Text(
text,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
);
}
}
// Usage examples
const StatusLabel(text: 'Active', color: Colors.green)
const StatusLabel(text: 'Pending', color: Colors.orange, icon: Icons.schedule)
const StatusLabel(text: 'Error', color: Colors.red, icon: Icons.error_outline)
Practical Example: Info Display Widget
A common pattern in apps is displaying key-value information in a structured layout:
Info Display Widget
class InfoDisplay extends StatelessWidget {
final String title;
final List<MapEntry<String, String>> items;
final IconData? headerIcon;
const InfoDisplay({
super.key,
required this.title,
required this.items,
this.headerIcon,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (headerIcon != null) ...[
Icon(headerIcon, color: Colors.blue),
const SizedBox(width: 8),
],
Text(title, style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold,
)),
],
),
const Divider(height: 24),
...items.map((entry) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(entry.key, style: TextStyle(
color: Colors.grey.shade600,
)),
Text(entry.value, style: const TextStyle(
fontWeight: FontWeight.w500,
)),
],
),
)),
],
),
);
}
}
// Usage
InfoDisplay(
title: 'Order Summary',
headerIcon: Icons.receipt_long,
items: [
const MapEntry('Subtotal', '\\$45.00'),
const MapEntry('Tax', '\\$3.60'),
const MapEntry('Total', '\\$48.60'),
],
)
Summary
In this lesson, you learned:
- StatelessWidget is an immutable widget whose output depends only on its constructor parameters
- The build() method returns a widget tree and receives a
BuildContext - All fields must be final to enforce immutability
- Use const constructors for better performance and memory efficiency
- Widget composition is the preferred way to build complex UIs from simple parts
- Use required and optional named parameters for flexible widget APIs
StatefulWidget and its lifecycle methods, which allow widgets to manage internal state and respond to changes over time.