3.3. Элементы: жизненный цикл и связь с виджетами и RenderObject
Автор: Никита Березовский
В прошлом парагафе мы рассмотрели методы и поля класса Element, которые участвуют во всём, что связано с созданием, обновлением и удалением элементов. В этом — рассмотрим их жизненный цикл и связь с виджетами и RenderObject.
Жизненный цикл
У каждого элемента есть жизненный цикл. С помощью него можно понять, что виджет удалился с экрана, переместился по дереву, добавился на экран. Всего есть 4 состояния, в которых может находиться элемент:
initial;active;inactive;defunct.
Состояние 1: initial
При создании элемента сразу же происходит инициализация его состояния
abstract class Element extends DiagnosticableTree implements BuildContext {
...
_ElementLifecycle _lifecycleState = _ElementLifecycle.initial
...
}
Первое состояние — initial — означает, что он не встроен в дерево: он не знает ни о своих родительских элементах, ни о дочерних, ни где он находится.
Состояние 2: active
У элемента вызывается метод mount . Этот метод вызывается только у нового элемента, который ещё не был в дереве элементов. На этом этапе элемент узнаёт о своём родительском элементе, о слоте, в котором будет находиться, о глубине на которой будет находится. Также происходит получение списка зависимостей, то есть всех InheritedWidget.
И тут состояние элемента становится равным active. Также, если у виджета был глобальный ключ, происходит его регистрация, то есть процесс сохранения.
После этого этапа нам становится доступна подписка на InheritedWidget.
Состояние 3: inactive
Состояние inactive наступает, когда элемент становится недоступным для использования.
Это происходит при:
- удалении виджета из дерева — например, закрыли страничку в приложении;
- перемещении виджета по дереву — например, когда в приложении списка дел поменяли местами две задачи;
- поступлении нового типа конфигурации элемента — например, был
StatelessWidget, а сталStatefulWidget.
На этой стадии очищаются все зависимости. При всём этом мы можем попробовать переиспользовать элемент, если:
- у конфигурации есть
GlobalKeyи он совпадает сGlobalKeyновой конфигурации; - и тип конфигурации (то есть тип виджета) не изменился.
В остальных случаях создаётся новый элемент.
Если мы можем попробовать переиспользовать элемент, то происходит следующее:
Неактивный элемент помещается в список к остальным таким же элементам. Этот список находится в классе _InactiveElements. Когда элемент добавляется в этот список, все его дочерние элементы деактивируются рекурсивно.
Состояние 4: defunct
Это последняя стадия элемента. Она наступает, только когда происходит удаление элемента из дерева и только после того, как элемент был деактивирован (т. е. находился в состоянии inactive).
Здесь уже происходит удаление глобального ключа, который был связан с текущим элементом, и очищение всех переменных, дабы избежать утечек памяти, когда элемент случайным образом где-то сохранился.
Чтобы показать, что элемент больше не нужен и должен быть удалён, происходит вызов метода unmount у этого самого элемента. Данный вызов происходит в классе _InactiveElements, когда завершается фаза построения. Именно в этот момент фреймворк проходится по списку неактивных элементов и удаляет их навсегда без повторного использования.
Теперь, когда мы рассмотрели базу элементов, можно перейти к тому, как происходит взаимодействие InheritedWidget и Element.
InheritedWidget и Element
InheritedWidget — это конфигурация для InheritedElement, а сам InheritedElement — это ProxyElement. И всё это означает, что InheritedWidget позволяет хранить данные, которые будут доступны для всех дочерних виджетов.
Дублируем диаграмму наследования, которую приводили пару статей назад, чтобы вам было удобнее ориентироваться.
Например, определим InhertiedWidget. Он будет хранить количество лайков:
class LikesInheritedWidget extends InheritedWidget {
/// Количество лайков
final int _amountLikes;
const LikesInheritedWidget({
super.key,
int initialLikes = 0, // начальное количество лайков
required super.child,
}) : _amountLikes = initialLikes;
int get amountLikes => _amountLikes;
/// Говорим, что конфигурация изменилась,
/// только если изменилось количество лайков
@override
bool updateShouldNotify(covariant LikesInheritedWidget oldWidget) {
return amountLikes != oldWidget.amountLikes;
}
/// **Подписываемся** на изменения данного виджета
/// Если поменяется количество лайков, то виджет, который вызвал этот метод,
/// перестроится с новыми данными (количеством лайков)
static LikesInheritedWidget? of(BuildContext context) {
/// [dependOnInheritedWidgetOfExactType] ищет ближайший вверх по дереву
/// виджет с указанным типом, в нашем случае это [LikesInheritedWidget],
/// и говорит виджету из `context`, что нужно перестроиться, если поменяется
/// количество лайков
final widget =
context.dependOnInheritedWidgetOfExactType<LikesInheritedWidget>();
return widget;
}
}
Теперь создадим виджет, который будет отображать количество лайков:
class LikesWidget extends StatelessWidget {
const LikesWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
/// NOTIFIER
final likes = LikesInheritedWidget.of(context)?.likes ?? 0;
return Text('Likes $likes');
}
}
Теперь, когда мы определили нужные виджеты, можно рассмотреть более детально, что же тут происходит.
В методе build вызывается статический метод of у LikesInheritedWidget, который в свою очередь вызывает dependOnInheritedWidgetOfExactType у переданного контекста (элемента).
Но что происходит при вызове метода dependOnInheritedWidgetOfExactType?
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
/// Смотрим список InheritedWidget у текущего элемента
/// Если их нет, то и элемента для виджета T нет
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
/// Если искомый элемент нашего виджета T найден, то вызывается этот метод
/// Туда мы передаём найденный элемент и [aspect].
/// [aspect] — это свойство, которое необходимо InheritedModel.
/// Благодаря ему мы можем понимать, что нужно перестроить те виджеты,
/// которые сослались на это свойство, если оно изменилось у InheritedModel
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
/// Если же искомого элемента для виджета T нет, а мы пытаемся на него сослаться,
/// то фреймворк помечает, что есть зависимость,
/// для которой не нашлось искомого виджета
_hadUnsatisfiedDependencies = true;
return null;
}
Теперь заглянем внутрь dependOnInheritedElement и ещё чуть позже рассмотрим, откуда берётся список, в котором мы ищем нужный нам InheritedWidget.
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, {Object? aspect}) {
/// Если списка зависимостей нет, то создаём его
/// Этот список содержит элементы, на которые мы подписались
_dependencies ??= HashSet<InheritedElement>();
/// Добавляем в список зависимостей найденный нами элемент для виджета T,
/// который наследуется от InheritedWidget, в нашем случае LikesInheritedWidget
_dependencies!.add(ancestor);
/// Теперь обновляем список зависимых элементов у InheritedElement,
/// добавляя туда наш текущий элемент
ancestor.updateDependencies(this, aspect);
/// И возвращаем наш LikesInheritedWidget
return ancestor.widget as InheritedWidget;
}
Метод updateDependencies просто вызывает другой метод setDependencies. А тот в свою очередь просто добавляет наш элемент виджета LikesWidget в список зависимых элементов. value — это aspect, который упомянут выше.
void setDependencies(Element dependent, Object? value) {
_dependents[dependent] = value;
}
Всё это лишь говорит, что дочерние виджеты добавили себя в список зависимых виджетов InheritedWidget, а также пометили для себя, что они зависят от указанного InheritedWidget.
А что насчёт списка зависимостей (InheritedWidget)? Давайте изучим _inheritedWidgets. Это Map, ключи которой — тип виджета, а значение — элемент виджета. Она просто хранит все родительские InheritedElements.
Обычные элементы просто сохраняют ссылку на эту мапу к себе.
/// Именно ссылаются на мапу, а не копируют её
_inheritedWidgets = _parent?._inheritedWidgets;
А вот InheritedElement создаёт эту мапу либо копирует её и добавляет туда себя, когда вызывается метод _updateInheritance.
@override
void _updateInheritance() {
final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null) {
/// копирует мапу родителя
_inheritedWidgets = HashMap<Type, InheritedElement>.of(incomingWidgets);
} else {
/// Создаёт новую, если выше по дереву это первый InheritedElement
_inheritedWidgets = HashMap<Type, InheritedElement>();
}
/// Добавляет себя в эту мапу
_inheritedWidgets![widget.runtimeType] = this;
}
Из этого можно сделать вывод, что все зависимости можно получить быстро за O(1), так как они копируются/ссылаются для каждого элемента.
Мы рассмотрели, как происходит подписка на изменение InheritedWidget, а что же происходит при обновлении данных InheritedWidget?
Допустим, по какой-то причине обновились данные у InheritedWidget. Например, мы поставили лайк и где-то вызвалась смена состояния, в итоге в InheritedWidget попало другое количество лайков.
Происходит обновление дочернего элемента, т. е. элемента для виджета LikesInheritedWidget. Для этого элемента происходит вызов метода update, который вызывает updated.
updated вызывает определённый у нашего LikesInheritedWidget метод updateShouldNotify. Если число лайков обновилось, то вызывает родительский updated. Он в свою очередь вызывает метод notifyClients, который сообщает всем зависимостям из списка _dependencies, что обновилось количество лайков.
Делается это с помощью вызова метода notifyDependent, который вызывает у зависимого элемента метод didChangeDependencies. А далее didChangeDependencies вызывает метод markNeedsBuild, который помечает зависимые элементы как требующие перерисовки.
Для наглядности продемонстрируем процесс вызовов диаграммой:
Теперь, когда мы рассмотрели взаимодействие InheritedWidget и Element, можем перейти к не менее важной паре — RenderObject и Element.
RenderObject и Element
Не все виджеты могут рисовать что-то. Некоторые виджеты просто содержат другие виджеты, вторые компонуют эти виджеты, третьи рисуют что-либо — например, красный овал.
Виджеты, которые могут компоновать (например, Column, Row и пр.) и отрисовывать, содержат метод createRenderObject. Он создаёт уже знакомый нам RenderObject.
Такие виджеты наследуются от класса RenderObjectWidget. И также содержат метод, который создаёт особый элемент RenderObjectElement.
RenderObjectElement содержит новые методы, отличные от других элементов:
insertRenderObjectChild.moveRenderObjectChild.removeRenderObjectChild.
А также переопределяют методы:
attachRenderObjectdetachRenderObject.
Давайте рассмотрим подробнее работу attachRenderObject, который добавляет RenderObject в дерево рендеринга при mount элемента:
@override
void attachRenderObject(...) {
...
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, ...);
...
}
Мы тут видим, что происходит поиск родительского RenderObjectElement. У родительского элемента вызывается метод insertRenderObjectChild, который вставляет RenderObject текущего элемента в родительский RenderObject.
Метод detachRenderObject вызывается при деактивации элемента. Одновременно с этим метод удаляет RenderObject из дерева рендеринга (при условии, что он уже находится там). Удаление RenderObject происходит с помощью вызова removeRenderObjectChild.
@override
void detachRenderObject() {
if (_ancestorRenderObjectElement != null) {
_ancestorRenderObjectElement!.removeRenderObjectChild(renderObject, ...);
_ancestorRenderObjectElement = null;
}
...
}
Метод moveRenderObjectChild вызывается у виджетов, содержащих множество дочерних, например виджет Column или Row, при перемещении внутри списка children. И служит для того, чтобы поместить RenderObject в новый слот.
Давайте подведём небольшой итог.
Мы узнали что Widget — это лишь конфигурация для Element, а сам Element — это реализация этого виджета (конфигурации). Мы рассмотрели, какие бывают типы элементов, и изучили процесс их создания. А заодно рассмотрели, как связаны элементы, виджеты и RenderObject.
Теперь вы сможете ответить на сложные вопросы об устройстве виджетов на собеседованиях 😃
А в следующей статье мы поговорим о сервисах связи (Bindings), которые выполняют роль клея между между фреймворком и движком Flutter.