3.1. Элементы: первое погружение

В предыдущих параграфах мы познакомились с основными виджетами Flutter. Начиная с этой темы мы будем углубляться в работу самого фреймворка.

Этот и несколько следующих параграфов мы посвятим элементам.

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

Давайте приступим.

Но начнём чуть издалека: не с элементов, а с BuildContext.

BuildContext

Вы уже сталкивались с этой сущностью, когда работали с методом build.

1@override
2Widget build(BuildContext context){
3 return SomeWidget();
4}

Или, например, когда получали текущую тему.

1@override
2Widget build(BuildContext context) {
3 final theme = Theme.of(context); // Тема
4
5 return SomeWidget(theme: theme);
6}

Но как мы получаем нужную тему? Как виджет понимает, кто его родитель и где в дереве виджетов он находится? И почему для получения темы мы используем контекст, почему при работе с навигатором нам тоже нужен контекст?

Вопросы и простые, и сложные одновременно. Нужно разбираться!

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

Пример
1class CircleWidget extends StatelessWidget {
2  const HelloWorldText();
3
4 @override
5 Widget build(BuildContext context){
6  final radius = 100;
7  return Container( 
8     decoration: BoxDecoration(
9         shape: BoxShape.circle,
10     color: Colors.red,
11     ),
12     width: radius,
13   height: radius,
14  );
15 }
16}

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

А теперь представьте, что этот виджет — это реальный маленький шар, который имеет какие-то параметры, скажем цвет, радиус и наполнен воздухом.

Всё это мы указываем, передавая такие параметры, как color: Colors.redshape: BoxShape.circle.

Именно таким образом мы задаём то, что хотим. Но как всё это преобразуется в то, что мы видим?

Если мы посмотрим, как реализован класс StatelessWidget или StatefulWidget, то увидим, что тот и другой являются реализацией класса Widget.

Пример класса Widget
1abstract class StatelessWidget extends Widget {
2  const StatelessWidget({super.key});
3
4  @override
5  StatelessElement createElement() => StatelessElement(this);
6
7  @protected
8  Widget build(BuildContext context);
9}
10// --------
11abstract class StatefulWidget extends Widget {
12  const StatefulWidget({super.key});
13
14  @override
15  StatefulElement createElement() => StatefulElement(this);
16
17  @protected
18  @factory
19  State createState();
20}

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

1@immutable
2abstract class Widget extends DiagnosticableTree {
3  const Widget({this.key});
4
5  final Key? key;
6
7  @protected
8  @factory
9  Element createElement(); /// ОН ТУТ :)
10
11  @override
12  String toStringShort() {...}
13
14  @override
15  void debugFillProperties(DiagnosticPropertiesBuilder properties) {...}
16
17  @override
18  @nonVirtual
19  bool operator ==(Object other) => ...;
20
21  @override
22  @nonVirtual
23  int get hashCode => super.hashCode;
24  
25  static bool canUpdate(Widget oldWidget, Widget newWidget) {...}
26  static int _debugConcreteSubtype(Widget widget) {...}
27}

Если мы прочтём документацию Flutter по этому методу, то увидим там такую строчку:

👉 Inflates this configuration to a concrete instance.

То есть: «Раздувает эту конфигурацию до конкретного экземпляра».

Её можно понять так: метод createElementпревращает заданную нами конфигурацию (то есть Widget) во что-то более конкретное.

И это «что-то более конкретное» как раз и есть экземпляр класса Element.

То есть, когда вы создаёте какой-либо виджет, фреймворк вызывает его метод createElement и возвращает Element, который сохраняет информацию, необходимую при работе приложения конкретно для этого виджета в конкретном месте приложения.

Именно он знает, где находится виджет, кто его родитель, какие у него потомки и многие другие данные.

flutter

И если мы углубимся в реализацию класса Element, то увидим, что он реализует интерфейс BuildContext.

1abstract class Element extends DiagnosticableTree implements BuildContext {
2 /// Тут находится куча методов и полей, которые мы рассмотрим ниже
3 ...
4}

И теперь всё становится чуть яснее.

