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 screenText('Hello')
Spacing/whitespacePadding, SizedBox
A layout ruleRow, Column, Center
Centering / alignmentAlign, Center — yes, even alignment is a widget
The whole app itselfMaterialApp

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:

ClassRole
StatefulWidgetImmutable 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:

SituationChoice
Output depends only on constructor parameters, nothing changes after it's builtStatelessWidget
Purely presentational — headings, static icons, a row of fixed buttonsStatelessWidget
Something must update in response to user interaction (taps, typing, dragging)StatefulWidget
Something must update in response to a timer, animation, or async data arrivingStatefulWidget
Note: A common (and good) habit is to default to StatelessWidget and only reach for StatefulWidget when you actually need mutable data. Stateless widgets are simpler to reason about, and it's very easy to convert one into the other later if requirements change — most IDEs even have a "Convert to StatefulWidget" refactor built in.

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.

Next up: Now that you know the two fundamental widget types, head to The Widget Tree & BuildContext to see how widgets nest together into a full screen, and what that mysterious context parameter in every build() method actually is.