StatefulWidget и жизненный цикл

StatefulWidget умеет помнить и менять данные внутри себя, перерисовываясь через setState.

Суть: StatefulWidget хранит изменяемое состояние в отдельном объекте State. Вызов setState помечает виджет «грязным», и Flutter перестраивает его. Жизненный цикл управляет созданием, обновлением и уничтожением.

Разделение StatefulWidget на две части — сам виджет и объект State — поначалу кажется лишней церемонией, но у него есть глубокая причина. Виджет, как и любой другой, неизменяем и может пересоздаваться; а вот State живёт дольше и сохраняется, пока виджет остаётся в дереве на своём месте. Именно поэтому изменяемые данные хранятся в State, а не в самом виджете: State переживает перестроения и помнит счётчик, текст поля или прогресс анимации.

Счётчик, переключатель, поле ввода, анимация — всё, что меняется в ответ на действия пользователя, требует памяти. StatelessWidget такой памяти не имеет. Поэтому существует StatefulWidget: он состоит из двух частей — неизменяемого виджета и связанного с ним объекта State, который и хранит меняющиеся данные.

class Counter extends StatefulWidget {
  const Counter({super.key});
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;   // изменяемое состояние живёт здесь

  void increment() {
    setState(() {           // помечаем виджет на перестроение
      count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: increment,
      child: Text('Нажато: $count'),
    );
  }
}

Ключевой момент — setState. Вы меняете данные внутри переданной ему функции, и Flutter после этого заново вызывает build. Если поменять count без setState, значение изменится, но экран не обновится.

Как работает жизненный цикл под капотом

У State есть упорядоченные этапы жизни. createState создаёт объект состояния. initState вызывается один раз при рождении — здесь делают начальную настройку и запросы. build рисует интерфейс. setState запускает повторный build. didUpdateWidget срабатывает, когда родитель дал новые данные. dispose вызывается при удалении — здесь освобождают ресурсы (контроллеры, подписки).

  createState()
       |
       v
  initState()            <- один раз: подписки, начальные данные
       |
       v
  didChangeDependencies()
       |
       v
  +-> build()            <- рисуем интерфейс
  |    ^   |
  |    |   v
  |  setState()          <- меняем данные -> снова build
  |    |
  |  didUpdateWidget()   <- родитель дал новые входные данные
  |    |
  +----+
       |
       v
  dispose()              <- удаление: отписки, освобождение
# Модель жизненного цикла State и setState -> build
class CounterState:
    def __init__(self):
        self.count = 0
        self.init_state()

    def init_state(self):
        print('initState: настройка один раз')

    def set_state(self, mutator):
        mutator()            # меняем данные внутри
        self.build()         # затем перестраиваем

    def build(self):
        print(f'build: Нажато {self.count}')

    def dispose(self):
        print('dispose: освобождаем ресурсы')

s = CounterState()
s.build()
s.set_state(lambda: setattr(s, 'count', s.count + 1))  # +1 и rebuild
s.set_state(lambda: setattr(s, 'count', s.count + 1))
s.dispose()

Частые ошибки

  • Менять состояние без setState — данные обновятся, а экран нет.
  • Забыть dispose для контроллеров и подписок — утечки памяти.
  • Тяжёлая работа в build вместо initState — запрос полетит при каждом перестроении.

Best practices

  • Меняйте состояние только внутри функции, переданной в setState.
  • Начальную настройку и подписки делайте в initState, освобождение — в dispose.
  • Делайте StatefulWidget настолько маленьким, насколько возможно: перестраивается только он, не весь экран.

Метод didUpdateWidget заслуживает внимания, потому что новички про него забывают. Он вызывается, когда родитель передал виджету новые входные данные, и позволяет среагировать на это — например, перезапустить анимацию или загрузку, если изменился переданный идентификатор. Без него State может застрять на старых данных, показывая профиль одного пользователя, когда родитель уже переключился на другого. Помните: входные данные виджета могут меняться, и State обязан это учитывать.

Итог: StatefulWidget — это виджет с памятью. Запомните цикл initState → build → setState↔build → dispose и золотое правило: меняешь данные — оборачивай в setState. Подробнее о потоке перестроения — в разделе про состояние.

Проверьте себя
1. Что произойдёт, если изменить count без вызова setState?
AПриложение упадёт
BЗначение изменится, но экран не перерисуется
CВиджет удалится
DВызовется dispose
2. В каком методе жизненного цикла правильно освобождать контроллеры и подписки?
AinitState
Bbuild
Cdispose
DsetState