В предыдущих параграфах мы познакомились с основными виджетами 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.
