Summary: in this tutorial, you’ll learn how about flutter stateful widgets.
Introduction to Flutter Stateful Widgets #
In flutter, a stateful widget is a widget that can change over time because it has mutable state. You create a stateful widget when its UI depends on data that can change while the app is running.
A StatefulWidget consists of:
- A Widget (immutable configuration)
- A State object (mutable, long‑lived)
Why two classes #
A StatefulWidget has two classes so Flutter can keep state alive while widgets are rebuilt and replaced. This allow flutter to separate “what it is” from “how it changes.”
The two classes in plain terms #
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
...
}Code language: PHP (php)1) StatefulWidget (the Widget) #
- Immutable
- Holds configuration (constructor parameters)
- Can be recreated at any time
Think of it as:
“What should this widget look like?”
2) State<T> (the State) #
- Mutable
- Holds data that changes
- Persists across rebuilds
Think of it as:
“What data does this widget remember?”
Why Flutter needs this separation #
Key rule in Flutter:
Widgets are cheap and disposable
Flutter may recreate widgets:
- When a parent rebuilds
- When the screen rotates
- When theme or locale changes
- During animations
If state lived inside the widget, it would be lost constantly.
So Flutter does this instead:
- Widget recreated
- State preserved
What actually happens at runtime #
- Flutter creates the
StatefulWidget - Flutter creates one
Stateobject - Flutter:
- Freely destroys & recreates the widget
- Keeps the same State as long as the widget stays in the tree
This is why:
build()runs ofteninitState()runs once
Visual mental model #
State (persistent)
├─ counter = 3
├─ controllers
└─ listeners
↑
│
Widget (temporary)
├─ title: "Hello"
├─ color: blue
└─ rebuilt many times
Code language: JavaScript (javascript)Why not combine them into one class? #
If Flutter used one mutable widget class:
- Rebuilds would overwrite state
- Diffing would be more complex
- Performance would suffer
- Predictability would suffer
By separating them:
- Widgets stay immutable
- State is preserved
- Rebuilds are fast
- Lifecycle is clear
Concrete example: why this matters #
setState(() {
counter++;
});
What happens:
Stateupdatescounter- Flutter creates a new widget
- The same State attaches to it
- UI updates correctly
If widget & state were one:
- ❌ counter could reset
- ❌ animations break
- ❌ inputs lose focus
Flutter Stateful widget example #
Here is a complete Flutter app that demonstrates a StatefulWidget using the classic counter example. You can copy to the Playground and run:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
/// Root of the application
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: CounterPage(),
);
}
}
/// A StatefulWidget because the counter value changes
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
/// The State object holds the mutable data
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
/// Called once when the state is created
@override
void initState() {
super.initState();
debugPrint('initState called');
}
/// Increases the counter and rebuilds the UI
void _incrementCounter() {
setState(() {
_counter++;
});
}
/// Decreases the counter and rebuilds the UI
void _decrementCounter() {
setState(() {
_counter--;
});
}
/// Builds the UI (called many times)
@override
Widget build(BuildContext context) {
debugPrint('build called');
return Scaffold(
appBar: AppBar(
title: const Text('StatefulWidget Counter'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Counter value:',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 8),
Text(
'$_counter',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: _decrementCounter,
child: const Text('-'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: _incrementCounter,
child: const Text('+'),
),
],
),
],
),
),
);
}
/// Called once when the widget is removed from the tree
@override
void dispose() {
debugPrint('dispose called');
super.dispose();
}
}
Code language: JavaScript (javascript)Why CounterPage is a StatefulWidget #
- The UI depends on
_counter _counterchanges over time- When it changes → UI updates
How the two classes work together #
class CounterPage extends StatefulWidget { ... }
class _CounterPageState extends State<CounterPage> { ... }
Code language: JavaScript (javascript)CounterPage→ immutable configuration_CounterPageState→ mutable state- Flutter recreates
CounterPage - Flutter preserves
_CounterPageState
Key lifecycle methods in this app #
initState() #
- Called once
- Good for setup
- You’ll see one log
initState called
build() #
- Called many times
- Called after
setState - You’ll see logs every button press
build called
dispose() #
- Called when widget is removed
- Cleanup place (controllers, timers, streams)
What setState() really does #
setState(() {
_counter++;
});
- Marks the state as dirty
- Schedules a rebuild
- Calls
build()again - Updates only what changed (efficient!)
🧪 Try this to understand it deeper #
- Press the
+button rapidly → Watchbuildspam in the console - Hot-reload the app → State is preserved
- Hot-restart the app →
_counterresets
Key takeaway #
A StatefulWidget has two classes so Flutter can rebuild widgets freely without losing mutable state.
When this design really shines #
- Lists (scroll position preserved)
- Forms (focus & text preserved)
- Animations
- Error + reload states
- Hot reload