Элементы: методы и поля класса Element

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

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

Поля

Их разберём совсем кратко, просто чтобы обозначить.

  • Widget — конфигурация, которая отвечала за создание элемента.
  • Owner — сущность, которая управляет билдом, жизненным циклом элемента.
  • RenderObject — ближайший RenderObject. Это RenderObject текущего элемента либо RenderObject одного из дочерних элементов. Если RenderObject не найден, то будет null.

Методы

А вот тут углубимся. В этой главке подробно разберём следующие методы:

  • reassemble
  • visitChildren
  • updateChild
  • inflateWidget
  • mount
  • _retakeInactiveElement
  • update
  • _activateWithParent
  • deactivateChild
  • attachRenderObject
  • detachRenderObject
  • activate
  • deactivate
  • unmount
  • markNeedsBuild
  • rebuild
  • performRebuild

reassemble

Этот метод вызывается всякий раз, когда приложение собирается повторно в режиме дебага — например, во время Hot Reload. Позволяет выполнить какую-либо инициализирующую логику, например загрузку изображений из ассетов.

visitChildren

Метод, который переопределяют классы-наследники для прохода по дочерним элементам.

Принимает в качестве аргумента колбэк, который вызывается для каждого ребёнка.

Например, чтобы рекурсивно убрать RenderObject для каждого ребёнка
1void detachRenderObject() {
2    visitChildren((Element child) {
3      child.detachRenderObject();
4    });
5  ...
6  }

updateChild

Этот метод служит для обновления дочернего элемента — и возвращает этот самый дочерний элемент. Вызывается, когда происходит добавление/удаление/обновление элемента.

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

Для начала — вот как он устроен
1Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
2  /// Виджета больше нет, и происходит деактивация элемента
3    if (newWidget == null) {
4      if (child != null) {
5        deactivateChild(child);
6      }
7      return null;
8    }
9
10    final Element newChild;
11  /// Дочерний элемент уже существовал, а значит, далее может быть
12    /// либо его обновление, либо удаление
13    if (child != null) {
14      bool hasSameSuperclass = true;
15      assert(() {
16        final int oldElementClass = Element._debugConcreteSubtype(child);
17        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
18        hasSameSuperclass = oldElementClass == newWidgetClass;
19        return true;
20      }());
21
22      /// Элемент совпадает с виджетом, и конфигурации одинаковы, а значит,
23      /// просто отрисовываем, не меняя ничего
24      if (hasSameSuperclass && child.widget == newWidget) {
25        if (child.slot != newSlot) {
26          updateSlotForChild(child, newSlot);
27        }
28        newChild = child;
29      } else if (hasSameSuperclass &&
30          Widget.canUpdate(child.widget, newWidget)) {
31        if (child.slot != newSlot) {
32          updateSlotForChild(child, newSlot);
33        }
34    ...
35    /// Если конфигурации различны, то обновляем наш элемент
36        child.update(newWidget);
37        newChild = child;
38      } else {
39    /// Если тип элемента не совпадает с типом виджета, то 
40    /// удаляем элемент и создаём новый, под переданный [newWidget]
41        deactivateChild(child);
42        assert(child._parent == null);
43        newChild = inflateWidget(newWidget, newSlot);
44      }
45    } else {
46      /// Создаём элемент, так как до этого его не существовало
47      newChild = inflateWidget(newWidget, newSlot);
48    }
49
50    return newChild;
51  }

Как видите, метод принимает три параметра:

  1. Element? child — дочерний элемент.
  2. Widget? newWidget — новую конфигурацию для элемента.
  3. Object? newSlot — новый слот (место в дереве элементов), где будет располагаться элемент.

Этот метод также может быть вызван другим методом — inflateWidget.

У этого метода существует несколько сценариев выполнения. Они представлены на диаграмме ниже.

flutter

Теперь разберём их чуть подробнее.

  1. Если newWidget == null (это значит, что виджета больше нет), то child деактивируется, и вернётся null.
  2. Если child == null && newWidget ≠ null (означает, что появился новый виджет), то происходит вызов метода inflateWidget, в результате чего создаётся новый элемент, который будет использоваться в качестве дочернего.
  3. Если child ≠ null && newWidget ≠ null, то произойдёт то, о чём мы расскажем дальше.

