StatefulWidget во Flutter: setState, initState и жизненный цикл State

Егор Долгов · 10.11.2026
LikeButton
StatefulWidget — неизменяем
_LikeButtonState
State — данные, которые меняются
createState() →
setState() · initState() · const · dispose()
Показать по шагам
Субтитры
StatefulWidget — виджет с объектом State. Разберём, что у него внутри: почему классов два, как работает setState, жизненный цикл и ловушки.

StatefulWidget — виджет, у которого есть объект State для данных, меняющихся во время работы приложения. Как его написать и когда он нужен — в Уроке 2 . Здесь — что происходит внутри: почему классов два, как работает setState() , откуда State берёт стартовые данные и о чём спрашивают на собеседованиях.

Примеры продолжают кнопку «лайк» из Урока 2. Её код — в разделе 4 урока: карточка LessonCard и кнопка LikeButton со счётчиком _likes .

Содержание:

  1. Почему классов два
  2. Что делает setState()
  3. Стартовое значение: initState() и widget
  4. Что даёт const
  5. Жизненный цикл State
  6. Частые ошибки
  7. Вопросы с собеседований

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() , затем у состояния:

  1. initState() — один раз, при создании;
  2. didChangeDependencies() — сразу после initState() ; а если состояние берёт данные из InheritedWidget (виджет, через который потомки получают общие данные, например тему), — ещё и когда тот меняется или виджет перемещается в дереве;
  3. build() — после initState() , didUpdateWidget() и didChangeDependencies() , после переноса в другое место дерева, а после setState() — в следующем кадре;
  4. didUpdateWidget() — когда родитель передаёт новый виджет того же типа и с тем же ключом;
  5. 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.