Widgets: Stateless vs Stateful
If you've read anything about Flutter before landing on this page, you've probably already heard
the phrase "everything is a widget." It sounds like a slogan, but it's actually
the single most important mental model you need to build Flutter apps. This lesson unpacks exactly
what that means, and then dives into the first real fork in the road every Flutter developer hits:
deciding whether a piece of UI should be a StatelessWidget or a StatefulWidget.
Get comfortable with this distinction now — it shapes almost every widget you write from here on.
1. Everything Is a Widget
In most UI frameworks, "layout" and "content" are different things — you might have a markup language for structure and a separate styling language for appearance. Flutter throws that split away entirely. In Flutter, a widget is just a description of a piece of UI, and that description can be anything: some visible text, an image, a button, an invisible box that adds padding, a row that arranges its children horizontally, or even an alignment rule. All of these are widgets, and they all work the same way — you configure them with a constructor and nest them inside one another.
| You might think of this as... | ...but in Flutter it's just a widget |
|---|---|
| Some text on screen | Text('Hello') |
| Spacing/whitespace | Padding, SizedBox |
| A layout rule | Row, Column, Center |
| Centering / alignment | Align, Center — yes, even alignment is a widget |
| The whole app itself | MaterialApp |
Because everything is a widget, and every widget can contain other widgets, your entire screen ends up being one big nested tree of widget objects. Building a UI in Flutter is really just a matter of composing small, simple widgets together into bigger ones — you'll see this "widget tree" idea explored properly in the next lesson.
2. StatelessWidget: No Memory of Its Own
A StatelessWidget is a widget that has no mutable state.
Once it's built, it never changes itself — its entire appearance is determined by the configuration
(the constructor parameters) it was given when it was created. Give a StatelessWidget
the same inputs twice, and its build() method will produce exactly the same output twice.
There's nothing hidden inside it that could make it behave differently on the second call.
This makes stateless widgets predictable, easy to test, and cheap for Flutter to work with. Here's
a complete, minimal example — a Greeting widget that just displays a name:
Example: A Stateless Greeting Widget
class Greeting extends StatelessWidget {
final String name;
const Greeting({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Text(
'Hello, $name! Welcome back.',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
);
}
}
// Using it elsewhere, e.g. inside another widget's build():
const Greeting(name: 'Sonu')
Notice there's no way for name to change from inside Greeting itself. The
only way the text on screen changes is if whoever created this widget builds a brand new
Greeting with a different name value. That's the defining trait of stateless:
all the "changeability" lives outside the widget, not inside it.
3. StatefulWidget: The Two-Class Pattern
Plenty of UI needs to change while it's on screen — a checkbox that toggles, a counter that increments, a text field the user is typing into. For this, Flutter gives you StatefulWidget. This is where beginners often get tripped up, because a stateful widget in Flutter is actually two classes working together, not one:
| Class | Role |
|---|---|
| StatefulWidget | Immutable configuration, exactly like a StatelessWidget. It holds no mutable data itself — its only job is to create its matching State object via createState(). |
| State<T> | The object that actually holds the mutable data (instance variables) and has the real build() method. Flutter keeps this object alive across rebuilds, so it can remember values over time. |
Why split it into two classes at all? Because the widget object itself gets thrown away and recreated constantly (every rebuild creates new widget instances — that's cheap, they're lightweight), but the data needs to survive those rebuilds. By keeping the mutable data in a separate, longer-lived State object, Flutter can discard and recreate the widget freely without losing your counter value, your text field contents, or whatever else you're tracking.
4. A Full Worked Example: A Simple Counter
Here is the classic Flutter "hello world" of interactivity — a button that increments a number
each time it's tapped. Focus on how the two classes divide responsibilities, and how
setState() is the trigger that tells Flutter "rebuild this, something changed":
Example: Counter Using StatefulWidget
class CounterCard extends StatefulWidget {
const CounterCard({super.key});
@override
State<CounterCard> createState() => _CounterCardState();
}
class _CounterCardState extends State<CounterCard> {
int _count = 0; // this lives in the State, not the widget
void _increment() {
setState(() {
_count++; // mutate, then tell Flutter to rebuild
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count', style: TextStyle(fontSize: 24)),
ElevatedButton(
onPressed: _increment,
child: Text('Add One'),
),
],
);
}
}
Walk through what happens on each tap: _increment() runs, and inside it,
setState() is called with a small callback that mutates _count.
That call to setState() is not optional decoration — it's the signal that tells
Flutter "the data behind this widget has changed, please call build() again."
If you removed the setState() wrapper and just wrote _count++; on its
own, the number would technically still increase in memory, but the screen would never update
to show it — you'll dig much deeper into exactly this trap in the upcoming
flutter-state-management-setstate.php lesson.
5. When to Choose Which
This decision comes up constantly, so it helps to have a simple rule of thumb rather than re-deriving it every time:
| Situation | Choice |
|---|---|
| Output depends only on constructor parameters, nothing changes after it's built | StatelessWidget |
| Purely presentational — headings, static icons, a row of fixed buttons | StatelessWidget |
| Something must update in response to user interaction (taps, typing, dragging) | StatefulWidget |
| Something must update in response to a timer, animation, or async data arriving | StatefulWidget |
6. The key Parameter
You've probably noticed both examples above accept a key parameter in their
constructor (super.key). Every widget in Flutter can accept a Key,
and while you can safely ignore it for most simple widgets, it becomes important in one specific
situation: helping Flutter tell widgets apart across rebuilds, especially when a list of similar
widgets changes (items get reordered, inserted, or removed).
Without a key, Flutter matches up old and new widgets in a list mostly by their position — which
can cause it to mix up state between items when the order changes (an animation applied to the
wrong item, a text field's content jumping to the wrong row, etc.). Passing a stable, unique
Key lets Flutter correctly track "this is the same logical item, just moved" instead
of "this is a different item that happens to be in the same slot." You'll see this matter for real
once you get to building dynamic lists in flutter-lists-listview.php — for now, just
know that accepting and forwarding key in every custom widget you write (as both
examples above do) is considered good practice from day one.
context parameter in every build() method actually is.