Stateful Виджет

Что такое `Stateful Widget`

StatefulWidget не хранит состояние сам и даже не имеет метод build(), но он умеет создавать и связывать с собой объект State, который уже отвечает за хранение состояния и управление обновлениями UI.

StatefulWidget — это лишь "оболочка", которая делегирует всю работу по управлению состоянием и построению UI своему State.

StatefulWidget — изменяет своё состояние в процессе работы приложения. Он состоит из двух классов:

  1. Сам виджет (StatefulWidget).
  2. Состояние (State), которое управляет изменяемыми данными.

Производительность Flutter

Чтобы отобразить пользовательский интерфейс (UI) на экране, Flutter вызывает метод build(). Этот метод присутствует у каждого виджета и может вызываться довольно часто.

При каждом вызове build() весь виджет перерисовывается, что звучит крайне неэффективно. Однако Flutter устроен умнее и не обновляет весь UI целиком.

Flutter оптимизирован для работы при 60 кадрах в секунду (fps) и поддерживает 120+ fps на современных устройствах.

Flutter использует эффективный механизм отрисовки:

  1. При первой отрисовке выполняются все необходимые вычисления:
    — определение положения и размеров элементов,
    — установка цвета, текста и других параметров.
    Это самый затратный этап, каждый пиксель нужно рассчитать и настроить.
  2. При последующих вызовах метода build() Flutter НЕ пересоздаёт весь UI заново, если в нём ничего не изменилось.
    Вместо этого он использует уже рассчитанные данные и снова показывает их на экране. Это позволяет значительно ускорить рендеринг.
  3. Реальная перерисовка затрагивает только те части интерфейса, состояние которых изменилось. Таким образом, Flutter обновляет только необходимые элементы, что делает его работу максимально эффективной.

🌳 Лес , деревья и Flutter

В основе архитектуры Flutter лежат три дерева, которые взаимодействуют друг с другом для эффективного построения и обновления пользовательского интерфейса (UI):

  1. Дерево виджетов (Widget Tree)
  2. Дерево элементов (Element Tree)
  3. Дерево рендеринга (RenderObject Tree)

Дерево виджетов

Представляет собой виджеты которые разработчики пишут в коде при создании Flutter приложения.

Это дерево создаётся вызовом метода build() и предоставляет описание того, как должен выглядеть UI. Важно понимать, что дерево виджетов не отображается на экране, оно только сообщает Flutter, что нужно отрисовать.

Дерево виджетов часто перестраивается, например, при вызове setState(). Однако это не означает, что весь UI пересоздаётся с нуля.

Дерево элементов

Это ключевая часть внутренней работы Flutter. Это дерево автоматически создаётся на основе дерева виджетов и связывает виджеты (конфигурации) с реальными объектами рендеринга (то что показывается на экране устройства)

Дерево элементов не перестраивается при каждом вызове build(). Оно управляется более эффективно и служит мостом между описанием (виджетами) и фактическим отображением (рендерингом).

Дерево рендеринга

Это то, что в итоге отображается на экране. Это дерево состоит из объектов, которые непосредственно отвечают за отрисовку пикселей. Flutter обновляет только те части, которые изменились, что делает процесс рендеринга очень эффективным.

Итого

Tree relationship diagram Widget, Element, RenderObject
Файл main.dart
Светлая тема Темная тема
class HomeWidget extends StatelessWidget {  
  HomeWidget({super.key}) {  
    debugPrint("💛 HomeWidget constructor");  
  }  
  
  @override  
  Widget build(BuildContext context) {  
    debugPrint("💛 HomeWidget build");  
  
    return Container(  
      width: double.infinity,  
      decoration: BoxDecoration(  
        gradient: LinearGradient(  
          colors: [Color(0xFFBFF098), Color(0xFF6FD6FF)],  
          begin: Alignment.topLeft,  
          end: Alignment.bottomRight,  
        ),  
      ),  
      child: SafeArea(  
        child: Center(  
          child: Padding(  
            padding: const EdgeInsets.all(32.0),  
            child: TrackCard(),  
          ),  
        ),  
      ),  
    );  
  }  
}
Файл 2_stateful_widget.dart
Светлая тема Темная тема
class TrackCard extends StatelessWidget {  
  TrackCard({super.key}) {  
    debugPrint("💛TrackCard constructor");  
  }  
  
  @override  
  Widget build(BuildContext context) {  
    debugPrint("💛TrackCard build");  
    return Card(  
      color: Colors.white,  
      child: ListTile(  
        leading: Image.asset("assets/images/pro.webp"),  
        title: const TitleWidget(text: "Stateless"),  
        subtitle: const SubtitleWidget(text: "Flutter vibes"),  
        trailing: LikeButton(),  
      ),  
    );  
  }  
}  
  
class TitleWidget extends StatelessWidget {  
  final String text;  
  const TitleWidget({super.key, this.text = ""});  
  
  @override  
  Widget build(BuildContext context) {  
    debugPrint("💛TitleWidget build");  
    return Text(  
      text,  
      style: TextStyle(  
        fontWeight: FontWeight.bold,  
      ),  
    );  
  }  
}  
  
class SubtitleWidget extends StatelessWidget {  
  final String text;  
  
  const SubtitleWidget({super.key, this.text = ""});  
  @override  
  Widget build(BuildContext context) {  
    debugPrint("💛SubtitleWidget build");  
  
    return Text(text.toUpperCase());  
  }  
}  
  
class LikeButton extends StatefulWidget {  
  const LikeButton({super.key});  
  
  @override  
  State createState() => _LikeButtonState();  
}  
  
class _LikeButtonState extends State {  
  
  bool isFavorite = false;  
  
