В прошлом параграфе мы начали разговор об элементах. Выяснили, что элемент — это вязующим звеном между 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 }
Как видите, метод принимает три параметра:
Element? child
— дочерний элемент.Widget? newWidget
— новую конфигурацию для элемента.Object? newSlot
— новый слот (место в дереве элементов), где будет располагаться элемент.
Этот метод также может быть вызван другим методом — inflateWidget
.
У этого метода существует несколько сценариев выполнения. Они представлены на диаграмме ниже.
Теперь разберём их чуть подробнее.
- Если
newWidget == null
(это значит, что виджета больше нет), тоchild
деактивируется, и вернётсяnull
. - Если
child == null && newWidget ≠ null
(означает, что появился новый виджет), то происходит вызов методаinflateWidget
, в результате чего создаётся новый элемент, который будет использоваться в качестве дочернего. - Если
child ≠ null && newWidget ≠ null
, то произойдёт то, о чём мы расскажем дальше.
Во-первых, происходит проверка на то, что нам была передана конфигурация типа, который соответствует типу элемента (под типом имеется в виду StatelessWidget
, StatefulWidget
или другие названия виджетов/элементов), результат сравнения лежит в переменной 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}
У него есть есть два параметра:
Widget? newWidget
— конфигурация для дочернего элемента.Object? newSlot
— слот для элемента.
Если у виджета есть глобальный ключ, то фреймворк попытается найти и взять уже существующий элемент, который был связан с текущим ключом, вызывая метод _retakeInactiveElement
.
Если элемент будет найден для заданного виджета, то он будет заново переиспользован (вызов методов _activateWithParent
и updateChild
с новой конфигурацией).
Может показаться, что методы updateChild
и inflateWidget
используют друг друга циклично, но на самом деле updateChild
просит inflateWidget
достать уже существующий элемент и вернуть его с новой конфигурацией либо создать новый элемент.
Если же элемент не будет найден или ключ не будет задан, то будет создан новый элемент для newWidget
. И после этого будет вызван метод mount
для дочернего элемента.
Вышеописанное можно представить в виде диаграммы:
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
у виджета и обновляет или создаёт элемент для него. Когда мы создаём виджет и определяем у него метод, этот метод возвращает нам дочерние виджеты. И именно под дочерние виджеты мы создаём элементы.
Создание первого и последующих элементов можно представить в виде диаграммы:
Отлично, с полями и методами класса Element познакомились. Как видите, никакой магии тут нет.
А следующем параграфе мы рассмотрим жизненный цикл элементов, а также взаимодействие элементов с InheritedWidget
и RenderObject
.