Во-первых, происходит проверка на то, что нам была передана конфигурация типа, который соответствует типу элемента (под типом имеется в виду StatelessWidgetStatefulWidget или другие названия виджетов/элементов), результат сравнения лежит в переменной hasSameSuperclass. Но эта проверка действительна только в режиме дебага.

Можем взглянуть на код:
```dart
assert(() {
   //Здесь просто определяется тип элемента: 1 — StatefulElement, 2 — StatelessElement, 0 — другой
   final int oldElementClass = Element._debugConcreteSubtype(child);
   //Здесь просто определяется тип виджета: 1 — StatefulWidget, 2 — StatelessWidget, 0 — другой
   final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
   // Здесь происходит сравнение, что нужный элемент соотносится с соответствующей ему конфигурацией
   hasSameSuperclass = oldElementClass == newWidgetClass;
   
   return true;
}());
```

Если вышло так, что конфигурация и элемент не совпадают, то происходит деактивация дочернего элемента и создание нового дочернего элемента в методе inflateWidget.

После проверки происходит сравнение новой и старой конфигураций.

  • Если конфигурации равны, то происходит переиспользование дочернего элемента.
  • Если конфигурации разные, но одного типа, то происходит вызов метода canUpdate у виджета дочернего элемента, и если мы можем обновить конфигурацию, то элемент помещается в новый слот, если таковой задан. А у child обновляется конфигурация с помощью вызова метода update дочернего элемента и передачи в этот метод новой конфигурации. На этом этот метод завершает свою работу.
  • Если различаются типы конфигураций, то child деактивируется и на его месте создаётся новый, как в сценарии номер 2.

inflateWidget

Этот метод создает дочерний элемент для заданного виджета.

Вот как он устроен внутри
1Element inflateWidget(Widget newWidget, Object? newSlot) {
2 ...
3  try {
4    final Key? key = newWidget.key;
5    if (key is GlobalKey) {
6      final Element? newChild = _retakeInactiveElement(key, newWidget);
7      if (newChild != null) {
8        ...
9        newChild._activateWithParent(this, newSlot);
10        final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
11    ...
12        return updatedChild!;
13      }
14    }
15  /// А вот тут и сам элемент — создаёт другой элемент
16    final Element newChild = newWidget.createElement();
17    ...
18    newChild.mount(this, newSlot);
19    ...
20    return newChild;
21  } finally {
22    ...
23  }
24}

У него есть есть два параметра:

  1. Widget? newWidget — конфигурация для дочернего элемента.
  2. Object? newSlot — слот для элемента.

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

Если элемент будет найден для заданного виджета, то он будет заново переиспользован (вызов методов _activateWithParent и updateChild с новой конфигурацией).

Может показаться, что методы updateChild и inflateWidget используют друг друга циклично, но на самом деле updateChild просит inflateWidget достать уже существующий элемент и вернуть его с новой конфигурацией либо создать новый элемент.

Если же элемент не будет найден или ключ не будет задан, то будет создан новый элемент для newWidget. И после этого будет вызван метод mount для дочернего элемента.

Вышеописанное можно представить в виде диаграммы:

flutter

mount

Метод, в котором происходит встраивание текущего элемента в дерево.

Он принимает родительский элемент и слот для размещения. Если виджет имел GlobalKey, то происходит привязка текущего элемента с глобальным ключом. Данные об этом заносятся в BuildOwner (пока не будем рассматривать что это за сущность, просто знайте, что она хранит все глобальные ключи).

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

_retakeInactiveElement

Этот метод вызывается родителем для дочернего элемента и возвращает неактивный элемент по переданному ключу. Он принимает два параметра: глобальный ключ и новую конфигурацию.

Вот как он устроен внутри
1Element? _retakeInactiveElement(GlobalKey key, Widget newWidget) {
2 final Element? element = key._currentElement;
3  if (element == null) {
4    return null;
5  }
6  if (!Widget.canUpdate(element.widget, newWidget)) {
7    return null;
8  }
9  final Element? parent = element._parent;
10  if (parent != null) {
11    assert(() {
12      if (parent == this) {
13       /// Тут происходит выбрасывание всем известной ошибки при работе с
14       /// глобальными ключами:
15       /// "A GlobalKey was used multiple times inside
16    /// one widget's child list."
17      }
18      return true;
19    }());
20    parent.forgetChild(element);
21    parent.deactivateChild(element);
22  }
23  assert(element._parent == null);
24  owner!._inactiveElements.remove(element);
25 return element;
26}

