StatefulWidget во Flutter: setState, initState и жизненный цикл State
Егор Долгов · 10.11.2026StatefulWidget — виджет с объектом State. Разберём, что у него внутри: почему классов два, как работает setState, жизненный цикл и ловушки.
StatefulWidget
— виджет, у которого есть объект
State
для данных, меняющихся во время работы приложения. Как его написать и когда он нужен — в
Уроке 2
. Здесь — что происходит внутри: почему классов два, как работает
setState()
, откуда
State
берёт стартовые данные и о чём спрашивают на собеседованиях.
Примеры продолжают кнопку «лайк» из Урока 2. Её код — в разделе 4 урока: карточка
LessonCard
и кнопка
LikeButton
со счётчиком
_likes
.
Содержание:
- Почему классов два
- Что делает setState()
- Стартовое значение: initState() и widget
- Что даёт const
- Жизненный цикл State
- Частые ошибки
- Вопросы с собеседований
1. Почему классов два
Виджет неизменяем, а при перестройке родитель может создать новый объект виджета вместо старого. Если бы кнопка хранила счёт в своём поле, он бы терялся всякий раз, когда родитель создаёт новый объект кнопки. Поэтому меняющиеся данные лежат в отдельном объекте
State
: Flutter сохраняет его, пока на этом месте в дереве виджетов стоит виджет того же типа и с тем же ключом, и только обновляет у него свойство
widget
— ссылку на текущий виджет. Ключ — необязательный параметр
key
, который есть у каждого виджета: по нему Flutter решает, заменить ли один виджет другим на том же месте.
Это видно при горячей перезагрузке: запустите кнопку из Урока 2, нажмите на сердечко несколько раз, замените
Icons.favorite_border
на
Icons.thumb_up_outlined
и сделайте горячую перезагрузку (
r
в терминале или кнопка hot reload в редакторе). Значок поменяется, а счёт останется: экран перестроен, состояние то же.
2. Что делает setState()
setState()
сразу выполняет переданную функцию и помечает виджет как требующий перестройки. Сам
build()
Flutter вызовет в следующем кадре.
Проверьте, что будет без него: оставьте в
onPressed
только
_likes++;
и сделайте горячую перезагрузку. Нажмите на сердечко несколько раз — число на экране не меняется, хотя
_likes
растёт. Сделайте горячую перезагрузку ещё раз — экран перестроится, и на кнопке появится накопленное число. Данные менялись, но Flutter узнаёт об изменениях только из
setState()
. Верните
setState()
на место.
Документация советует оборачивать в
setState()
само изменение данных, а не вычисления вокруг него. Вызывать
setState()
после того, как состояние удалено из дерева, — ошибка; проверить это можно свойством
mounted
. Но документация советует лучше отменять работу, которая вызовет
setState()
, чем только проверять
mounted
.
3. Стартовое значение: initState() и widget
У настоящего лайка есть стартовое число, и повторное нажатие лайк снимает. Стартовое число передадим из карточки в кнопку параметром. Полный
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: 'Урок 1. Hello World', likes: 12),
LessonCard(title: 'Урок 2. Два вида виджетов', 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'),
);
}
}
Сделайте горячий перезапуск —
R
в терминале или кнопка hot restart в редакторе: состояния кнопок создадутся заново, и
initState()
возьмёт стартовые числа. На карточках — 12 и 5. Нажмите на сердечко: оно закрасится, число станет 13. Нажмите ещё раз — лайк снимется, снова 12.
-
initialLikes— параметр виджетаLikeButton. Карточка передаёт в него своиlikes. -
widget— так объект состояния обращается к своему виджету:widget.initialLikes. -
initState()Flutter вызывает один раз, когда создаёт состояние. Здесь мы берём стартовое число из виджета. Первой строкой —super.initState(): так велит документация.setState()здесь не нужен — послеinitState()Flutter и так вызоветbuild(). -
lateговорит Dart, что значение появится позже:_likesполучит его вinitState(). Если прочитать такую переменную до этого, во время работы возникнет ошибка. -
!_liked— логическое «не»: превращаетtrueвfalseи наоборот._liked ? Icons.favorite : Icons.favorite_border— если лайк стоит, значок закрашенный, иначе контурный. -
onPressed: _toggle— без скобок: кнопке передаётся сама функция, кнопка вызовет её при нажатии. Со скобками_toggle()будет ошибка: Dart попробует передать кнопке результат вызова, а функция ничего не возвращает (void).
Здесь видны два вида данных:
title
и
likes
карточке передали — они в виджете,
final
. А
_liked
и
_likes
меняются от нажатий — они в
State
. И здесь же понятно, зачем нужен
State
:
LikeButton(initialLikes: likes)
написан без
const
, поэтому при каждой перестройке карточки (например, при горячей перезагрузке) создаётся новый объект
LikeButton
, а состояние остаётся прежним.
Ловушка: initState() вызывается один раз.
Поменяйте
likes: 12
на
likes: 20
и сделайте горячую перезагрузку — число на кнопке не изменится: горячая перезагрузка сохраняет состояние и не вызывает
initState()
повторно. Чтобы увидеть 20, нужен горячий перезапуск. То же случится в работающем приложении: если родитель передаст в кнопку новое
initialLikes
, скопированное в
_likes
значение не обновится. Если параметр может меняться, читайте
widget.…
прямо в
build()
или обновляйте копию в
didUpdateWidget()
— его Flutter вызывает, когда родитель передаёт новый виджет того же типа и с тем же ключом. Поэтому параметр и назван
initialLikes
— это только стартовое значение.
4. Что даёт const
В Уроке 2 кнопка была написана как
const LikeButton()
. Это константа: Dart создаёт её ещё при компиляции, и при каждой перестройке карточка подставляет в
trailing
тот же самый объект. Flutter видит, что виджет тот же, и не перестраивает его вместе с карточкой:
build()
кнопки не вызывается. Сама кнопка по-прежнему перестраивается от своего
setState()
, а горячая перезагрузка перестраивает всё дерево целиком,
const
-виджеты тоже.
Документация Flutter советует ставить
const
везде, где можно: это то же, что сохранить готовый виджет и использовать его снова. И объявлять у своих виджетов
const
-конструктор — тогда и те, кто использует виджет, смогут написать
const
.
const
возможен, только если все поля виджета
final
, а значения известны при компиляции.
const LikeButton(initialLikes: likes)
не соберётся (
Invalid constant value
):
likes
— поле карточки, его значение при компиляции неизвестно.
5. Жизненный цикл State
Flutter вызывает у виджета
createState()
, затем у состояния:
-
initState()— один раз, при создании; -
didChangeDependencies()— сразу послеinitState(); а если состояние берёт данные изInheritedWidget(виджет, через который потомки получают общие данные, например тему), — ещё и когда тот меняется или виджет перемещается в дереве; -
build()— послеinitState(),didUpdateWidget()иdidChangeDependencies(), после переноса в другое место дерева, а послеsetState()— в следующем кадре; -
didUpdateWidget()— когда родитель передаёт новый виджет того же типа и с тем же ключом; -
dispose()— когда состояние удаляется из дерева навсегда; после этогоsetState()вызывать нельзя.
Подробнее — в документации класса State .
Совет из той же документации: держите состояние как можно ниже в дереве.
setState()
перестраивает виджет, в котором его вызвали, и может перестроить всё, что внутри, — чем меньше этот виджет, тем меньше работы. Документация приводит пример: если на странице есть часы, сделайте отдельный виджет часов, который обновляет только себя, а не всю страницу.
6. Частые ошибки
-
setState()вStatelessWidget. Его там нет — ошибкаThe method 'setState' isn't defined. Сделайте меняющуюся частьStatefulWidget(Урок 2, разделы 3–4). -
Данные поменяли без
setState(). Экран не обновится (раздел 2). -
Значение из
widgetскопировали вinitState()и ждут, что оно обновится.initState()вызывается один раз (раздел 3). -
onPressed: _toggle()со скобками. Ошибкаuse_of_void_result: нужна сама функция,onPressed: _toggle(раздел 3). -
setState()послеdispose(). Ошибка; отменяйте работу при удалении состояния (раздел 2).
7. Вопросы с собеседований
Чем StatelessWidget отличается от StatefulWidget?
StatelessWidget
строит интерфейс из своих параметров и того, что берёт через
context
(например, тему).
StatefulWidget
создаёт объект
State
, который хранит изменяемые данные и может попросить Flutter перестроить интерфейс через
setState()
.
StatefulWidget изменяемый?
Нет. Неизменяемы оба вида виджетов: все поля —
final
. Меняется объект
State
, а не виджет.
Зачем у StatefulWidget отдельный класс State?
При перестройке родитель может создать новый объект виджета вместо старого.
State
же Flutter сохраняет, пока на этом месте в дереве стоит виджет того же типа и с тем же ключом, и только обновляет у него свойство
widget
. Поэтому изменяемые данные живут в
State
и переживают перестройку и горячую перезагрузку.
Что делает setState()?
Сразу вызывает переданную функцию и помечает виджет как требующий перестройки; Flutter перестроит его в следующем кадре. Без
setState()
экран может не обновиться. После
dispose()
вызывать
setState()
нельзя — проверить это можно свойством
mounted
.
StatelessWidget никогда не перестраивается?
Перестраивается. Документация называет три обычных случая, когда вызывается его
build()
: виджет впервые вставлен в дерево; родитель передал ему новый виджет-конфигурацию; изменился
InheritedWidget
, от которого он зависит.
Зачем писать
const
перед виджетом?
const
-объект Dart создаёт ещё при компиляции, и при перестройке родителя на этом месте оказывается тот же самый объект. Flutter видит это и не перестраивает виджет вместе с родителем. Его всё равно перестроят собственный
setState()
, изменение
InheritedWidget
и горячая перезагрузка.
const
-конструктор возможен, только если все поля
final
; написать
const
при вызове можно, только если все значения известны при компиляции.
Значение из
widget
скопировали в State в
initState()
, а родитель потом его поменял. Что будет на экране?
Старое значение:
initState()
вызывается один раз для каждого объекта
State
. Читайте
widget.…
прямо в
build()
или обновляйте копию в
didUpdateWidget()
.
Как передать данные из виджета в State?
Через параметры виджета и свойство
widget
в состоянии. Передавать их в конструктор класса состояния (
createState() => _MyState(value)
) не нужно — анализатор выдаёт замечание
no_logic_in_create_state
:
createState()
должен только создавать объект состояния. Причина: конструктор
State
вызывается один раз, и новые значения от родителя в него не попадут, а
widget
всегда указывает на текущий виджет (раздел 3).
В каком порядке вызываются методы State?
createState()
у виджета, затем у состояния
initState()
→
didChangeDependencies()
→
build()
; дальше
build()
— после
didUpdateWidget()
, после смены зависимостей и в следующем кадре после
setState()
;
dispose()
— при удалении из дерева навсегда. Подробно — в разделе 5.
Смотрите также: Урок 2. StatelessWidget и StatefulWidget , документация StatefulWidget , State , StatelessWidget .
Вопросы и предложения по статье присылайте на contact@flutter.su.