3.3 Элементы: жизненный цикл и связь с виджетами и RenderObject

В прошлом парагафе мы рассмотрели методы и поля класса Element, которые участвуют во всём, что связано с созданием, обновлением и удалением элементов. В этом — рассмотрим их жизненный цикл и связь с виджетами и RenderObject.

Жизненный цикл

У каждого элемента есть жизненный цикл. С помощью него можно понять, что виджет удалился с экрана, переместился по дереву, добавился на экран. Всего есть 4 состояния, в которых может находиться элемент:

  • initial;
  • active;
  • inactive;
  • defunct.

Состояние 1: initial

При создании элемента сразу же происходит инициализация его состояния

1abstract class Element extends DiagnosticableTree implements BuildContext {
2 ...
3 _ElementLifecycle _lifecycleState = _ElementLifecycle.initial
4 ...
5}

Первое состояние — initial  — означает, что он не встроен в дерево: он не знает ни о своих родительских элементах, ни о дочерних, ни где он находится.

Состояние 2: active

У элемента вызывается метод mount . Этот метод вызывается только у нового элемента, который ещё не был в дереве элементов. На этом этапе элемент узнаёт о своём родительском элементе, о слоте, в котором будет находиться, о глубине на которой будет находится. Также происходит получение списка зависимостей, то есть всех InheritedWidget.

И тут состояние элемента становится равным active. Также, если у виджета был глобальный ключ, происходит его регистрация, то есть процесс сохранения.

После этого этапа нам становится доступна подписка на InheritedWidget.

Состояние 3: inactive

Состояние inactive наступает, когда элемент становится недоступным для использования.

Это происходит при:

  • удалении виджета из дерева — например, закрыли страничку в приложении;
  • перемещении виджета по дереву — например, когда в приложении списка дел поменяли местами две задачи;
  • поступлении нового типа конфигурации элемента — например, был StatelessWidget, а стал StatefulWidget.

На этой стадии очищаются все зависимости. При всём этом мы можем попробовать переиспользовать элемент, если:

  • у конфигурации есть GlobalKey и он совпадает с GlobalKey новой конфигурации;
  • и тип конфигурации (то есть тип виджета) не изменился.

В остальных случаях создаётся новый элемент.

Если мы можем попробовать переиспользовать элемент, то происходит следующее:

flutter

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

Состояние 4: defunct

Это последняя стадия элемента. Она наступает, только когда происходит удаление элемента из дерева и только после того, как элемент был деактивирован (т. е. находился в состоянии inactive).

Здесь уже происходит удаление глобального ключа, который был связан с текущим элементом, и очищение всех переменных, дабы избежать утечек памяти, когда элемент случайным образом где-то сохранился.

Чтобы показать, что элемент больше не нужен и должен быть удалён, происходит вызов метода unmount  у этого самого элемента. Данный вызов происходит в классе _InactiveElements, когда завершается фаза построения. Именно в этот момент фреймворк проходится по списку неактивных элементов и удаляет их навсегда без повторного использования.

Теперь, когда мы рассмотрели базу элементов, можно перейти к тому, как происходит взаимодействие InheritedWidget и Element.

InheritedWidget и Element

InheritedWidget — это конфигурация для InheritedElement, а сам InheritedElement — это ProxyElement. И всё это означает, что InheritedWidget позволяет хранить данные, которые будут доступны для всех дочерних виджетов.

Дублируем диаграмму наследования, которую приводили пару параграфов назад, чтобы вам было удобнее ориентироваться.

flutter

Например, определим InhertiedWidget. Он будет хранить количество лайков:

1class LikesInheritedWidget extends InheritedWidget {
2  /// Количество лайков
3  final int _amountLikes;
4
5  const LikesInheritedWidget({
6    super.key,
7    int initialLikes = 0, // начальное количество лайков
8    required super.child,
9  }) : _amountLikes = initialLikes;
10
11  int get amountLikes => _amountLikes;
12
13  /// Говорим, что конфигурация изменилась, 
14  /// только если изменилось количество лайков
15  @override
16  bool updateShouldNotify(covariant LikesInheritedWidget oldWidget) {
17    return amountLikes != oldWidget.amountLikes;
18  }
19
20  /// **Подписываемся** на изменения данного виджета
21  /// Если поменяется количество лайков, то виджет, который вызвал этот метод,
22  /// перестроится с новыми данными (количеством лайков)
23  static LikesInheritedWidget? of(BuildContext context) {
24    /// [dependOnInheritedWidgetOfExactType] ищет ближайший вверх по дереву 
25    /// виджет с указанным типом, в нашем случае это [LikesInheritedWidget],
26    /// и говорит виджету из `context`, что нужно перестроиться, если поменяется
27    /// количество лайков
28    final widget =
29        context.dependOnInheritedWidgetOfExactType<LikesInheritedWidget>();
30
31    return widget;
32  }
33}

Теперь создадим виджет, который будет отображать количество лайков:

1class LikesWidget extends StatelessWidget {
2  const LikesWidget({Key? key}) : super(key: key);
3
4  @override
5  Widget build(BuildContext context) {
6    /// NOTIFIER
7    final likes = LikesInheritedWidget.of(context)?.likes ?? 0;
8
9    return Text('Likes $likes');
10  }
11}

Теперь, когда мы определили нужные виджеты, можно рассмотреть более детально, что же тут происходит.

В методе build вызывается статический метод of у LikesInheritedWidget, который в свою очередь вызывает dependOnInheritedWidgetOfExactType у переданного контекста (элемента).

Но что происходит при вызове метода dependOnInheritedWidgetOfExactType?

1@override
2T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
3  /// Смотрим список InheritedWidget у текущего элемента
4  /// Если их нет, то и элемента для виджета T нет
5  final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
6  if (ancestor != null) {
7     /// Если искомый элемент нашего виджета T найден, то вызывается этот метод
8     /// Туда мы передаём найденный элемент и [aspect].
9     /// [aspect] — это свойство, которое необходимо InheritedModel.
10     /// Благодаря ему мы можем понимать, что нужно перестроить те виджеты,
11     /// которые сослались на это свойство, если оно изменилось у InheritedModel
12     return dependOnInheritedElement(ancestor, aspect: aspect) as T;
13  }
14
15  /// Если же искомого элемента для виджета T нет, а мы пытаемся на него сослаться,
16  /// то фреймворк помечает, что есть зависимость, 
17  /// для которой не нашлось искомого виджета
18  _hadUnsatisfiedDependencies = true;
19  return null;
20}

Теперь заглянем внутрь dependOnInheritedElement и ещё чуть позже рассмотрим, откуда берётся список, в котором мы ищем нужный нам InheritedWidget.

1@override
2InheritedWidget dependOnInheritedElement(InheritedElement ancestor, {Object? aspect}) {
3  /// Если списка зависимостей нет, то создаём его
4  /// Этот список содержит элементы, на которые мы подписались
5  _dependencies ??= HashSet<InheritedElement>();
6  /// Добавляем в список зависимостей найденный нами элемент для виджета T,
7  /// который наследуется от InheritedWidget, в нашем случае LikesInheritedWidget
8  _dependencies!.add(ancestor);
9  /// Теперь обновляем список зависимых элементов у InheritedElement,
10  /// добавляя туда наш текущий элемент
11  ancestor.updateDependencies(this, aspect);
12  /// И возвращаем наш LikesInheritedWidget
13  return ancestor.widget as InheritedWidget;
14}

Метод updateDependencies просто вызывает другой метод setDependencies. А тот в свою очередь просто добавляет наш элемент виджета LikesWidget в список зависимых элементов. value — это aspect, который упомянут выше.

1void setDependencies(Element dependent, Object? value) {
2  _dependents[dependent] = value;
3}

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

А что насчёт списка зависимостей (InheritedWidget)? Давайте изучим _inheritedWidgets. Это Map, ключи которой — тип виджета, а значение — элемент виджета. Она просто хранит все родительские InheritedElements.

Обычные элементы просто сохраняют ссылку на эту мапу к себе.

1/// Именно ссылаются на мапу, а не копируют её
2_inheritedWidgets = _parent?._inheritedWidgets;

А вот InheritedElement создаёт эту мапу либо копирует её и добавляет туда себя, когда вызывается метод _updateInheritance.

