StatefulWidget in Flutter: setState, initState and the State lifecycle
Egor Dolgov · 10.11.2026A 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:
- Why two classes
- What setState() does
- A starting value: initState() and widget
- What const gives you
- The State lifecycle
- Common mistakes
- 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.
-
initialLikesis a parameter of theLikeButtonwidget. The card passes itslikesinto it. -
widgetis 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 issuper.initState(): the documentation says to do so.setState()isn't needed here — afterinitState()Flutter callsbuild()anyway. -
latetells Dart that the value will come later:_likesgets it ininitState(). If you read such a variable before that, a runtime error occurs. -
!_likedis logical “not”: it turnstrueintofalseand back._liked ? Icons.favorite : Icons.favorite_bordermeans: if the like is set, the icon is filled, otherwise outlined. -
onPressed: _togglehas 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:
-
initState()— once, on creation; -
didChangeDependencies()— right afterinitState(); and if the state takes data from anInheritedWidget(a widget through which descendants get shared data, such as the theme), also when that changes or the widget moves in the tree; -
build()— afterinitState(),didUpdateWidget()anddidChangeDependencies(), after a move to another place in the tree, and aftersetState()— in the next frame; -
didUpdateWidget()— when the parent passes a new widget of the same type and with the same key; -
dispose()— when the state is removed from the tree for good; after that you must not callsetState().
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 aStatelessWidget. It doesn't exist there — errorThe method 'setState' isn't defined. Make the changing part aStatefulWidget(Lesson 2, sections 3–4). -
Data changed without
setState(). The screen won't update (section 2). -
A value from
widgetcopied ininitState(), expected to update.initState()is called once (section 3). -
onPressed: _toggle()with parentheses. Erroruse_of_void_result: you need the function itself,onPressed: _toggle(section 3). -
setState()afterdispose(). 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.