Как Flutter работает на самом деле?
Что такое Widgets, Elements, BuildContext, RenderOject, Bindings?..
Сложность: Новичок
Вступление
В прошлом году (прим: в 2018), когда я начал свое путешествие в сказочный мир Flutter, в Интернете было очень мало информации по сравнению с тем, что есть сегодня. Сейчас, несмотря на то, что уже написано много материалов, лишь небольшая их часть рассказывает о том, как на самом деле работает Flutter.
Что же такое Widgets (виджеты), Elements (элементы), BuildContext? Почему Flutter быстрый? Почему иногда он работает не так, как ожидается? Что такое деревья и зачем они нужны?
В 95% случаев при написании приложения вы будете иметь дело только с виджетами, чтобы что-то отображать на экране или взаимодействовать с ним. Но неужели вы никогда не задумывались, как вся эта магия работает внутри? Как система узнает, когда обновить экран и какие части должны быть обновлены?
Содержание:
- Вступление
- Часть 1: Предыстория
- Часть 2. От виджетов к пикселям
- Часть 3: Обработка жестов
- Часть 4: Анимации
- Полная картина
- BuildContext
- Заключение
Часть 1: Предыстория
В первой части статьи представлены некоторые ключевые понятия, которые будут использованы во второй части материала и помогут лучше понять Flutter.
Немного об устройстве
Давайте начнем с конца и вернемся к основам.
Когда вы смотрите на свое устройство или, точнее, на приложение, запущенное на вашем устройстве, вы видите только экран.
На самом деле, всё, что вы видите – это пиксели, которые вместе составляют 2-мерное изображение, и когда вы касаетесь экрана пальцем, устройство распознает только положение вашего пальца на стекле.
Вся магия приложения (с визуальной точки зрения) в большинстве случаев заключается в обновлении этого изображения на основе следующих взаимодействий:
- с экраном устройства (например, палец на стекле)
- с сетью (например, связь с сервером)
- со временем (например, анимация)
- с другими внешними датчиками
Визуализация изображения на экране обеспечивается аппаратным обеспечением (дисплеем), которое регулярно (обычно 60 раз в секунду) обновляет дисплей. Эта называется "частотой обновления" и выражается в Гц (Герцах).
Дисплей получает информацию для отображения от GPU (Graphics Processing Unit), представляющего собой специализированную электронную схему, оптимизированную и предназначенную для быстрого формирования изображения из некоторых данных (полигонов и текстур). Количество раз в секунду, которое графический процессор может генерировать "изображение" (=буфер кадров) для отображения и отправки его на аппаратное обеспечение, называется кадровой частотой (прим: frame rate). Это измеряется с помощью блока кадров в секунду (например, 60 кадров в секунду или 60fps).
Вы, возможно, спросите меня, почему я начал эту статью с понятий 2-мерного изображения, отображаемого GPU / аппаратным обеспечением и датчиком физического стекла и какова связь с обычными виджетами Flutter?
Думаю, что будет легче понять, как на самом деле работает Flutter, если мы посмотрим на него с этой точки зрения, так как одна из главных целей приложения Flutter – создать это 2-мерное изображение и дать возможность взаимодействовать с ним. Также потому, что во Flutter, хотите верьте, хотите нет, почти все обусловлено необходимостью обновления экрана быстро и в нужный момент!
Интерфейс между кодом и устройством
Так или иначе, все интересующиеся Flutter уже видели следующую картинку, которая описывает архитектуру высокого уровня Flutter.
Когда мы пишем приложение Flutter, используя Dart, мы остаемся на уровне Flutter Framework (выделено зеленым цветом).
Flutter Framework взаимодействует с Flutter Engine (синим цветом) через слой абстракции, называемый Window. Этот уровень абстракции предоставляет ряд API для косвенного взаимодействия с устройством.
Также через этот уровень абстракции Flutter Engine уведомляет Flutter Framework, когда:
- событие, представляющее интерес, происходит на уровне устройства (изменение ориентации, изменение настроек, проблема с памятью, состояние работы приложения…)
- какое-то событие происходит на уровне стекла (=жест)
- канал платформы отправляет некоторые данные
- но также и в основном, когда Flutter Engine готов к рендерингу нового кадра
Управление Flutter Framework рендерингом Flutter Engine
В это сложно поверить, но это правда. За исключением некоторых случаев (cм. ниже) ни один код Flutter Framework не выполняется без запуска рендеринга Flutter Engine.
Исключения:
- Gesture / Жест (= событие на стекле)
- Сообщения платформы (= сообщения, которые создаются устройством, например, GPS)
- Сообщения устройства (= сообщения, которые относятся к изменению состояния устройства, например, ориентация, приложение, отправленное в фоновом режиме, предупреждения памяти, настройки устройства…)
- Future или http-ответы
(Между нами говоря, на самом деле можно применить визуальное изменение без вызова от Flutter Engine, но это не рекомендуется делать)
Вы меня спросите: "Если какой-то код, связанный с жестом, выполняется и вызывает визуальное изменение или если я использую timer для задания периодичности задачи, которая приводит к визуальным изменениям (например, анимация), то как это работает?"
Если вы хотите, чтобы произошло визуальное изменение или чтобы какой-то код выполнялся на основе таймера, то вам нужно сообщить Flutter Engine, что что-то должно быть отрисовано.
Обычно при следующем обновлении Flutter Engine обращается к Flutter Framework для выполнения некоторого кода и в конечном итоге предоставляет новую сцену для рендеринга.
Поэтому важный вопрос заключается в том, как движок Flutter организует все поведение приложения на основе рендеринга.
Чтобы вам получить представление о внутренних механизмах, посмотрите на следующую анимацию:
Краткое объяснение (более подробная информация будет позже):
- Некоторые внешние события (жест, http-ответы и тд) или даже futures могут запускать задачи, которые приводят к необходимости обновления отображения. Соответствующее сообщение отправляется Flutter Engine (= Schedule Frame)
- Когда Flutter Engine готов приступить к обновлению рендеринга, он создает Begin Frame запрос
- Этот Begin Frame запрос перехватывается Flutter Framework, который выполняет задачи, связанные в основном с Tickers (например, анимацию)
- Эти задачи могут повторно создать запрос для более поздней отрисовки (пример: анимация не закончила своё выполнение, и для завершения ей потребуется получить еще один Begin Frame на более позднем этапе)
- Далее Flutter Engine отправляет Draw Frame, который перехватывается Flutter Framework, который будет искать любые задачи, связанные с обновлением макета с точки зрения структуры и размера
- После того, как все эти задачи выполнены, он переходит к задачам, связанным с обновлением макета с точки зрения отрисовки
- Если на экране есть что-то, что нужно нарисовать, то новая сцена (Scene) для визуализации отправляется в Flutter Engine, который обновит экран
- Затем Flutter Framework выполняет все задачи, которые будут выполняться после завершения рендеринга (= PostFrame callbacks), и любые другие последующие задачи, не связанные с рендерингом
- … и этот процесс начинается снова и снова
RenderView и RenderObject
Прежде чем погружаться в детали, связанные с потоком действий, самое время ввести понятие Rendering Tree.
Как уже говорилось ранее, всё в конечном итоге преобразуется в пиксели, которые будут отображаться на экране, и Flutter Framework преобразует Widgets, которые мы используем для разработки приложения, в визуальные блоки, которые будут отображаться на экране.
Данные визуальные части соответствуют объектам, называемым RenderObject, которые используются для:
- определения некоторой области экрана с точки зрения размеров, положения, геометрии, а также с точки зрения «rendered content»
- определения зон экрана, на которые могут повлиять жесты (= касания пальцев)
Набор всех RenderObject формирует дерево, называемое Render Tree. В верхней части этого дерева (= root) мы находим RenderView.
RenderView представляет общую поверхность для объектов Render Tree и является специальной версией RenderObject.
Визуально мы могли бы представить все это следующим образом:
Cвязь между Widget и RenderObject будет рассмотрена далее. А пока пришло время немного углубиться…
Инициализация bindings
При запуске Flutter приложения сначала вызывается функция main()
, который в конечном итоге вызовет метод runApp(Widget app)
.
Во время вызова метода runApp()
Flutter Framework инициализирует интерфейсы между собой и Flutter Engine. Эти интерфейсы называются bindings (прим: привязки).
Введение в привязки
Привязки предназначены для того, чтобы быть связующим звеном между фреймворком и движком Flutter. Только с помощью привязок можно обмениваться данными между Flutter Framework и Flutter Engine.
(Есть только одно исключение из этого правила – RenderView, но мы обсудим это позже).
Каждая привязка отвечает за обработку набора конкретных задач, действий, событий, сгруппированных по области деятельности.
На момент написания этой статьи во Flutter Framework насчитывается 8 привязок.
Ниже приведены 4 из них, которые будут рассмотрены в этой статье:
- SchedulerBinding
- GestureBinding
- RendererBinding
- WidgetsBinding
Для полноты картины упомяну и остальные 4:
- ServicesBinding: отвечает за обработку сообщений, отправленных каналом платформы (platform channel)
- PaintingBinding: отвечает за обработку кэша изображений
- SemanticsBinding: зарезервировано для последующей реализации всего, что связано с семантикой
- TestWidgetsFlutterBinding: используется библиотекой тестов виджетов
Можно также упомянуть WidgetsFlutterBinding, но на самом деле это не является привязкой, а скорее своего рода "инициализатором привязки".
На следующей диаграмме показано взаимодействие между привязками, которые я собираюсь рассмотреть далее, и Flutter Engine.
Давайте посмотрим на каждую из этих "основных" привязок.
SchedulerBinding
У этой привязки есть две основные обязанности:
- Сказать Flutter Engine: "Эй! В следующий раз, когда вы не будете заняты, "разбудите" меня, чтобы я мог немного поработать и сказать вам, что отрендерить, или, если мне нужно, чтобы вы вызвали меня позже..."
- Слушать и реагировать на такие "тревожные пробуждения" (см. ниже)
Когда SchedulerBinding запрашивает "тревожное пробуждение"?
Когда Ticker должен отработать новый tick
Например, у вас есть анимация, вы ее запускаете. Анимация кадрируется с помощью Ticker, который с регулярным интервалом (= tick) вызывается для выполнения обратного вызова. Чтобы запустить такой обратный вызов, нам нужно сказать Flutter Engine, чтобы он "разбудил" нас при следующем обновлении (= Begin Frame). Это запустит обратный вызов ticker для выполнения его задачи. Если ticker все еще нужно продолжить выполнение, то в конце своей задачи он вызовет SchedulerBinding для планирования другого кадра.
Когда надо обновить отображение
Например, надо отработать событие, которое приводит к визуальному изменению (пример: обновление цвета части экрана, прокрутка, добавление / удаление чего-либо с экрана), для этого нам нужно предпринять необходимые шаги, чтобы в конечном итоге показать на экране обновленное изображение. В этом случае, когда происходит такое изменение, Flutter Framework вызывает SchedulerBinding для планирования другого кадра с помощью Flutter Engine. (Позже мы увидим, как это работает на самом деле)
GestureBinding
Данная привязка слушает взаимодействие с движком в терминах "пальца" (= жест).
В частности, он отвечает за прием данных, относящихся к пальцу, и за определение того, с какой частью (частями) экрана работают жесты. Затем он соответственно уведомляет об этом / этих частях.
RendererBinding
Эта привязка является связующим звеном между Flutter Engine и Render Tree. Она отвечает за:
- прослушивание событий, создаваемых движком, чтобы сообщить об изменениях, применяемых пользователем через настройки устройства, которые влияют на визуальные эффекты и / или семантику
- сообщение движку об изменениях, которые будут применены к отображению
Чтобы предоставить изменения, которые будут отображаться на экране, RendererBinding отвечает за управление PipelineOwner и инициализацию RenderView.
PipelineOwner — это своего рода оркестратор, который знает, что нужно сделать с RenderObject в соответствии с компоновой, и координирует эти действия.
WidgetsBinding
Данная привязка прослушивает изменения, применяемые пользователем через настройки устройства, которые влияют на язык (= locale) и семантику.
Небольшое примечание
Я предполагаю, что на более позднем этапе развития Flutter все события, связанные с семантикой, будут перенесены в SemanticsBinding, но на момент написания этой статьи это еще не так.
Кроме этого, WidgetsBinding является связующим звеном между виджетами и Flutter Engine. Она отвечает за:
- управление процессом обработки изменений структуры виджетов
- вызов рендеринга
Обработка изменений структуры виджетов осуществляется с помощью BuildOwner.
BuildOwner отслеживает, какие виджеты нуждаются в перестройке, и обрабатывает другие задачи, которые применяются к структуре виджетов в целом.
Часть 2. От виджетов к пикселям
Теперь, когда мы познакомились с основами внутренней работы Flutter, пришло время поговорить о виджетах.
Во всей документации Flutter вы прочитаете, что всё Widgets (виджеты).
Это почти правильно. Но для того, чтобы быть немного более точным, я бы скорее сказал:
Со стороны разработчика, всё, что связано с пользовательским интерфейсом с точки зрения компоновки и взаимодействия, делается с помощью виджетов.
К чему такая точность? К тому, что Widget позволяет разработчику определить часть экрана с точки зрения размеров, содержания, компоновки и взаимодействия, НО за этим есть гораздо большее. Так что же такое Widget на самом деле?
Неизменяемая конфигурация
Если вы посмотрите исходный код Flutter, то заметите следующее определение класса Widget.
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
...
}
Что это значит?
Аннотация "@immutable" очень важна и говорит нам, что любая переменная в классе Widget должна быть FINAL, другими словами: "определена и назначена ОДИН РАЗ ДЛЯ ВСЕХ". Таким образом, после создания экземпляр Widget больше не сможет изменить свои внутренние переменные.
Так как Widget неизменяемый, то можно его считать статичной конфигурацией
Иерархическая структура виджетов
Когда вы разрабатываете с помощью Flutter, вы определяете структуру своего экрана(ов), используя виджеты примерно так:
Widget build(BuildContext context){
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('My title'),
),
body: Container(
child: Center(
child: Text('Centered Text'),
),
),
),
);
}
В этом примере используется 7 виджетов, которые вместе образуют иерархическую структуру. Очень упрощенная схема, основанная на данном коде, выглядит следующим образом:
Как можно заметить, представленная схема выглядит, как дерево, где SafeArea является его корнем.
Лес за деревьями
Как вы уже знаете, виджет сам по себе может быть агрегацией других виджетов. В качестве примера можно изменить предыдущий код следующим образом:
Widget build(BuildContext context){
return MyOwnWidget();
}
Данный вариант предполагает, что виджет "MyOwnWidget" сам будет отображать SafeArea, Scaffold. Но самое главное в этом примере заключается в том, что
Widget может представлять лист, узел в дереве, даже само дерево или, почему бы и нет, лес деревьев...
Понимание Element в дереве
При чём здесь это?
Как будет показано далее, чтобы иметь возможность генерировать пиксели, которые составляют изображение, отображаемое на устройстве, Flutter должен знать в деталях все маленькие части, которые составляют экран, и, чтобы определить все части, ему необходимо знать раскрытие всех виджетов.
Чтобы проиллюстрировать данный момент, рассмотрим принцип матрёшки: в закрытом состоянии вы видите только 1 куклу, но она содержит другую, которая в свою очередь содержит ещё одну и так далее...
Когда Flutter "раскроет" все виджеты (часть экрана), это будет похоже на получение всех кукол (часть целого).
На картинке ниже показана часть конечной иерархической структуры виджетов, соответствующая предыдущему коду. Желтым цветом я выделил виджеты, которые были упомянуты в коде ранее, чтобы вы могли определить их в финальном дереве.
Важное уточнение
Формулировка "дерево виджетов" (Widget tree) существует только для облегчения понимания, поскольку программисты используют виджеты, но во Flutter НЕТ дерева виджетов!
На самом деле, правильнее будет сказать "дерево элементов" (tree of Elements)
Настало время ввести понятие элемента (Element).
Каждому виджету соответствует один элемент. Элементы связаны друг с другом и образуют дерево. Следовательно элемент является ссылкой на что-то в дереве.
Для начала подумайте об элементе как о узле, который имеет родителя и, возможно, ребенка. Связывая их вместе через отношение родитель — ребёнок, мы получаем древовидную структуру.
Как вы можете видеть, элемент указывает на один виджет, а также может указывать на RenderObject.
Даже лучше… Element указывает на Widget, который создал этот Element!
Давайте подведём итоги:
- Нет никакого дерева виджетов, но есть дерево элементов
- Элементы создаются виджетами
- Элемент ссылается на виджет, который его создал
- Элементы связаны вместе с родительскими отношениями
- У элемента может быть "ребёнок"
- Элементы также могут указывать на RenderObject
Элементы определяют, как части отображаемые блоки связаны друг с другом
Для того, чтобы лучше представить, где понятие элемент подходит, давайте рассмотрим следующее визуальное представление:
Как вы можете заметить, дерево элементов является фактической связью между виджетами и RenderObjects.
Но почему Widget создает Element?
3 категории виджетов
Во Flutter виджеты разделены на 3 категории, лично я называю их следующим образом (но это только мой способ классифицировать их):
Proxy
Основная задача этих виджетов состоит в том, чтобы хранить некоторую информацию (которая должна быть доступной для виджетов), части древовидной структуры, основанной на Proxy. Примером таких виджетов является InheritedWidget или LayoutId.
Эти виджеты не принимают непосредственного участия в формировании пользовательского интерфейса, но используются для получения информации, которую они могут предоставить.
Renderer
Данные виджеты имеют непосредственное отношение к компоновке экрана, поскольку они определяют (или используются для определения) размеры, положение, отрисовку. Типичными примерами являются: Row, Column, Stack, а также Padding, Align, Opacity, RawImage...
Component
Это другие виджеты, которые предоставляют непосредственно не окончательную информацию, связанную с размерами, позициями, внешним видом, а скорее данные (или подсказки), которые будут использоваться для получения той самой финальной информации. Эти виджеты обычно называются компонентами.
Примеры: RaisedButton, Scaffold, Text, GestureDetector, Container...
В этом PDF-файле перечислена большая часть виджетов, сгруппированных по категориям.
Почему это разделение важно? Потому что в зависимости от категории виджета, соответствующий тип элемента связан с…
Типы элементов
Есть несколько типов элементов:
Как вы можете видеть на картинке выше, элементы делятся на 2 основных типа:
ComponentElement
Эти элементы напрямую не отвечают за отрисовку какой-либо части отображения.
RenderObjectElement
Данные элементы отвечают за части отображаемого изображения на экране.
Отлично! Столько информации, но как всё это связано друг с другом и почему об этом интересно рассказать?
Как виджеты и элементы работают вместе
Во Flutter вся механика основана на инвалидации элемента или renderObject.
Инвалидация элемента может быть сделана следующими способами:
- используя
setState
, который инвалидирует весь StatefulElement (обратите внимание, что я намеренно не говорю StatefulWidget) - через уведомления, обрабатываемые proxyElement (например, InheritedWidget), который инвалидирует любой элемент, зависящий от данного proxyElement
Результатом инвалидации является то, что на соответствующий элемент появляется ссылка в списке dirty элементов.
Инвалидация renderObject означает, что структура элементов никак не меняется, но происходит изменение на уровне renderObject, например:
- изменение его размеров, положения, геометрии...
- необходимо что-то перекрасить, например, когда вы просто меняете цвет фона, стиль шрифт...
Результатом такой инвалидации является ссылка на соответствующий renderObject в списке объектов рендеринга (renderObjects), которые необходимо перестроить или перекрасить.
Независимо от типа инвалидации вызывается SchedulerBinding (помните такое?) для запроса к Flutter Engine, чтобы тот запланировал новый кадр.
Это именно тот момент, когда Flutter Engine "будит" SchedulerBinding и происходит вся магия...
onDrawFrame()
Ранее в этой статье мы отметили, что у SchedulerBinding две основные обязанности, одна из которых заключается в готовности обрабатывать запросы, создаваемые Flutter Engine, связанные с перестроением кадра. Это идеальный момент, чтобы сосредоточиться на этом.
Ниже на частичной диаграмме последовательности показано, что происходит, когда SchedulerBinding получает запрос onDrawFrame() от Flutter Engine.
Шаг 1. Элементы
Вызывается WidgetsBinding, и данная привязка сначала рассматривает изменения, связанные с элементами. WidgetsBinding вызывает метод buildScope объекта buildOwner, так как BuildOwner отвечает за обработку дерева элементов. Этот метод проходит по списку dirty элементов и запрашивает их перестроение (rebuild).
Основными принципами данного метода-перестроения (rebuild()
) являются:
- Следует запрос на перестроение элемента (это займёт большую часть времени), вызывая метод
build()
виджета, на который ссылается этот элемент (= методWidget build (BuildContext context) {...}
). Данный методbuild()
вернёт новый виджет - Если у элемента нет "детей", то для нового виджета создаётся элемент (см. ниже) (прим: inflateWidget), в противном случае
- новый виджет сравнивается с тем, на который ссылается дочерний элемент элемента
- Если они взаимозаменяемы (= тот же тип виджета и ключ), то обновление происходит и дочерний элемент сохраняется.
- Если они не взаимозаменяемы, то дочерний элемент отбрасывается (~ discarded) и для нового виджета создаётся элемент
- Данный новый элемент монтируется как дочерний элемент элемента. (монтируется (mounted) = вставляется в дерево элементов)
Следующая анимация попытается сделать это объяснение немного нагляднее.
Примечание по виджетам и элементам
Для нового виджета создаётся элемент конкретного типа, соответствущего категории виджета, а именно:
- InheritedWidget -> InheritedElement
- StatefulWidget -> StatefulElement
- StatelessWidget -> StatelessElement
- InheritedModel -> InheritedModelElement
- InheritedNotifier -> InheritedNotifierElement
- LeafRenderObjectWidget -> LeafRenderObjectElement
- SingleChildRenderObjectWidget -> SingleChildRenderObjectElement
- MultiChildRenderObjectWidget -> MultiChildRenderObjectElement
- ParentDataWidget -> ParentDataElement
У каждого из этих типов элементов есть свое собственное поведение. Например:
- StatefulElement вызовет метод
widget.createState()
при инициализации, который создаст состояние (State) и свяжет его с элементом - Когда элемент типа RenderObjectElement смонтирован, то он создаёт RenderObject. Этот объект renderObject будет добавлен в Render Tree и связан с элементом.
Шаг 2. renderObjects
Теперь после завершения всех действий, связанных с dirty элементами, Element Tree является стабильным. Поэтому пришло время рассмотреть процесс визуализации.
Поскольку RendererBinding отвечает за обработку Render Tree, WidgetsBinding вызывает метод drawFrame
RendererBinding.
Ниже на частичной диаграмме показана последовательность действий, выполняемых во время запроса drawFrame().
На этом шаге выполняются следующие действия:
- Каждый renderObject, помеченный как dirty, запрашивается для выполнения его компоновки (то есть вычисления его размеров и геометрии)
- Каждый renderObject, помеченный как "нуждающийся в перерисовке", перерисовывается, используя свой метод layer
- Результирующая сцена формируется и отправляется во Flutter Engine, чтобы последний передал ее на экран устройства
- Наконец, также обновляется семантика и отправляется во Flutter Engine
В конце этого потока действий экран устройства обновляется.
Часть 3: Обработка жестов
Жесты (= события, связанные с действиями пальца на стекле) обрабатываются с помощью GestureBinding.
Когда Flutter Engine отправляет информацию о событии, связанном с жестом, через window.onPointerDataPacket API, то GestureBinding перехватывает её, выполняет некоторую буферизацию и:
- преобразует координаты, выдаваемые Flutter Engine, в соответствие с device pixel ratio, а затем
- запрашивает у renderView список всех RenderObjects, которые находятся в части экрана, относящейся к координатам события
- затем проходит по полученному списку renderObjects и отправляет связанное событие каждому из них
- если renderObject "слушает" события такого типа, то он его обрабатывает
Надеюсь, сейчас понятно, насколько важны renderObjects.
Часть 4: Анимации
Эта часть статьи посвящена понятию анимации и глубокому пониманию Ticker.
Когда вы работаете с анимациями, то вы обычно используете AnimationController или любой виджет для анимаций (прим: AnimatedCrossFade).
Во Flutter всё, что связано с анимациями, относится к Ticker. У Ticker, когда он активен, есть только одна задача: "он просит SchedulerBinding зарегистрировать обратный вызов и сообщить Flutter Engine, что надо разбудить его, когда появится новый обратный вызов". Когда Flutter Engine готов, он вызывает SchedulerBinding через запрос: "onBeginFrame". SchedulerBinding обращается к списку обратных вызовов ticker и выполняет каждый из них.
Каждый tick перехватывается "заинтересованным" контроллером для его обработки. Если анимация завершена, то ticker "отключён", иначе ticker запрашивает SchedulerBinding для планирования нового обратного вызова. И так далее...
Полная картина
Теперь мы узнали, как работает Flutter:
BuildContext
Напоследок вернёмся к диаграмме, которая показывает различные типы элементов, и рассмотрим сигнатуру корневого Element:
abstract class Element extends DiagnosticableTree implements BuildContext {
...
}
Мы видим тот самый всем известный BuildContext! Но что это такое?
BuildContext — это интерфейс, определяющий ряд геттеров и методов, которые могут быть реализованы элементом. В основном BuildContext используется в методе build()
StatelessWidget или State для StatefulWidget.
BuildContext — это не что иное, как сам Element, который соответствует
- обновляемому виджету (внутри методов
build
илиbuilder
)- StatefulWidget, связанному со State, в котором вы ссылаетесь на переменную контекста.
Это означает, что большинство разработчиков постоянно работают с элементами, даже не зная об этом.
Насколько полезным может быть BuildContext?
Поскольку BuildContext соответствует элементу, связанному с виджетом, а также местоположению виджета в дереве, то BuildContext может быть полезен, когда надо:
- получить ссылку на объект RenderObject, соответствующий виджету (или, если виджет не является Renderer, то виджету-потомку)
- получить размер RenderObject
- обратиться к дереву. Это используется фактически всеми виджетами, которые обычно реализуют метод
of
(например,MediaQuery.of(context)
,Theme.of(context)
…)
Забавы ради
Теперь, когда мы осознали, что BuildContext – это элемент, я хотел бы показать вам другой способ его использования. Ниже совершенно бесполезный код позволяет StatelessWidget обновить себя так, как если бы это был StatefulWidget, но без использования setState()
, а с помощью BuildContext.
ПРЕДУПРЕЖДЕНИЕ
Пожалуйста, не используйте этот код!
Его единственная задача – продемонстрировать, что StatelessWidget может запрашивать обновление.
Если вам нужно некоторое состояние с виджетом, пожалуйста, используйте StatefulWidget.
void main(){
runApp(MaterialApp(home: TestPage(),));
}
class TestPage extends StatelessWidget {
// final because a Widget is immutable (remember?)
final bag = {"first": true};
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(title: Text('Stateless ??')),
body: Container(
child: Center(
child: GestureDetector(
child: Container(
width: 50.0,
height: 50.0,
color: bag["first"] ? Colors.red : Colors.blue,
),
onTap: (){
bag["first"] = !bag["first"];
//
// This is the trick
//
(context as Element).markNeedsBuild();
}
),
),
),
);
}
}
Если честно, когда вы вызываете setState()
, то в конечном итоге он делает то же самое: _element.markNeedsBuild()
.
Заключение
Вы скажете: "Ещё одна длинная статья". Но я подумал, что вам было бы интересно узнать, как построена архитектура Flutter, и решил напомнить, что всё было разработано, чтобы быть эффективным, масштабируемым и открытым для будущих расширений. Кроме того, ключевые понятия, такие как Widget, Element, BuildContext, RenderObject, не всегда очевидны для восприятия. Могу только надеяться, что эта статья была для вас полезной.
Ждите новых новостей уже скоро. А пока позвольте пожелать вам успешного программирования.
PS Всю критику, вопросы и предложения по переводу буду рад услышать в (личных) сообщениях.
PSS Ещё раз ссылка на оригинальную статью Flutter internals от Didier Boelens, так как одной в шапке перевода для такого большого материала мало)
Комментарии (3)
20912
20.11.2019 17:10+1Отличный перевод. В оригинале несколько витиеватый английский и несколько настораживает рейтинг «для начинающих» (ИМХО: половина этой статьи нужна исключительно НЕначинающим). Хорошо, что такой материал теперь есть на русском.
mkulesh
Спасибо за замечательную статью. Я только-только закончил портирование одного из моих приложений на Flutter. Уже был готов выкатить обновление на Google Play, но на этапе финального тестирования обнаружил огромную проблему. А именно, на некоторых реальных устройствах (у меня это безымянный китайский планшет и ASUS пятилетный давности) производительность рендеринга составляет пару кадров в секунду. О 60 fps речи даже близко нет. То есть все работает, но пользоваться приложением невозможно. И это не только у меня. В трекере Flutter есть очень древняя запись с большим числом комментариев: github.com/flutter/flutter/issues/11861. Ее пару недель назад закрыли, но последнее обновление ситуацию не улучшило. Вы что-нибудь слышали об этой проблеме? Есть идея, почему такая проблема вообще появилась? Ведь, пока такие подножки остаются, говорить о применении Flutter для продуктивной разработки очень сложно. А жаль, так как и идея, и реализация в целом на очень высоком уровне.