В предыдущих параграфах мы познакомились с основными виджетами 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.red
, shape: 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
, который сохраняет информацию, необходимую при работе приложения конкретно для этого виджета в конкретном месте приложения.
Именно он знает, где находится виджет, кто его родитель, какие у него потомки и многие другие данные.
И если мы углубимся в реализацию класса 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
мы поговорим в отдельном параграфе).
Но что вызывает создание элемента? Хоть метод createElement
и находится у виджета, но вызывает этот метод не виджет, а вызывают его другие элементы у своих детей.
Чтобы понять процесс создания, нам достаточно заглянуть вглубь Flutter, найти класс Element
и посмотреть, что делает его метод inflateWidget
.
Мы видим, что в этот виджет передаётся конфигурация (параметр newWidget
), и это конфигурация для дочернего элемента, у неё мы вызываем метод createElement
. В качестве результата вызова мы получим дочерний элемент. И так происходит, пока мы не дойдём до последнего виджета.
Пример
1Element inflateWidget(Widget newWidget, Object? newSlot) {
2 ...
3 final Element newChild = newWidget.createElement();
4 ...
5}
Если вы заглянете в устройство любого виджета, то увидите, какой элемент создаётся для виджета. Например, для 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
.
Диаграмма ниже показывает, какие элементы от какого наследуются.
Рассмотрим каждую группу подробнее.
1-я группа
StatelessElement
— принадлежитStatelessWidget
.StatefulElement
— принадлежитStatefulWidget
.ProxyElement
— это элемент, от которого наследуютсяInheritedElement
иParentDataElement
. И, грубо говоря,ProxyElement
служит для того, чтобы хранить промежуточные данные между родительским и дочерним элементом.InheritedElement
— создаётсяInheritedWidget
. Про то, как они связаны, мы поговорим в отдельном параграфе.ParentDataElement
— создаётсяParentDataWidget
.ParentDataWidget
используется, чтобы сообщить какие-то данные, которые могут быть использованы родительским виджетом при отрисовке.
👉 Для примера можно взять виджеты
Stack
иPositioned
. Когда мы используем виджетPositioned
, мы задаём ему такие параметры, какleft
,right
,top
,bottom
и прочие.Он хранит эти данные до тех пор, пока
MultiChildRenderObjectElement
не попросит применить их вызовом методаapplyParentData
. И в соответствии с этими даннымиStack
сам расположит дочерние элементы.
2-я группа
FooElement
— различные классы, унаследованные от RenderObjectElement
, где Foo
— название класса RenderObject
. К примеру, LeafRenderObjectElement
, SingleChildRenderObjectElement
, MultiChildRenderObjectElement
и прочие.
Итак, в этом параграфе мы узнали, что такое элемент, как он связан с BuildContext, и какие элементы бывают.
Торопиться не будем — пока остановимся на этом. А уже в следующем параграфе углубимся в процесс создания элементов: рассмотрим методы и поля класса Element
.