StatefulWidget in Flutter: setState, initState and the State lifecycle

Egor Dolgov · 10.11.2026
LikeButton
StatefulWidget — immutable
_LikeButtonState
State — the data that changes
createState() →
setState() · initState() · const · dispose()
Show me step by step
Subtitles
A StatefulWidget is a widget with a State object. Let's see what's inside: why there are two classes, how setState works, the lifecycle, and the traps.

StatefulWidget is a widget that has a State object for data that changes while the app is running. How to write one and when you need it is covered in Lesson 2 . Here we look inside: why there are two classes, how setState() works, where State gets its starting data, and what interviewers ask.

The examples continue the “like” button from Lesson 2. Its code is in section 4 of the lesson: the LessonCard card and the LikeButton button with the _likes counter.

Contents:

  1. Why two classes
  2. What setState() does
  3. A starting value: initState() and widget
  4. What const gives you
  5. The State lifecycle
  6. Common mistakes
  7. Interview questions

1. Why two classes

A widget is immutable, and on a rebuild the parent may create a new widget object instead of the old one. If the button kept its count in its own field, the count would be lost every time the parent created a new button object. That's why the changing data lives in a separate State object: Flutter keeps it as long as a widget of the same type and with the same key stays at that place in the widget tree, and only updates its widget property — the reference to the current widget. The key is the optional key parameter every widget has: Flutter uses it to decide whether one widget replaces another at the same place.

You can see this with hot reload: run the button from Lesson 2, tap the heart a few times, replace Icons.favorite_border with Icons.thumb_up_outlined and hot reload ( r in the terminal or the hot reload button in your editor). The icon changes, but the count stays: the screen was rebuilt, the state is the same.

2. What setState() does

setState() runs the function you pass right away and marks the widget as needing a rebuild. Flutter calls build() itself in the next frame.

Check what happens without it: leave only _likes++; in onPressed and hot reload. Tap the heart a few times — the number on screen doesn't change, although _likes grows. Hot reload once more — the screen rebuilds and the button shows the accumulated number. The data was changing, but Flutter only learns about changes from setState() . Put setState() back.

The documentation recommends wrapping only the actual change of data in setState() , not the computation around it. Calling setState() after the state has been removed from the tree is an error; you can check this with the mounted property. But the documentation says it's better to cancel the work that would call setState() than to merely check mounted .

3. A starting value: initState() and widget

A real like has a starting number, and tapping again removes the like. We pass the starting number from the card into the button as a parameter. The full lib/main.dart :

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('flutter.su')),
        body: const MyBody(),
      ),
    );
  }
}

class MyBody extends StatelessWidget {
  const MyBody({super.key});

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        LessonCard(title: 'Lesson 1. Hello World', likes: 12),
        LessonCard(title: 'Lesson 2. Two kinds of widgets', likes: 5),
      ],
    );
  }
}

class LessonCard extends StatelessWidget {
  const LessonCard({super.key, required this.title, required this.likes});

  final String title;
  final int likes;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        title: Text(title),
        trailing: LikeButton(initialLikes: likes),
      ),
    );
  }
}

class LikeButton extends StatefulWidget {
  const LikeButton({super.key, required this.initialLikes});

  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;
  }

  void _toggle() {
    setState(() {
      _liked = !_liked;
      if (_liked) {
        _likes++;
      } else {
        _likes--;
      }
    });
  }

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

Do a hot restart — R in the terminal or the hot restart button in your editor: the button states are created again, and initState() picks up the starting numbers. The cards show 12 and 5. Tap the heart: it fills in and the number becomes 13. Tap again — the like is removed, back to 12.

  • initialLikes is a parameter of the LikeButton widget. The card passes its likes into it.
  • widget is how the state object refers to its widget: widget.initialLikes .
  • Flutter calls initState() once, when it creates the state. Here we take the starting number from the widget. The first line is super.initState() : the documentation says to do so. setState() isn't needed here — after initState() Flutter calls build() anyway.
  • late tells Dart that the value will come later: _likes gets it in initState() . If you read such a variable before that, a runtime error occurs.
  • !_liked is logical “not”: it turns true into false and back. _liked ? Icons.favorite : Icons.favorite_border means: if the like is set, the icon is filled, otherwise outlined.
  • onPressed: _toggle has no parentheses: the button gets the function itself and calls it on tap. With parentheses, _toggle() , you get an error: Dart tries to pass the button the result of the call, and the function returns nothing ( void ).

There are two kinds of data here: title and likes were passed to the card — they live in the widget and are final . _liked and _likes change on taps — they live in State . And here you can see why State is needed: LikeButton(initialLikes: likes) is written without const , so every rebuild of the card (on hot reload, for example) creates a new LikeButton object, while the state stays the same.

