setState и поток перестроения

setState — это сигнал Flutter: «данные изменились, перерисуй меня».

Суть: вы меняете данные внутри setState, Flutter помечает виджет «грязным», заново вызывает build, сравнивает новое дерево со старым и обновляет только изменившиеся пиксели.

Ключ к производительности — понимание, что setState перестраивает не только сам виджет, но и всё его поддерево. Поэтому если вы вызываете setState в корне экрана, перестраивается весь экран, даже неизменная его часть. Решение — опускать состояние как можно ниже, ближе к тому маленькому виджету, который реально меняется. Тогда дорогая работа касается крошечного куска дерева, а остальное Flutter оставляет нетронутым.

Локальное состояние — это самый простой способ заставить интерфейс реагировать. Счётчик, чекбокс, развёрнутая карточка — всё это живёт в State и обновляется через setState. Понимание точного потока «изменение → перестроение → кадр» избавит вас от большинства багов «почему не обновляется» и «почему тормозит».

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

class _LikeButtonState extends State<LikeButton> {
  bool liked = false;

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(liked ? Icons.favorite : Icons.favorite_border),
      color: liked ? Colors.red : Colors.grey,
      onPressed: () {
        setState(() {        // 1. меняем данные
          liked = !liked;    // 2. внутри setState
        });                  // 3. Flutter перестроит виджет
      },
    );
  }
}

Как работает поток под капотом

Когда вы вызываете setState, Flutter не перерисовывает экран мгновенно. Он помечает Element этого виджета как «грязный» и планирует перестроение на следующий кадр. В нужный момент он вызывает build, получает новое дерево виджетов и через алгоритм сравнения (diffing) находит, что именно изменилось. Обновляются только отличающиеся render-объекты — поэтому Flutter держит 60 кадров в секунду.

  onPressed -> setState(() { liked = !liked; })
                     |
                     v
        Element помечен "грязным" (dirty)
                     |
                     v
        запланирован новый кадр
                     |
                     v
              build() заново
                     |
                     v
        сравнение нового и старого дерева (diff)
                     |
                     v
        обновлены ТОЛЬКО изменившиеся пиксели -> кадр
# Модель потока setState -> build -> diff -> кадр
class Widget:
    def __init__(self):
        self.liked = False
        self.dirty = False

    def set_state(self, mutator):
        mutator()
        self.dirty = True            # помечаем грязным
        self.flush()                 # планируем перестроение

    def build(self):
        return 'favorite' if self.liked else 'favorite_border'

    def flush(self):
        if self.dirty:
            new_tree = self.build()
            print('кадр обновлён:', new_tree)
            self.dirty = False

w = Widget()
w.flush()                                     # ничего, не грязный
w.set_state(lambda: setattr(w, 'liked', True))  # лайк -> перестроение
w.set_state(lambda: setattr(w, 'liked', False)) # снятие лайка

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

  • Менять данные вне setState — экран не обновится.
  • Тяжёлая работа в setState — он должен только менять данные; запросы и вычисления выносите наружу.
  • setState после dispose (например, по завершении запроса на удалённом экране) — ошибка; проверяйте mounted.

Best practices

  • Внутри setState держите только присваивания, а не логику и не сетевые вызовы.
  • Делайте StatefulWidget маленьким — тогда перестраивается лишь он, а не весь экран.
  • Перед setState после await проверяйте if (mounted).

Помогает и щедрое использование const-виджетов. Помеченный const виджет создаётся один раз и при перестроении родителя переиспользуется без пересоздания — Flutter мгновенно понимает, что он не изменился. Расставляя const везде, где данные не зависят от меняющегося состояния, вы бесплатно ускоряете приложение и снимаете нагрузку с механизма перестроения. Это привычка, которую стоит выработать с первых дней.

Итог: setState — простой и мощный механизм локального состояния. Зная поток «грязный Element → build → diff → кадр», вы понимаете, почему Flutter быстр и как избегать лишних перестроений. Для данных, общих между экранами, нужен другой подход — об этом дальше.

Проверьте себя
1. Что делает Flutter сразу после вызова setState?
AМгновенно перерисовывает весь экран
BПомечает Element грязным и планирует перестроение на следующий кадр
CУдаляет виджет
DВызывает dispose
2. Почему изменение данных вне setState не обновляет экран?
AДанные не меняются
BFlutter не узнаёт, что нужно заново вызвать build
CЭто вызывает ошибку компиляции
DsetState запрещён