«Во 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и что перестраивается.