Всем привет! Меня зовут Михаил Зотьев, я работаю Flutter-разработчиком в серфинг .
Что мне больше всего нравится, как, наверное, и большинству других разработчиков, работающих с Flutter, — это то, насколько легко создавать красивые и удобные приложения.
Чтобы приступить к разработке Flutter, требуется очень мало времени.
Недавно я занимался разработкой игр, а сейчас полностью перешёл на кроссплатформенная мобильная разработка с использованием Flutter .
Что такое простота? С помощью десятка базовых виджетов можно собрать вполне приличные пользовательские интерфейсы.
А со временем, когда багаж подержанного накопится вполне прилично, вряд ли какая-то задача поставит вас в тупик: будь то необычный дизайн или сложная анимация.
И самое интересное, что, скорее всего, вы сможете им пользоваться, даже не задумываясь над вопросом: «Как это вообще работаетЭ» Поскольку Flutter имеет открытый исходный код, я решил разобраться, что скрывается под капотом (на Дартс-стороне Силы), и поделиться этим с вами.
Отдельно о Дарте и его преимуществах вы можете прочитать в нашей статье « Почему Flutter использует Dart, а не Kotlin или JavaScript ".
Виджет
Все мы не раз слышали фразу от команды разработчиков фреймворка: «Все во Flutter — это виджеты» .Давайте посмотрим, так ли это на самом деле.
Для этого обратимся к классу Виджет (далее — виджет) и начинаем постепенно знакомиться с содержимым.
Первое, что мы прочитаем в документации к классу:
Описывает конфигурацию [Элемента].Получается, что сам виджет — это всего лишь описание какого-то Элемент (далее именуемый элементом).
Виджеты — это центральная иерархия классов в среде Flutter. Виджет — это неизменяемое описание части пользовательского интерфейса.Подводя итог, фраза «Все во Flutter — это виджеты» — минимальный уровень понимания того, как всё работает, чтобы использовать Flutter. Виджет — центральный класс в иерархии Flutter. В то же время вокруг него существует множество дополнительных механизмов, которые помогают фреймворку справиться со своей задачей.Виджеты можно развернуть в элементы, которые управляют базовым деревом рендеринга.
Итак, мы узнали еще несколько фактов:
- виджет — неизменяемое описание части пользовательского интерфейса;
- виджет связан с некоторым расширенным представлением, называемым элементом;
- элемент управляет некоторой сущностью дерева рендеринга.
Пользовательский интерфейс и неизменяемость не очень хорошо сочетаются друг с другом, я бы даже сказал, что это совершенно несовместимые понятия.
Но к этому мы вернемся, когда возникнет более полное представление об устройстве мира Flutter, а пока продолжим знакомиться с документацией виджета.
Сами виджеты не имеют изменяемого состояния (все их поля должны быть окончательными).Этот пункт немного дополняет первый пункт: если нам нужна изменяемая конфигурация, для этого используется специальная сущность Состояние (далее — состояние), которое описывает текущее состояние этого виджета.Если вы хотите связать изменяемое состояние с виджетом, рассмотрите возможность использования [StatefulWidget], который создает объект [State] (через [StatefulWidget.createState]) всякий раз, когда он раздувается в элемент и включается в дерево.
Однако состояние связано не с виджетом, а с его элементарным представлением.
Данный виджет может быть включен в дерево ноль или более раз.Один и тот же виджет может быть включен в дерево виджетов много раз или не включаться вообще.В частности, данный виджет можно размещать в дереве несколько раз.
Каждый раз, когда виджет помещается в дерево, он раздувается до [Элемента], что означает, что виджет, включенный в дерево несколько раз, будет раздуваться несколько раз.
Но каждый раз, когда виджет включается в дерево виджетов, с ним связан элемент. Итак, на этом этапе с виджетами мы почти закончили, подведем итоги:
- виджет — центральный класс иерархии;
- виджет — это некая конфигурация;
- виджет — неизменяемое описание части пользовательского интерфейса;
- виджет связан с элементом, который каким-то образом управляет рендерингом;
- Изменяемое состояние виджета может быть описано некоторой сущностью, но оно связано не с виджетом, а с элементом, представляющим этот виджет.
Элемент
Из того, что мы узнали, возникает вопрос: «Что это за элементы, которые всем управляютЭ» Давайте сделаем то же самое — откроем документацию класса Element.Создание экземпляра [Виджета] в определенном месте дерева.Элемент — это некоторое представление виджета в определенном месте дерева.
Виджеты описывают, как настроить поддерево, но один и тот же виджет можно использовать для одновременной настройки нескольких поддеревьев, поскольку виджеты неизменяемы.Виджет описывает настройку некоторой части пользовательского интерфейса, но, как мы уже знаем, один и тот же виджет может использоваться в разных местах дерева.[Элемент] представляет собой использование виджета для настройки определенного места в дереве.
Со временем виджет, связанный с данным элементом, может измениться, например, если родительский виджет перестроится и создаст новый виджет для этого местоположения.
Каждое такое место будет представлено соответствующим элементом.
Но со временем виджет, связанный с элементом, может измениться.
Это означает, что элементы более долговечны и продолжают использоваться только при обновлении своих соединений.
Это довольно рациональное решение.
Как мы уже определили выше, виджеты — это неизменяемая конфигурация, просто описывающая определённую часть интерфейса, а значит, они должны быть очень лёгкими.
А элементы в зоне управления гораздо более тяжеловесные, но без необходимости их не воссоздают. Чтобы понять, как это делается, рассмотрим жизненный цикл элемента:
- Элемент создается путем вызова метода Widget.createElement и настраивается экземпляром виджета, для которого был вызван этот метод.
- Метод mount добавляет созданный элемент в указанную позицию родительского элемента.
При вызове этого метода также связываются дочерние виджеты, а объекты дерева рендеринга связываются с элементами.
- Виджет станет активным и должен появиться на экране.
- Если виджет, связанный с элементом, изменится (например, если изменился родительский элемент), существует несколько вариантов того, что произойдет. Если новый виджет имеет тот же runtimeType и ключ, то элемент связан с ним.
В противном случае текущий элемент удаляется из дерева, а новый элемент создается и связывается с новым виджетом.
- В случае, если родительский элемент решит удалить дочерний элемент или элемент между ними, это приведет к удалению объекта рендеринга и перемещению этого элемента в неактивный список, что приведет к деактивации элемента (вызов метода deactivate).
- Когда элемент считается неактивным, его нет на экране.
Элемент может оставаться неактивным только до конца текущего кадра; если в течение этого времени он остается неактивным, он размонтируется, после чего считается несуществующим и больше не будет включаться в дерево.
- Например, при повторной вставке в дерево элементов, если элемент или его предки имеют глобальный ключ, он будет удален из списка неактивных элементов, будет вызван метод активации и объект рендеринга, связанный с этим элементом, будет вставлен повторно.
в дерево рендеринга.
Это означает, что элемент должен снова появиться на экране.
Практически полностью соответствует описанию товара.
Этот интерфейс используется, чтобы избежать прямых манипуляций с элементом, но при этом обеспечивает доступ к необходимым методам контекста.
Например, findRenderObject, который позволит вам найти объект дерева рендеринга, соответствующий данному элементу.
РендерОбъект
Осталось разобраться с последним звеном этой триады – РендерОбъект .Как следует из названия, это объект дерева рендеринга.
У него есть родительский объект, а также поле данных, которое родительский объект использует для хранения конкретной информации о самом объекте, например его позиции.
Этот объект отвечает за реализацию основных протоколов рендеринга и макета.
RenderObject не ограничивает модель использования дочерних объектов: их может быть ни одного, один или несколько.
Система позиционирования также не ограничена: для использования доступна декартова система, полярные координаты, все это и многое другое.
Нет ограничений на использование протоколов компоновки: регулировка ширины или высоты, ограничение размера, указание размера и положения родителем или использование данных родительского объекта при необходимости.
Трепещущая картина мира
Давайте попробуем составить общую картину того, как все работает вместе.Как мы отмечали выше, виджет представляет собой неизменяемое описание, но пользовательский интерфейс не является статическим.
Данное несоответствие устраняется путем разделения объектов на 3 уровня и разделения зон ответственности.
- Дерево виджетов, в зону ответственности которого входит объявление конфигураций и хранение свойств.
- Дерево элементов, которые управляют жизненным циклом и связывают виджеты в некоторую иерархию и визуализируют с их помощью объекты.
- Дерево объектов визуализации, ответственностью которых является рендеринг, с учетом размеров позиции и ограничений.
Давайте посмотрим, как выглядят эти деревья, на простом примере:
В данном случае у нас есть некий StatelessWidget, завернутый в виджет Padding и содержащий текст внутри.
Давайте поставим себя на место Флаттера — нам дали это дерево виджетов.
Трепетание: «Эууу, Падинг, мне нужен твой элемент»
Заполнение: «Конечно, оставьте SingleChildRenderObjectElement»
Трепетание: «Элемент, вот твое место, устраивайся»
SingleChildRenderObjectElement: «Ребята, всё ок, но мне нужен RenderObject»
Трепетание: — Падинг, как мне вообще тебя нарисовать?
Заполнение: «Вот, RenderPadding»
SingleChildRenderObjectElement: «Отлично, приступим к работе».
Трепетание: «Итак, кто следующий? StatelessWidget, теперь дай мне элемент"
Виджет без сохранения состояния: «Вот StatelessElement»
Трепетание: «StatelessElement, вы будете подчиняться SingleChildRenderObjectElement, вот место, начните»
БезгражданныйЭлемент: "ХОРОШО"
Трепетание: «RichText, покажи мне элемент, пожалуйста»
RichText дает MultiChildRenderObjectElement
Трепетание: «MultiChildRenderObjectElement, вот ваше место, начните»
МультиЧилдРендерОбжектЭлемент: «Мне нужен рендер для работы»
Трепетание: «RichText, нам нужен объект рендеринга»
Богатый текст: «Вот RenderParagraph»
Трепетание: «RenderParagraph будет получать инструкции от RenderPadding, а MultiChildRenderObjectElement будет вами управлять»
МультиЧилдРендерОбжектЭлемент: «Теперь все ок, я готов»
Наверняка вы зададите логичный вопрос: «Где объект рендеринга для StatelessWidget, почему его там нет? Мы определили выше, что элементы связывают конфигурации с отображениемЭ» Обратим внимание на базовую реализацию метода монтирования, о которой говорилось в этом пункте описания жизненного цикла.
Мы не увидим в нем создания объекта рендеринга.void mount(Element parent, dynamic newSlot) { assert(_debugLifecycleState == _ElementLifecycle.initial); assert(widget != null); assert(_parent == null); assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active); assert(slot == null); assert(depth == null); assert(!_active); _parent = parent; _slot = newSlot; _depth = _parent != null ? _parent.depth + 1 : 1; _active = true; if (parent != null) _owner = parent.owner; if (widget.key is GlobalKey) { final GlobalKey key = widget.key; key._register(this); } _updateInheritance(); assert(() { _debugLifecycleState = _ElementLifecycle.active; return true; }()); }
Но элемент реализует BuildContext, у которого есть метод поиска объекта рендеринга findRenderObject, который приведет нас к следующему геттеру: RenderObject get renderObject {
RenderObject result;
void visit(Element element) {
assert(result == null);
if (element is RenderObjectElement)
result = element.renderObject;
else
element.visitChildren(visit);
}
visit(this);
return result;
}
В базовом случае элемент не может создавать объект рендеринга; Для этого требуется только RenderObjectElement и его потомки, но в этом случае элемент на некотором уровне вложенности должен иметь дочерний элемент, имеющий объект рендеринга.
Казалось бы, зачем все эти трудности.
Целых 3 дерева, разные зоны ответственности и т. д. Ответ довольно простой — на этом и строится работоспособность Flutter. Виджеты имеют неизменяемые конфигурации, поэтому пересоздаются довольно часто, но при этом достаточно легковесны, что не влияет на производительность.
Но Flutter старается максимально повторно использовать тяжелые элементы.
Давайте посмотрим на пример.
Текст в центре экрана.
Код в этом случае будет выглядеть примерно так: body: Center(
child: Text(“Hello world!”)
),
В этом случае дерево виджетов будет выглядеть так:
После того, как Flutter построит все 3 дерева, мы получим следующую картину:
Что произойдет, если мы изменим текст, который собираемся отображать?
Теперь у нас есть новое дерево виджетов.
Выше мы говорили о максимально возможном повторном использовании элементов.
Давайте взглянем на метод класса Widget с говорящим именем.
можно обновить .
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
}
Проверяем тип предыдущего виджета и нового, а также их ключи.
Если они одинаковые, то менять элемент нет необходимости.
Итак, до обновления первый элемент — Центр, после обновления — тоже Центр.
У обоих нет ключей, полное совпадение.
Мы можем обновить ссылку элемента на новый виджет.
Но помимо типа и ключа у виджета есть еще описание и конфигурация, а также могли измениться значения параметров, необходимых для отображения.
Вот почему элемент после обновления ссылки на виджет должен инициировать обновления объекта рендеринга.
В случае с Центром ничего не изменилось, и мы продолжаем сравнивать дальше.
Еще раз тип и ключ говорят нам, что пересоздавать элемент нет смысла.
Text является потомком StatelessWidget и не имеет объекта прямого отображения.
Перейдите в РичТекст. Виджет также не изменил свой тип, расхождений в клавишах нет. Элемент обновляет свою связь с новым виджетом.
Соединение обновилось, осталось только обновить свойства.
В результате RenderParagraph отобразит новое текстовое значение.
И как только придет время следующего кадра рендеринга, мы увидим ожидаемый результат.
Благодаря такого рода работе достигается такая высокая производительность Flutter.
В приведенном выше примере описан случай, когда сама структура виджета не изменилась.
Но что произойдет, если структура изменится? Flutter, конечно, продолжит пытаться максимально использовать существующие объекты, как мы поняли из описания жизненного цикла, но для всех новых виджетов будут создаваться новые элементы, а старые и уже не нужные будут удаляться в конце рама.
Давайте посмотрим на пару примеров.
И чтобы убедиться в вышесказанном, мы используем инструмент Android Studio — Flutter Inspector. @override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _isFirst ? first() : second(),
),
floatingActionButton: FloatingActionButton(
child: Text("Switch"),
onPressed: () {
setState(() {
_isFirst = !_isFirst;
});
},
),
);
}
Widget first() => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"test",
style: TextStyle(fontSize: 25),
),
SizedBox(
width: 5,
),
Icon(
Icons.error,
),
],
);
Widget second() => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"one more test",
style: TextStyle(fontSize: 25),
),
Padding(
padding: EdgeInsets.only(left: 5),
),
Icon(
Icons.error,
),
],
);
В этом случае при нажатии на кнопку один из виджетов изменится.
Посмотрим, что нам покажет инспектор.
Как мы видим, Flutter воссоздал рендер только для Padding, остальные просто переиспользовались.
Рассмотрим еще один вариант, при котором структура изменится более глобально — изменим уровни вложенности.
Widget second() => Container(child: first(),);
Несмотря на то, что визуально дерево совершенно не изменилось, элементы и объекты дерева рендеринга были воссозданы.
Это произошло потому, что Flutter выполняет сравнение по уровням (в данном случае не важно, что большая часть дерева не изменилась), исключение этой части произошло в момент сравнения Container и Row. Однако из этой ситуации можно выйти.
GlobalKey поможет нам в этом.
Добавим такой ключ для Row. var _key = GlobalKey(debugLabel: "testLabel");
Widget first() => Row(
key: _key,
…
);
Как только мы сказали Flutter, что эту деталь можно использовать повторно, они ухватились за эту возможность.
Заключение
Мы стали немного ближе к волшебству Flutter и теперь знаем, что оно касается не только виджетов.Flutter — это продуманный, слаженный механизм со своей иерархией и зонами ответственности, с помощью которого можно создавать не только красивые, но и продуктивные приложения.
Конечно, мы рассмотрели лишь небольшую, хотя и довольно важную часть его структуры, поэтому продолжим анализировать различные аспекты внутренней работы фреймворка в дальнейших статьях.
А пока вы можете прочитать статья, сравнивающая Flutter и React Native , в котором мы сравнили основные характеристики этих фреймворков.
Я надеюсь, что информация в этой статье поможет понять, как работает Flutter внутри, и поможет вам найти элегантные и производительные решения во время разработки.
Спасибо за внимание!
Ресурсы
трепетать «Как Flutter визуализирует виджеты» Эндрю Фитц Гиббон, Мэтт Салливан Теги: #Разработка мобильных приложений #программирование #Программное обеспечение #flutter #dart #surf-
Учет Времени В Проектах: Почему Это Важно
19 Oct, 24 -
Визуальное Отображение Оставшихся Картриджей
19 Oct, 24 -
Вудикаст #03. В Стиле Подкаста =)
19 Oct, 24