  @override  
  Widget build(BuildContext context) {  
    return IconButton(  
      onPressed: () {  
        // При нажатии вызываем setState, чтобы сообщить Flutter о необходимости перестроить виджет
        setState(() {  
          isFavorite = !isFavorite;  
        });  
      },  
      // В зависимости от значения isFavorite, показываем разную иконку
      icon: isFavorite  
          ? Icon(Icons.favorite, color: Colors.red)  
          : Icon(Icons.favorite_border),  
    );  
  }  
}

Как это всё работает?

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

Element

Элемент - это объект, который указывает на виджет (содержит ссылку)

  • Элемент не хранит конфигурацию виджета.
  • Элемент связывает виджет с деревом рендеринга.

Элемент содержит:

  • Context - механизм доступа к данным о расположении + методы работы с ним.
  • State - ссылку на состояние, если это Stateful виджет.
Дерево элементов и грязные объекты

Когда состояние виджета меняется, Flutter не перерисовывает весь экран приложения, а помечает определенные элементы как грязные (Dirty) и в дальнейшем обновляет только их.

В нашем случае Dirty становится элемент LikeButton, потому что он вызывает метод setState(). В этом случае, элемент LikeButton отмечается как грязный, и во время следующего цикла сборки build() он будет перестроен.

Как происходит обновление виджета на экране

  1. Нажимаем на IconButton → вызывается setState()
  2. StatefulElement, который хранит _LikeButtonState помечается как Dirty
  3. Flutter вызывает метод build() только для LikeButton
  4. Все виджеты внутри LikeButton пересоздаются
  5. Создаётся новый IconButton и новый Icon
  6. Их элементы Element остаются неизменными
  7. Если тип элемента совпадает с типом нового виджета (и ключи), то
  8. Элементы обновляют ссылки на новые виджеты
  9. Остальная часть UI не перерисовывается
  10. Объект состояния _LikeButtonState сохраняется

setState()

Метод setState() не уничтожает State и Element → он пересоздает виджет

Посмотрим как это работает на практике с помощью дебагера:

Debugger properties
Debugger animation

Проверим, что пересоздаются виджеты только внутри build() у виджета LikeButton

Воспользуемся методом identityHashCode. Он возвращает уникальный идентификатор конкретного экземпляра в памяти. Его нельзя переопределить, он привязан к адресу объекта в памяти.

Файл 2_stateful_widget.dart
Светлая тема Темная тема
class TrackCard extends StatelessWidget {  
  const TrackCard({super.key});  
  
  @override  
  Widget build(BuildContext context) {  
  
    final likeButton = LikeButton();  
    
    // Выводим хэш-код созданного экземпляра LikeButton
    debugPrint("🆕 LikeButton created: ${identityHashCode(likeButton)}");  
  
    return Card(  
      color: Colors.white,  
      child: ListTile(  
        leading: Image.asset("assets/images/pro.webp"),  
        title: const TitleWidget(text: "Stateless"),  
        subtitle: const SubtitleWidget(text: "Flutter vibes"),  
        trailing: LikeButton(),  
      ),  
    );  
  }  
}
Файл 2_stateful_widget.dart
Светлая тема Темная тема
class LikeButton extends StatefulWidget {  
  const LikeButton({super.key});  
  
  @override  
  State createState() => _LikeButtonState();  
}  
  
class _LikeButtonState extends State {  
  bool isFavorite = false;  
  
  @override  
  Widget build(BuildContext context) {  
    final iconButton = IconButton(  
      onPressed: () {  
        setState(() {  
          isFavorite = !isFavorite;  
        });  
      },  
      icon: isFavorite  
          ? Icon(Icons.favorite, color: Colors.red)  
          : Icon(Icons.favorite_border),  
    );  
    // Выводим хэш-код созданного экземпляра IconButton
    debugPrint("🆕 IconButton created: ${identityHashCode(iconButton)}");  
    return iconButton;  
  }  
}
flutter: 🆕 LikeButton created: 1070612665
flutter: 🆕 IconButton created: 331251001

flutter: 🆕 IconButton created: 950743870
flutter: 🆕 IconButton created: 45617114
flutter: 🆕 IconButton created: 35391228
flutter: 🆕 IconButton created: 455893635
flutter: 🆕 IconButton created: 272893660
flutter: 🆕 IconButton created: 77883252
flutter: 🆕 IconButton created: 896606673
Hashcode animation

Перестроение виджетов

Виджет LikeButton был создан только один раз!
Виджет IconButton создается каждый раз при изменении состояния!

Как работает Перестроение виджетов (слайды и анимация)

StatefulWidget - шаг 1
StatefulWidget - шаг 2
StatefulWidget - шаг 3
StatefulWidget - шаг 4
StatefulWidget - шаг 5
StatefulWidget - шаг 6
StatefulWidget - шаг 7
StatefulWidget - шаг 8
StatefulWidget - шаг 9
StatefulWidget - шаг 10
StatefulWidget - шаг 11
StatefulWidget - шаг 12
StatefulWidget - шаг 13
StatefulWidget - шаг 14
StatefulWidget - шаг 15
StatefulWidget - шаг 16
StatefulWidget - шаг 17
StatefulWidget - шаг 18
StatefulWidget - шаг 19
StatefulWidget - шаг 20
StatefulWidget - шаг 21
StatefulWidget - шаг 22
StatefulWidget - шаг 23
StatefulWidget - шаг 24

Итог

Краткие итоги

Существует дерево ЭЛЕМЕНТОВ, которое не перестраивается всякий раз, когда вызывается метод build(). Перестраивается только дерево ВИДЖЕТОВ, дерево элементов может автоматически обновлять свои ссылки на виджеты, чтобы знать, доступна ли новая конфигурация, и если она доступна, то передать эту информацию в дерево РЕНДЕРИНГА объектов, чтобы они могли повторно отображаться на экране.