Trap: initState() is called once. Change likes: 12 to likes: 20 and hot reload — the number on the button doesn't change: hot reload keeps the state and doesn't call initState() again. To see 20 you need a hot restart. The same happens in a running app: if the parent passes a new initialLikes to the button, the value copied into _likes won't update. If a parameter can change, read widget.… directly in build() or update the copy in didUpdateWidget() — Flutter calls it when the parent passes a new widget of the same type and with the same key. That's why the parameter is called initialLikes — it's only the starting value.

4. What const gives you

In Lesson 2 the button was written as const LikeButton() . That's a constant: Dart creates it at compile time, and on every rebuild the card puts the very same object into trailing . Flutter sees that the widget is the same and doesn't rebuild it together with the card: the button's build() isn't called. The button itself still rebuilds on its own setState() , and hot reload rebuilds the whole tree, const widgets included.

The Flutter documentation recommends using const wherever possible: it's the same as keeping a ready widget and reusing it. And to give your widgets a const constructor — then the people who use your widget can write const too.

const is only possible when all of the widget's fields are final and the values are known at compile time. const LikeButton(initialLikes: likes) won't build ( Invalid constant value ): likes is a field of the card, and its value isn't known at compile time.

5. The State lifecycle

Flutter calls createState() on the widget, then on the state:

  1. initState() — once, on creation;
  2. didChangeDependencies() — right after initState() ; and if the state takes data from an InheritedWidget (a widget through which descendants get shared data, such as the theme), also when that changes or the widget moves in the tree;
  3. build() — after initState() , didUpdateWidget() and didChangeDependencies() , after a move to another place in the tree, and after setState() — in the next frame;
  4. didUpdateWidget() — when the parent passes a new widget of the same type and with the same key;
  5. dispose() — when the state is removed from the tree for good; after that you must not call setState() .

More in the State class documentation .

A tip from the same documentation: keep state as low in the tree as possible. setState() rebuilds the widget it was called in and may rebuild everything inside it — the smaller that widget, the less work. The documentation gives an example: if a page has a clock, make a separate clock widget that updates only itself, not the whole page.

6. Common mistakes

  • setState() in a StatelessWidget . It doesn't exist there — error The method 'setState' isn't defined . Make the changing part a StatefulWidget (Lesson 2, sections 3–4).
  • Data changed without setState() . The screen won't update (section 2).
  • A value from widget copied in initState() , expected to update. initState() is called once (section 3).
  • onPressed: _toggle() with parentheses. Error use_of_void_result : you need the function itself, onPressed: _toggle (section 3).
  • setState() after dispose() . An error; cancel the work when the state is removed (section 2).

7. Interview questions

What is the difference between StatelessWidget and StatefulWidget? A StatelessWidget builds its UI from its parameters and what it gets through context (the theme, for example). A StatefulWidget creates a State object that holds mutable data and can ask Flutter to rebuild the UI through setState() .

Is a StatefulWidget mutable? No. Both kinds of widgets are immutable: all fields are final . It's the State object that changes, not the widget.

Why does a StatefulWidget have a separate State class? On a rebuild the parent may create a new widget object instead of the old one. Flutter keeps the State as long as a widget of the same type and with the same key stays at that place in the tree, and only updates its widget property. That's why mutable data lives in State and survives rebuilds and hot reload.

What does setState() do? It calls the function you pass right away and marks the widget as needing a rebuild; Flutter rebuilds it in the next frame. Without setState() the screen may not update. You must not call setState() after dispose() — you can check this with the mounted property.

Does a StatelessWidget never rebuild? It does rebuild. The documentation names three typical cases when its build() is called: the widget is inserted into the tree for the first time; the parent passes it a new widget configuration; an InheritedWidget it depends on changes.

Why write const before a widget? Dart creates a const object at compile time, and when the parent rebuilds, the very same object ends up at that place. Flutter sees this and doesn't rebuild the widget together with its parent. It still gets rebuilt by its own setState() , by a change in an InheritedWidget and by hot reload. A const constructor is only possible when all fields are final ; you can write const at the call site only when all values are known at compile time.

A value from widget was copied into State in initState() , and then the parent changed it. What shows on screen? The old value: initState() is called once for each State object. Read widget.… directly in build() or update the copy in didUpdateWidget() .

How do you pass data from the widget to its State? Through the widget's parameters and the widget property in the state. Don't pass them into the state class constructor ( createState() => _MyState(value) ) — the analyzer reports no_logic_in_create_state : createState() should only create the state object. The reason: the State constructor is called once, so new values from the parent never reach it, while widget always points to the current widget (section 3).

In what order are the State methods called? createState() on the widget, then on the state initState() → didChangeDependencies() → build() ; after that build() follows didUpdateWidget() , dependency changes, and comes in the next frame after setState() ; dispose() when the state is removed from the tree for good. Details in section 5.

See also: Lesson 2. StatelessWidget and StatefulWidget , documentation for StatefulWidget , State , StatelessWidget .

Send questions and suggestions about this article to contact@flutter.su.