Flutter Stateless vs Stateful Widgets
Every Flutter widget is either stateless or stateful. This distinction determines whether a widget can change its appearance while the app is running. Choosing the right type is a core Flutter skill.
What Is State
State is data that can change over time and affects what the user sees. A counter value, a checkbox tick, a selected color — these are all examples of state.
No state change needed: ┌─────────────────────────────────────┐ │ "Welcome to our app" │ → StatelessWidget │ (always shows the same text) │ └─────────────────────────────────────┘ State changes needed: ┌─────────────────────────────────────┐ │ Counter: [0] [+] [-] │ → StatefulWidget │ (number changes when buttons tap) │ └─────────────────────────────────────┘
StatelessWidget
A StatelessWidget builds its UI once. It cannot update itself — if the parent widget rebuilds, the stateless widget rebuilds with it, but it holds no internal data of its own.
class ProfileCard extends StatelessWidget {
final String name;
final String role;
const ProfileCard({required this.name, required this.role});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Text(name, style: TextStyle(fontSize: 20)),
Text(role, style: TextStyle(color: Colors.grey)),
],
),
);
}
}
Lifecycle: ────────────────────────────────────────── Create → build() → Draw → (parent changes?) → Rebuild
StatefulWidget
A StatefulWidget splits into two parts: the widget class itself (immutable) and a State class (mutable). The State class holds the data that can change.
class CounterWidget extends StatefulWidget {
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0; // This is the state
void _increment() {
setState(() {
_count++; // Tell Flutter to rebuild
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count', style: TextStyle(fontSize: 32)),
ElevatedButton(
onPressed: _increment,
child: Text('Add'),
),
],
);
}
}
The Role of setState()
setState() is the key method. It does two things: updates the variable value and tells Flutter to re-run the build() method so the screen refreshes.
Without setState():
_count++; → Variable updates in memory
→ Screen stays the same (stale)
With setState():
setState(() {
_count++; → Variable updates
}); → Flutter calls build() again
→ Screen shows new value ✓
StatefulWidget Lifecycle
The State class goes through several lifecycle stages you need to know about.
┌──────────────────────────────────────────────────┐ │ LIFECYCLE FLOW │ │ │ │ createState() │ │ ↓ │ │ initState() ← runs once on creation │ │ ↓ │ │ build() ← runs every time state changes │ │ ↓ │ │ setState() ← triggers rebuild │ │ ↓ │ │ build() ← runs again │ │ ↓ │ │ dispose() ← runs when widget is removed │ └──────────────────────────────────────────────────┘
initState() — Run Code on Start
@override
void initState() {
super.initState();
// Runs once when widget is first created
// Good for: fetching data, starting timers
fetchUserData();
}
dispose() — Clean Up
@override
void dispose() {
// Runs when widget is removed from screen
// Good for: cancelling timers, closing streams
_timer.cancel();
super.dispose();
}
Choosing Between Stateless and Stateful
| Use StatelessWidget when… | Use StatefulWidget when… |
|---|---|
| Content is fixed (headings, labels, icons) | Content changes after user interaction |
| Data comes from parent only | Widget manages its own data |
| No buttons or input fields that trigger changes | Involves counters, toggles, form inputs |
| Performance-critical, frequently rebuilt areas | Loading spinners that show/hide |
Practical Example — Toggle Button
class FavoriteButton extends StatefulWidget {
@override
State<FavoriteButton> createState() => _FavoriteButtonState();
}
class _FavoriteButtonState extends State<FavoriteButton> {
bool _isFavorite = false;
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
_isFavorite ? Icons.favorite : Icons.favorite_border,
color: _isFavorite ? Colors.red : Colors.grey,
),
onPressed: () {
setState(() {
_isFavorite = !_isFavorite;
});
},
);
}
}
Tapped OFF: ♡ (grey border heart) Tapped ON: ♥ (red filled heart) Toggle back: ♡ (grey border heart)
Performance Tip
Keep StatefulWidgets as small as possible. If only a small part of your screen changes, extract that part into its own StatefulWidget. This way Flutter only rebuilds that small piece instead of the whole screen.
