Урок по треку Flutter и Dart 8 мин

Виджеты, элементы и рендер-объекты

Widget, Element, RenderObject, BuildContext, Stateless и Stateful, keys.

«Во Flutter всё виджет» верно лишь наполовину. Виджет это лёгкое неизменяемое описание. Чтобы картинка появилась на экране, Flutter держит три дерева, и вопрос «расскажите про три дерева» звучит почти на каждом собесе middle и выше.

Три дерева

ДеревоЧто этоЖивёт
Widgetконфигурация: «здесь текст такого цвета»пересоздаётся при каждом build
Elementэкземпляр виджета в конкретном месте дерева, связывает виджет с рендер-объектом и держит Stateдолго, переиспользуется
RenderObjectразмеры, раскладка, отрисовка, hit testдолго, дорогой

Когда вызывается build, создаются новые виджеты. Элемент сравнивает новый виджет со старым: если совпадают runtimeType и key (это проверяет Widget.canUpdate), элемент остаётся, получает новый виджет и обновляет рендер-объект. Если нет, старое поддерево выбрасывается и строится новое, вместе с State.

Поэтому пересоздавать виджеты дёшево: это просто объекты в памяти. Дорого то, что ниже: раскладка и отрисовка, и Flutter их по возможности переиспользует.

BuildContext

BuildContext это и есть Element, только через ограниченный интерфейс. Он знает своё место в дереве, поэтому через него ищут предков:

Theme.of(context);            // ближайший Theme выше
Navigator.of(context).push(...);
MediaQuery.sizeOf(context);

Ловушка: контекст виджета, внутри build которого создан Scaffold, находится выше этого Scaffold. Scaffold.of(context) в том же build его не найдёт. Лечится виджетом Builder или выносом части в отдельный виджет, у которого свой контекст ниже.

Ещё ловушка: использовать context после await. Виджет мог быть уже удалён из дерева. Проверяйте if (!context.mounted) return; (в State есть mounted).

StatelessWidget и StatefulWidget

StatelessWidget только описывает интерфейс по входным параметрам. StatefulWidget состоит из двух объектов: сам виджет (неизменяемый) и State, который живёт столько же, сколько элемент. Состояние хранится в State, а не в виджете: виджет пересоздаётся при каждом перестроении родителя, State сохраняется.

setState(fn) выполняет fn синхронно и помечает элемент «грязным». На следующем кадре вызовется build у этого State, и перестроится его поддерево, а не всё приложение.

Keys

Ключ помогает элементу понять, какой новый виджет соответствует старому.

ListView(children: [
  for (final t in todos) TodoTile(key: ValueKey(t.id), todo: t),
]);

Без ключей сопоставление идёт по позиции. Удалили первый элемент списка, и State первого (например, отмеченный чекбокс или введённый текст) достанется бывшему второму. Это классический баг.

  • ValueKey, ObjectKey: уникальны среди соседей, для списков.
  • UniqueKey: новый каждый раз, принудительно пересоздаёт элемент и сбрасывает состояние.
  • GlobalKey: уникален во всём приложении, даёт доступ к State и context (formKey.currentState!.validate()) и позволяет перенести поддерево с сохранением состояния. Дорогой, без нужды не используют.

const-виджеты

const Text('Заголовок') это один и тот же экземпляр при каждом build. Flutter видит идентичный виджет и не заходит в его поддерево. Это самая дешёвая оптимизация перестроений.

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

  • Хранить состояние в полях StatefulWidget, а не State.
  • Генерировать UniqueKey() в build: состояние сбрасывается на каждом кадре.
  • Ключ по индексу (ValueKey(index)) в списке, где элементы удаляются и сортируются.
  • context после await без проверки mounted.

Что спросят на собесе

  • Три дерева: зачем их три, какое дорогое.
  • Что такое BuildContext, почему Scaffold.of не находит Scaffold.
  • Как Flutter решает, переиспользовать элемент или создать новый.
  • Зачем ключи, какие бывают, когда нужен GlobalKey.
  • Что делает setState и что перестраивается.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.