StatefulWidget and setState

Flutter · Essentials · 14 min

Any widget that changes in response to the user — a counter, a toggle, a text field — starts here.

A `StatefulWidget` is split into two classes:

  • The widget itself, still immutable, holding configuration.
  • A `State<T>` object, created by createState(), which holds mutable data and builds the UI. Flutter keeps the State alive across rebuilds as long as the widget stays at the same place in the tree.

To change the UI, mutate the state inside `setState(() { ... })`. That marks the widget dirty so Flutter calls build again on the next frame. Changing a field without setState updates the value but not the screen.

Lifecycle methods on State:

  • initState() — runs once when the State is created; set up controllers, subscriptions and animations here (call super.initState() first).
  • didUpdateWidget(oldWidget) — the parent rebuilt with new configuration; react to changed fields.
  • dispose() — release controllers, cancel subscriptions and timers (call super.dispose() last).

Inside State, the widget's fields are available through widget.someField, and context and mounted are properties. After an await, check if (!mounted) return; before calling setState or using context, because the widget may have been removed meanwhile.

Keep state as low in the tree as possible: only the widget that owns the state rebuilds. When several widgets need the same state, lift it up or use a state-management approach.

Example

import 'package:flutter/material.dart';
class LikeButton extends StatefulWidget {
  const LikeButton({super.key, this.initialLikes = 0});
  final int initialLikes;
  @override
  State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
  late int _likes;
  bool _liked = false;
  @override
  void initState() { super.initState(); _likes = widget.initialLikes; }  // read config once

  void _toggle() => setState(() {
        _liked = !_liked;
        _likes += _liked ? 1 : -1;
      });

  @override
  Widget build(BuildContext context) => TextButton.icon(
        onPressed: _toggle,
        icon: Icon(_liked ? Icons.favorite : Icons.favorite_border),
        label: Text('$_likes'),
      );
}

Remember

  • Mutable data lives in a State object that survives rebuilds
  • Change it inside setState to trigger a rebuild
  • Set up in initState, clean up in dispose, check mounted after awaits

Watch out for

  • Calling setState after an await when the widget has been disposed — check mounted first.

Check yourself

  1. You update a State field without calling setState. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/ui/interactivity