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 новой конфигурации;
  • и тип конфигурации (то есть тип виджета) не изменился.

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

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

flutter_3_3.1

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

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

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

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

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

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

InheritedWidget и Element

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

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

flutter_3_3.2

Например, определим 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?

flutter_3_3.3

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

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

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

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

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

flutter_3_3.4

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

RenderObject и Element

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

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

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

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

  • insertRenderObjectChild.
  • moveRenderObjectChild.
  • removeRenderObjectChild.

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

  • attachRenderObject
  • detachRenderObject.

Давайте рассмотрим подробнее работу 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.