Skip to content
Boring Doc
Official documentation (snapshot)doc

Stateful widgets

Learn about StatefulWidgets and rebuilding Flutter UI.

Source last updated Oct 21, 2026 · last synchronized 3 weeks ago · commit b3045439b7c0 · read on docs.flutter.dev

This is a rendered snapshot of the official page as stored by Boring Doc for provenance and search. Site-specific components (embedded demos, DartPad, images with relative paths) may not render; use the official page for the full experience. Content © The Flutter Authors, CC BY 3.0.

Learn when widgets need to be stateful and how to trigger UI updates with setState.


Introduction

So far, your app displays a grid and an input field, but the grid doesn't yet update to reflect the user's guesses. When this app is complete, each tile in the next unfilled row should update after each submitted user guess by:

  • Displaying the correct letter.
  • Changing color to reflect whether the letter is correct (green), is in the word but at an incorrect position (yellow), or doesn't appear in the word at all (grey).

To handle this dynamic behavior, you need to convert GamePage from a StatelessWidget to a StatefulWidget.

Why stateful widgets?

When a widget's appearance or data needs to change during its lifetime, you need a StatefulWidget and a companion State object. While the StatefulWidget itself is still immutable (its properties can't change after creation), the State object is long-lived, can hold mutable data, and can be rebuilt when that data changes, causing the UI to update.

For example, the following widget tree imagines a simple app that uses a stateful widget with a counter that increases when the button is pressed.

Here is the basic StatefulWidget structure (doesn't do anything yet):

class ExampleWidget extends StatefulWidget {
  ExampleWidget({super.key});

  @override
  State<ExampleWidget> createState() => _ExampleWidgetState();
}

class _ExampleWidgetState extends State<ExampleWidget> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Convert GamePage to a stateful widget

To convert the GamePage (or any other) widget from a stateless widget to a stateful widget, do the following steps:

  1. Change GamePage to extend StatefulWidget instead of StatelessWidget.
  2. Create a new class named _GamePageState, that extends State<GamePage>. This new class will hold the mutable state and the build method. Move the build method and all properties instantiated on the widget from GamePage to the state object.
  3. Implement the createState() method in GamePage, which returns an instance of _GamePageState.

You don't have to manually do this work, as the Flutter plugins for VS Code and IntelliJ provide "quick assists" that can do this conversion for you.

class GamePage extends StatefulWidget {
  GamePage({super.key});

  @override
  State&#x3C;GamePage> createState() => _GamePageState();
}

class _GamePageState extends State&#x3C;GamePage> {
  final Game _game = Game();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        children: [
          for (var guess in _game.guesses)
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                for (var letter in guess)
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 2.5, vertical: 2.5),
                    child: Tile(letter.char, letter.type),
                  )
              ],
            ),
          GuessInput(
            onSubmitGuess: (_) {
              // TODO, handle guess
              print(guess); // Temporary
            },
          ),
        ],
      ),
    );
  }
}

Updating the UI with setState

Whenever you mutate a State object, you must call setState to signal the framework to update the user interface and call the build method again.

In this app, when a user makes a guess, the word they guessed is saved on the Game object, which is a property on the GamePage class, and therefore is state that might change and require the UI to update. When this state is mutated, the grid should be re-drawn to show the user's guess.

To implement this, update the callback function passed to GuessInput. The function needs to call setState and, within setState, it needs to execute the logic to determine whether the users guess was correct.

and outside the scope of this tutorial.

class GamePage extends StatefulWidget {
  GamePage({super.key});

  @override
  State&#x3C;GamePage> createState() => _GamePageState();
}

class _GamePageState extends State&#x3C;GamePage> {
  final Game _game = Game();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        children: [
          for (var guess in _game.guesses)
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                for (var letter in guess)
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 2.5, vertical: 2.5),
                    child: Tile(letter.char, letter.type),
                  )
              ],
            ),
          GuessInput(
            onSubmitGuess: (String guess) {
              setState(() { // NEW
                _game.guess(guess);
              });
            },
          ),
        ],
      ),
    );
  }
}

Now, when you type a legal guess into the TextInput and submit it, the application will reflect the user's guess. If you were to call _game.guess(guess) without a calling setState, the internal game data would change, but Flutter wouldn't know it needs to repaint the screen, and the user wouldn't see any updates.

Convert GuessInput to a stateful widget

When GamePage rebuilds after calling setState, all of its child widgets are rebuilt as well. Because GuessInput was originally created as a StatelessWidget, its TextEditingController and FocusNode were stored directly in the widget class. Since a new widget instance is created on every rebuild, these controllers were recreated each time, causing the text input field to lose focus and leaving unused controllers without proper disposal.

To keep focus on the text field between guesses and manage controller lifecycles properly, convert GuessInput into a StatefulWidget:

  1. Change GuessInput to extend StatefulWidget instead of StatelessWidget.
  2. Create a companion _GuessInputState class extending State<GuessInput>.
  3. Move _textEditingController, _focusNode, _onSubmit(), and build() into _GuessInputState.
  4. Implement dispose() to clean up _textEditingController and _focusNode.

Your modified GuessInput widget should look like this:

class GuessInput extends StatefulWidget {
  const GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  @override
  State&#x3C;GuessInput> createState() => _GuessInputState();
}

class _GuessInputState extends State&#x3C;GuessInput> {
  final TextEditingController _textEditingController = TextEditingController();
  final FocusNode _focusNode = FocusNode();

  @override
  void dispose() {
    _textEditingController.dispose();
    _focusNode.dispose();
    super.dispose();
  }

  void _onSubmit() {
    widget.onSubmitGuess(_textEditingController.text.trim());
    _textEditingController.clear();
    _focusNode.requestFocus();
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              focusNode: _focusNode,
              autofocus: true,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              onSubmitted: (input) {
                _onSubmit();
              },
            ),
          ),
        ),
        IconButton(
          padding: EdgeInsets.zero,
          icon: const Icon(Icons.arrow_circle_up),
          onPressed: _onSubmit,
        ),
      ],
    );
  }
}

By converting GuessInput to a StatefulWidget, _GuessInputState persists across parent rebuilds, keeping focus on the text field after each guess is submitted and properly disposing of resources when the widget is unmounted.

Review

Test yourself

Was this useful?

Content from flutter/website, © The Flutter Authors, used under the Creative Commons Attribution 3.0 License; code samples under the BSD License.