Flutter Stateful Widgets

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 #

  1. Flutter creates the StatefulWidget
  2. Flutter creates one State object
  3. Flutter:
    • Freely destroys & recreates the widget
    • Keeps the same State as long as the widget stays in the tree

This is why:

  • build() runs often
  • initState() 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:

  • State updates counter
  • 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
  • _counter changes 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)
  • CounterPageimmutable configuration
  • _CounterPageStatemutable 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 #

  1. Press the + button rapidly → Watch build spam in the console
  2. Hot-reload the app → State is preserved
  3. Hot-restart the app → _counter resets

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
Was this tutorial helpful ?