Если Element — это BuildContext и Element знает, где он находится, кто его родитель, то получается, когда мы передаём куда-либо context, то передаём и всю эту информацию, а фреймворк уже сам понимает, что и откуда брать, и отдаёт нам тем самым тему при вызове Theme.of(context).

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

И в действительности единственным классом, который реализует BuildContext, является Element. Поэтому, когда вы работаете с BuildContext, помните, что это просто Element.

А теперь давайте подробнее изучим сам Element.

Что такое Element

Для начала нам стоит определиться с понятием виджета, элемента и RenderObject.

Widget — это неизменяемая конфигурация. Почему конфигурация? Потому что виджет хранит данные, по которым будет строится UI. Это, например, цвет, радиус, дочерние виджеты. То есть конфигурация — это набор данных. Из этого следует, что понятия виджета и конфигурации взаимозаменяемы. Поэтому когда вы будете думать о виджете, то просто представляйте, что это набор данных, по которому определяется поведение и отрисовка объекта на экране.

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

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

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

flutter

Но что вызывает создание элемента? Хоть метод createElement и находится у виджета, но вызывает этот метод не виджет, а вызывают его другие элементы у своих детей.

Чтобы понять процесс создания, нам достаточно заглянуть вглубь Flutter, найти класс Element и посмотреть, что делает его метод inflateWidget.

Мы видим, что в этот виджет передаётся конфигурация (параметр newWidget), и это конфигурация для дочернего элемента, у неё мы вызываем метод createElement. В качестве результата вызова мы получим дочерний элемент. И так происходит, пока мы не дойдём до последнего виджета.

Пример
1Element inflateWidget(Widget newWidget, Object? newSlot) {
2 ...
3  final Element newChild = newWidget.createElement();
4  ...
5}

flutter

Если вы заглянете в устройство любого виджета, то увидите, какой элемент создаётся для виджета. Например, для StatelessWidget создаётся StatelessElement.

Например, для StatelessWidget создаётся StatelessElement.
1abstract class StatelessWidget extends Widget {
2  const StatelessWidget({super.key});
3
4  @override
5  StatelessElement createElement() => StatelessElement(this);
6
7  @protected
8  Widget build(BuildContext context);
9}

Существует ещё много других элементов. Рассмотрим их ниже

Классификация элементов

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

  • те, у которых нет RenderObject;
  • те, которые есть RenderObject.

Первая группа (без RenderObject) — это все наследники ComponentElement.

Поскольку RenderObject у них нет, они принимают виджеты и передают их для отрисовки другим элементам, у которых RenderObject есть.

Плюс они хранят данные.

Вторая группа (с RenderObject) — это RenderObjectElement и все его наследники. Данный элемент содержит свой собственный RenderObject.

Диаграмма ниже показывает, какие элементы от какого наследуются.

flutter

Рассмотрим каждую группу подробнее.

1-я группа

  • StatelessElement — принадлежит StatelessWidget.
  • StatefulElement — принадлежит StatefulWidget.
  • ProxyElement — это элемент, от которого наследуются InheritedElement и ParentDataElement. И, грубо говоря, ProxyElement служит для того, чтобы хранить промежуточные данные между родительским и дочерним элементом.
  • InheritedElement — создаётся InheritedWidget. Про то, как они связаны, мы поговорим в отдельном параграфе.
  • ParentDataElement — создаётся ParentDataWidgetParentDataWidget используется, чтобы сообщить какие-то данные, которые могут быть использованы родительским виджетом при отрисовке.

👉 Для примера можно взять виджеты Stack и Positioned. Когда мы используем виджет Positioned, мы задаём ему такие параметры, как leftrighttopbottom и прочие.

Он хранит эти данные до тех пор, пока MultiChildRenderObjectElement не попросит применить их вызовом метода applyParentData. И в соответствии с этими данными Stack сам расположит дочерние элементы.

2-я группа

FooElement — различные классы, унаследованные от RenderObjectElement, где Foo — название класса RenderObject. К примеру, LeafRenderObjectElementSingleChildRenderObjectElementMultiChildRenderObjectElement и прочие.


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

Торопиться не будем — пока остановимся на этом. А уже в следующем параграфе углубимся в процесс создания элементов: рассмотрим методы и поля класса Element.

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

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

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

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