StatelessWidget vs StatefulWidget
Every Flutter UI is a tree of widgets. Learn the two fundamental building blocks — the immutable StatelessWidget and the mutable-state StatefulWidget — and when to reach for each.
Learning objectives
- Explain what makes a StatelessWidget immutable and when it rebuilds.
- Describe how a StatefulWidget stores mutable state in a persistent State object and refreshes the UI with setState().
- Decide which widget type to use for a given piece of UI, preferring stateless where possible.
- Explain why Flutter separates the widget (config) from its State (persistent data).
Simple explanation
Think of a movie poster versus a live scoreboard at a stadium. A poster is printed once and never changes — if the film changes, you print a brand-new poster. A scoreboard keeps the same frame bolted to the wall, but the numbers on it change all game long.
A StatelessWidget is the poster: it draws itself purely from the information handed to it and never changes on its own. A StatefulWidget is the scoreboard: the widget itself is a light frame, but it is paired with a separate object that *holds numbers that change over time* and can ask Flutter to redraw when they do.
Technical explanation
In Flutter, everything on screen is a widget, and widgets are cheap, immutable descriptions of what the UI should look like right now. There are two ways to build your own.
A StatelessWidget has no internal mutable state. All of its data comes in through its constructor (its final fields). It exposes a single build() method that returns a subtree. Because it holds nothing that can change, Flutter only rebuilds it when its inputs change — a parent passes new constructor arguments — or when an inherited widget it depends on (via BuildContext) changes. Given the same inputs, it always produces the same output.
A StatefulWidget is really two classes working together:
- The widget class itself — still immutable, still just
finalconfiguration — whose job is to create state. - A separate State object, returned from
createState(), that *persists across rebuilds* and is allowed to hold mutable fields.
You mutate that state by calling setState(), which updates the fields and marks the element dirty so Flutter schedules a rebuild. The build() method lives on the State, so it reads the current mutable values every time it runs. The State object is also where the lifecycle lives: initState() runs once when the state is inserted, and dispose() runs once when it is removed for good — the right places to start and stop timers, animations, and stream subscriptions.
Why it matters
Choosing correctly keeps your UI both correct and fast. A stateless widget is easier to reason about, cheaper to rebuild, and trivially testable because it is a pure function of its inputs. Reaching for StatefulWidget only when you genuinely need to remember something between frames — a toggle, a text field's contents, an animation's progress — keeps most of your tree simple. Misjudging this leads to two classic problems: UIs that silently fail to update, and UIs that leak resources or rebuild far more than they should.
How it works
When Flutter builds the UI it walks the widget tree and, for each widget, creates a matching Element that stays in place across frames:
- For a StatelessWidget, the element simply calls
build()again whenever the widget is replaced or a dependency changes. - For a StatefulWidget, the element also owns the long-lived State object. The widget can be thrown away and recreated on every frame, but the State survives — so the data it holds survives too.
- Calling
setState()doesn't repaint immediately; it flags the element as dirty. On the next frame Flutter callsbuild()again, diffs the new subtree against the old one, and updates only what changed.
That is why the widget can be immutable while the UI still feels dynamic: the *changing part* lives in the persistent State, not in the disposable widget.
Real-world analogy
Imagine a hotel room (the State) and the little printed card slipped into the door slot that says which guest is staying (the widget). Guests come and go — the card is reprinted constantly — but the room itself, with all the belongings inside, stays put. Swapping the card is cheap; it doesn't empty the room. In the same way, Flutter can rebuild the lightweight StatefulWidget as often as it likes without throwing away the State (and its data) sitting behind it.
Important terminology
- Immutable — once constructed, a widget's fields never change; to show something different, Flutter builds a new widget instance.
- State object — the persistent companion of a StatefulWidget that holds mutable data and survives rebuilds.
- setState() — the call that updates state fields and tells Flutter to schedule a rebuild; changing a field without it will not update the UI.
- Lifecycle (initState/dispose) — hooks on the State for one-time setup and teardown, e.g. starting and cancelling a subscription.
Key definitions
- StatelessWidget
- An immutable widget with no internal mutable state; it renders purely from its constructor inputs and inherited data.
- StatefulWidget
- A widget paired with a persistent State object that can hold mutable data and trigger rebuilds over time.
- State object
- The long-lived companion returned by createState() that persists across rebuilds and hosts the build() method and lifecycle.
- setState()
- A method on State that updates mutable fields and marks the widget dirty so Flutter schedules a rebuild.
Must memorize
Must memorize
- StatelessWidget = immutable; it rebuilds only when its inputs or an inherited dependency change.
- StatefulWidget keeps its data in a separate State object that survives rebuilds.
- Change state only inside setState(); mutating a field without it will not repaint the UI.
- initState() and dispose() live on the State — use them to set up and tear down timers, animations, and subscriptions.
Real-world example
A profile header vs. a like button
A profile header that just shows a name and avatar passed in from its parent is a perfect StatelessWidget — it never changes on its own. A like button that toggles filled/unfilled and updates a count when tapped needs a StatefulWidget, because it must remember its own toggled state and call setState() to redraw.
A countdown timer screen
A screen that counts down each second uses a StatefulWidget: it starts a periodic timer in initState(), calls setState() every tick to update the remaining seconds, and cancels the timer in dispose() so it does not leak or fire after the screen is gone.
Code example
import 'package:flutter/material.dart';
class GreetingCard extends StatelessWidget {
const GreetingCard({super.key, required this.name});
// Immutable configuration — comes from the constructor.
final String name;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Hello, $name!'),
),
);
}
}A StatelessWidget: pure output from its final inputs.
import 'package:flutter/material.dart';
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0; // Mutable state — lives on the State, survives rebuilds.
void _increment() {
// Update the field AND ask Flutter to rebuild.
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Count: $_count'),
ElevatedButton(
onPressed: _increment,
child: const Text('Increment'),
),
],
);
}
}A StatefulWidget: mutable count updated through setState().
import 'dart:async';
import 'package:flutter/material.dart';
class Ticker extends StatefulWidget {
const Ticker({super.key});
@override
State<Ticker> createState() => _TickerState();
}
class _TickerState extends State<Ticker> {
int _seconds = 0;
Timer? _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
setState(() => _seconds++);
});
}
@override
void dispose() {
_timer?.cancel(); // Prevent leaks: stop the timer when removed.
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('Elapsed: $_seconds s');
}
}A StatefulWidget using the lifecycle: start a timer in initState, clean up in dispose.
Common mistakes
Mistake
Reaching for StatefulWidget by default, even when the widget only displays data passed in from its parent.
Do this instead
Prefer StatelessWidget whenever a widget has no data that changes on its own. It is simpler, cheaper to rebuild, and easier to test.
Mistake
Mutating a State field directly (e.g. `count++;`) and expecting the UI to update.
Do this instead
Wrap the change in setState(() { count++; }) so Flutter marks the widget dirty and schedules a rebuild.
Interview questions
Quiz
Flashcards
Show answer · Space