← All courses

Stateless vs Stateful Widgets

🗓 May 31, 2026 ⏱ 2 min read

Two kinds of widgets you write

When you create your own widgets, they’re one of two types, and choosing correctly is fundamental:

  • StatelessWidget — for UI that never changes by itself (a label, an icon, a static card). It just builds from the data passed in.
  • StatefulWidget — for UI that changes over time in response to user actions or data (a counter, a form, a toggle).

StatelessWidget

class Greeting extends StatelessWidget {
  final String name;
  const Greeting(this.name, {super.key});

  @override
  Widget build(BuildContext context) {
    return Text('Hi, $name!');
  }
}

It receives name and displays it. If name never changes for this widget, stateless is the right, efficient choice.

StatefulWidget

A stateful widget has a companion State class that holds data that can change. When you call setState, Flutter rebuilds the widget with the new values.

class Counter extends StatefulWidget {
  const Counter({super.key});
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;          // this is the state

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('Count: $count', style: const TextStyle(fontSize: 24)),
        ElevatedButton(
          onPressed: () => setState(() => count++),   // change + rebuild
          child: const Text('Add'),
        ),
      ],
    );
  }
}

What setState actually does

setState does two things: it lets you change a state variable, and it tells Flutter “this widget’s data changed — please rebuild it”. Flutter then calls build again with the new value and efficiently repaints only what changed. If you change a variable without setState, the UI won’t update.

The State lifecycle

  • initState() — called once when the widget is created; set up controllers, listeners, initial data.
  • build() — called every time the UI needs to render.
  • dispose() — called when the widget is removed; clean up controllers and subscriptions here to avoid leaks.

Keep state minimal

Only the data that actually changes belongs in state. Pass everything else in via the constructor. Smaller state means simpler, faster widgets.

Common mistakes

  • Changing a variable without setState (UI doesn’t update).
  • Using StatefulWidget when Stateless would do (unnecessary complexity).
  • Forgetting to dispose controllers (memory leaks).
Summary: Use StatelessWidget for fixed UI and StatefulWidget for UI that changes. Store changing data in the State class and update it with setState, which triggers an efficient rebuild. Clean up in dispose.