1@override
2void _updateInheritance() {
3  final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
4  if (incomingWidgets != null) {
5    /// копирует мапу родителя
6    _inheritedWidgets = HashMap<Type, InheritedElement>.of(incomingWidgets);
7  } else {
8   /// Создаёт новую, если выше по дереву это первый InheritedElement
9    _inheritedWidgets = HashMap<Type, InheritedElement>();
10  }
11  /// Добавляет себя в эту мапу
12  _inheritedWidgets![widget.runtimeType] = this;
13}

Из этого можно сделать вывод, что все зависимости можно получить быстро за O(1), так как они копируются/ссылаются для каждого элемента.

Мы рассмотрели, как происходит подписка на изменение InheritedWidget, а что же происходит при обновлении данных InheritedWidget?

flutter

Допустим, по какой-то причине обновились данные у InheritedWidget. Например, мы поставили лайк и где-то вызвалась смена состояния, в итоге в InheritedWidget попало другое количество лайков.

Происходит обновление дочернего элемента, т. е. элемента для виджета LikesInheritedWidget. Для этого элемента происходит вызов метода update, который вызывает updated.

updated вызывает определённый у нашего LikesInheritedWidget метод updateShouldNotify. Если число лайков обновилось, то вызывает родительский updated. Он в свою очередь вызывает метод notifyClients, который сообщает всем зависимостям из списка _dependencies, что обновилось количество лайков.

Делается это с помощью вызова метода notifyDependent, который вызывает у зависимого элемента метод didChangeDependencies. А далее didChangeDependencies вызывает метод markNeedsBuild, который помечает зависимые элементы как требующие перерисовки.

Для наглядности продемонстрируем процесс вызовов диаграммой:

flutter

Теперь, когда мы рассмотрели взаимодействие InheritedWidget и Element, можем перейти к не менее важной паре — RenderObject и Element.

RenderObject и Element

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

Виджеты, которые могут компоновать (например, ColumnRow и пр.) и отрисовывать, содержат метод createRenderObject. Он создаёт уже знакомый нам RenderObject.

Такие виджеты наследуются от класса RenderObjectWidget. И также содержат метод, который создаёт особый элемент RenderObjectElement.

RenderObjectElement содержит новые методы, отличные от других элементов:

  • insertRenderObjectChild.
  • moveRenderObjectChild.
  • removeRenderObjectChild.

А также переопределяют методы:

  • attachRenderObject
  • detachRenderObject.

Давайте рассмотрим подробнее работу attachRenderObject, который добавляет RenderObject в дерево рендеринга при mount элемента:

1@override
2void attachRenderObject(...) {
3 ...
4  _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
5 _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, ...);
6  ...
7}

Мы тут видим, что происходит поиск родительского RenderObjectElement. У родительского элемента вызывается метод insertRenderObjectChild, который вставляет RenderObject текущего элемента в родительский RenderObject.

Метод detachRenderObject вызывается при деактивации элемента. Одновременно с этим метод удаляет RenderObject из дерева рендеринга (при условии, что он уже находится там). Удаление RenderObject происходит с помощью вызова removeRenderObjectChild.

1@override
2  void detachRenderObject() {
3    if (_ancestorRenderObjectElement != null) {
4      _ancestorRenderObjectElement!.removeRenderObjectChild(renderObject, ...);
5      _ancestorRenderObjectElement = null;
6    }
7    ...
8  }

Метод moveRenderObjectChild вызывается у виджетов, содержащих множество дочерних, например виджет Column или Row, при перемещении внутри списка children. И служит для того, чтобы поместить RenderObject в новый слот.


Давайте подведём небольшой итог.

Мы узнали что Widget — это лишь конфигурация для Element, а сам Element — это реализация этого виджета (конфигурации). Мы рассмотрели, какие бывают типы элементов, и изучили процесс их создания. А заодно рассмотрели, как связаны элементы, виджеты и RenderObject.

Теперь вы сможете ответить на сложные вопросы об устройстве виджетов на собеседованиях 😃

А в следующем параграфе мы поговорим о сервисах связи (Bindings), которые выполняют роль клея между между фреймворком и движком Flutter.

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E
Предыдущий параграф3.2. Элементы: методы и поля класса Element
Следующий параграф3.4. Сервисы связи