В начале метода мы пытаемся достать элемент из ключа. Если ключ не содержит элемента или виджет не может быть обновлён, то данный метод вернет null.

Далее мы достаём родительский элемент из элемента в ключе. Если родитель существует, то это означает, что какой-то другой виджет уже использует этот глобальный ключ, и происходит выброс ошибки A GlobalKey was used multiple times inside one widget's child list.

Если же родителя нет, то элемент удаляется из списка неактивных (owner!._inactiveElements.remove(element)) и возвращается.

update

Метод, который обновляет конфигурацию текущего элемента, если конфигурация того же типа (см. метод updateChild). Здесь нет ничего особенного, просто текущему элементу присваивается в поле _widget новая конфигурация.

1void update(covariant Widget newWidget) {
2    _widget = newWidget;    
3}

_activateWithParent

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

deactivateChild

Этот метод деактивирует дочерний элемент и помещает его в список неактивных элементов. В этом же методе происходит отсоединение элемента от его родителя и вызывается метод detachRenderObject.

attachRenderObject

В этом методе происходит встраивание текущего RenderObject в дерево рендеринга.

detachRenderObject

В данном методе происходит удаление RenderObject из дерева рендеринга

activate

Здесь элемент меняет свое состояние на active.

Вот как он устроен внутри
1 void activate() {
2    // Смена состояния элемента, о жизненном цикле мы поговорим чуть позже
3    _lifecycleState = _ElementLifecycle.active;
4    // Тут происходит чистка зависимостей. 
5    // Под зависимостями понимается связь между Inherited-элементами и текущим
6    _dependencies?.clear();
7    _hadUnsatisfiedDependencies = false;
8    _updateInheritance();
9    // Если элемент помечен как «грязный», то есть у него обновилась конфигурация 
10    // или, к примеру, мы вызвали метод setState(), 
11    // то мы говорим, что требуется сбилдить заново этот элемент (для упрощения — виджет)
12    if (_dirty) {
13      owner!.scheduleBuildFor(this);
14    }
15    // А вот тут происходит вызов метода didChangeDependencies у State, 
16    // который связан со StatefulWidget.
17    if (hadDependencies) {
18      didChangeDependencies();
19    }
20}

deactivate

Вызов этого метода сделает элемент неактивным, то есть он просто не сможет быть использован для построения, а соответственно, отрисован. Обычно вызывается, когда активный элемент поместился в список деактивированных и потащил за собой все дочерние элементы. Если посмотреть на его код, то мы там просто увидим отвязку от всех Inherited и смену состояния:

1void deactivate() {
2    if (_dependencies != null && _dependencies!.isNotEmpty) {
3        for (final InheritedElement dependency in _dependencies!) {
4            dependency._dependents.remove(this);
5        }
6    }
7    _inheritedWidgets = null; 
8    _lifecycleState = _ElementLifecycle.inactive;
9}

unmount

Метод вызывается, когда элемент больше нигде не нужен. Здесь происходит отвязка конфигурации и зависимостей, чтобы не было утечек в памяти, если элемент вдруг сохранился. А также его состояние меняется на defunct.

markNeedsBuild

Метод помечает текущий элемент как «грязный», который следует построить заново. И говорит, что ему нужно обновиться в следующем кадре. Является ли элемент «грязным», можно узнать, посмотрев на поле _dirty у этого элемента.

rebuild

Данный метод вызывается такой сущностью, как BuildOwner, когда элемент впервые создаётся, а также когда вызывается метод update. При этом этот метод вызывает другой performRebuild.

performRebuild

Вызывает метод build у виджета и обновляет или создаёт элемент для него. Когда мы создаём виджет и определяем у него метод, этот метод возвращает нам дочерние виджеты. И именно под дочерние виджеты мы создаём элементы.

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

flutter


Отлично, с полями и методами класса Element познакомились. Как видите, никакой магии тут нет.

А следующем параграфе мы рассмотрим жизненный цикл элементов, а также взаимодействие элементов с InheritedWidget и RenderObject.

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф3.1. Элементы: первое погружение
Следующий параграф3.3. Элементы: жизненный цикл и связь с виджетами и RenderObject