Резюме: В статье будут рассказаны некоторые тонкости платформы Flutter, за счет которых Flutter можно назвать будущим «королем» кроссплатформенных фреймворков.
Кратко про Flutter
Flutter — молодая, но очень перспективная платформа, уже привлекшая к себе внимание крупных компаний, которые запустили свои приложения. Интересна эта платформа своей простотой сравнимой с разработкой веб-приложений, и скоростью работы на равне с нативными приложениями. Высокая производительность приложения и скорость разработки достигается за счет нескольких техник:
В отличии от многих известных на сегодняшний день мобильных платформ, Flutter не использует JavaScript, для написания основного кода приложения, конечно можно имплементировать сторонний виджет написанный на JavaScript, но он не будет влиять на основную часть кода приложения. В качестве языка программирования для Flutter выбрали Dart, который компилируется в бинарный код, за счет чего достигается скорость выполнения операций сравнимая с Objective-C, Swift, Java, или Kotlin.
- Flutter не использует нативные компоненты, опять же, ни в каком виде, так что не приходится писать никаких прослоек для коммуникации с ними. Вместо этого, подобно игровым движкам (а вы ведь знаете что у игр очень динамичный UI), он отрисовывает весь интерфейс самостоятельно. Кнопки, текст, медиа-элементы, фон — все это отрисовывается внутри графического движка в самом Flutter. После вышесказанного стоит отметить, что “Hello World” приложение на Flutter занимает совсем немного места: iOS ≈ 2.5Mb и Android ≈ 4Mb.
- Для построения UI во Flutter используется декларативный подход, вдохновленный вебфреймворком ReactJS, на основе виджетов (в мире веба именуемых компонентами). Для еще большего прироста в скорости работы интерфейса виджеты перерисовываются по необходимости — только когда в них что-то изменилось (подобно тому как это делает Virtual DOM в мире веб-фронтенда).
- В дополнение ко всему, в фреймворк встроен Hot-reload(Моментальная перезагрузка и обновление UI в приложении), такой привычный для веба, и до сих пор отсутствовавший в нативных платформах.
Кратко про Dart
Dart – представляет язык программирования общего назначения от компании Google, который предназначен прежде всего для разработки веб-приложений (как на стороне клиента, так и на стороне сервера) и мобильных приложений. Так же Dart строго- типизированный объектно-ориентированный язык. Все значения, которые используются в программе на Dart, представляют объекты.
В своем развитии Dart испытал влияние более ранних языков, таких как Smalltak, Java, JavaScript. Его синтаксис похож на синтаксис других си-подобных языков.
Самая простая программа написанная на Dart выглядит так:
void main() {
print('Hello, World!');
}
Тонкости Flutter
Агрессивная композитность
Одним из наиболее отличительных аспектов Flutter является его агрессивная компоновка. Виджеты создаются путем составления других виджетов, которые сами по себе строятся из более простых виджетов. Например, Padding – это виджет, а не свойство других виджетов. В результате пользовательские интерфейсы, созданные с помощью Flutter, состоят из множества виджетов.
Рекурсия создания виджетов находится в RenderObjectWidgets, которые являются виджетами, которые создают узлы в нижележащем дереве визуализации. Дерево рендеринга – это структура данных, в которой хранится геометрия пользовательского интерфейса, которая вычисляется во время макета и используется в момент отрисовки и проведения тестирования. Большинство разработчиков Flutter не создают объекты визуализации напрямую, а манипулируют деревом визуализации с помощью виджетов.
Для поддержки агрессивной компоновки на уровне виджетов Flutter использует ряд эффективных алгоритмов и оптимизаций как на уровне виджетов, так и на уровне дерева визуализации, которые описаны в следующих подразделах.
Сублинейный макет
С большим количеством виджетов и объектов рендеринга ключ к хорошей производительности - эффективные алгоритмы. Первостепенное значение имеет производительность макета, который является алгоритмом, который определяет геометрию (например, размер и положение) объектов рендеринга. Некоторые другие наборы инструментов используют алгоритмы компоновки, которые имеют O*(N²) или хуже (например, итерация с фиксированной точкой в некоторой области ограничений). Flutter стремится к линейной производительности для начальной компоновки и производительности сублинейной компоновки в общем случае последующего обновления существующей компоновки. Как правило, количество времени, затрачиваемое на макет, должно масштабироваться медленнее, чем количество объектов рендеринга.
Flutter выполняет одну разметку на кадр, а алгоритм разметки работает за один проход. Ограничения передаются по дереву родительскими объектами, вызывающими метод компоновки для каждого из своих дочерних объектов. Дети(слуги или подвиджеты) рекурсивно выполняют свой собственный макет, а затем возвращают геометрию вверх по дереву, возвращаясь из метода макета. Важно отметить, что после того, как объект рендеринга вернулся из метода макета, этот объект рендеринга не будет посещен снова до макета для следующего кадра. Этот подход сочетает в себе то, что в противном случае может быть отдельной мерой, и макеты проходят в один проход, и в результате каждый объект рендеринга посещается не более двух раз во время макета: один раз по пути вниз по дереву и один раз по пути вверх по дереву.
Flutter имеет несколько специализаций этого общего протокола. Наиболее распространенной специализацией является RenderBox, который работает в двумерных декартовых координатах. В макете блока ограничены минимальная и максимальная ширина, а также минимальная и максимальная высота. Во время размещения дочерний элемент определяет свою геометрию, выбирая размер в этих пределах. После того, как ребенок возвращается из макета, родитель решает положение ребенка в системе координат родителя3. Обратите внимание, что дочерний макет не может зависеть от его положения, поскольку положение не определяется до тех пор, пока дочерний элемент не вернется из макета. В результате родитель может свободно перемещать дочерний элемент без необходимости пересчитывать его макет.
В целом, во время макета единственной информацией, которая передается от родителя к потомку, являются ограничения, а единственной информацией, которая передается от родителя к родителю, является геометрия. Эти инварианты могут уменьшить объем работы, требуемой во время макета:
- Если дочерний элемент не пометил свой собственный макет как грязный, дочерний элемент может немедленно вернуться из макета, отключив прогулку, если родительский элемент предоставляет дочернему элементу те же ограничения, что и дочерний элемент, полученный во время предыдущего макета.
- Всякий раз, когда родитель вызывает метод макета дочернего элемента, родитель указывает, использует ли он информацию о размере, возвращенную дочерним элементом. Если, как это часто бывает, родитель не использует информацию о размере, тогда родителю не нужно пересчитывать свой макет, если дочерний элемент выбирает новый размер, потому что родитель гарантирует, что новый размер будет соответствовать существующим ограничениям.
- К жестким ограничениям относятся те, которые могут быть удовлетворены ровно одной допустимой геометрией. Например, если минимальная и максимальная ширины равны друг другу, а минимальная и максимальная высоты равны друг другу, единственный размер, который удовлетворяет этим ограничениям, равен одному с такой шириной и высотой. Если родитель предоставляет жесткие ограничения, тогда родителю не нужно пересчитывать свой макет всякий раз, когда дочерний элемент пересчитывает свой макет, даже если родитель использует размер дочернего элемента в своем макете, потому что дочерний элемент не может изменить размер без новых ограничений своего родителя.
- Объект рендеринга может объявить, что он использует ограничения, предоставленные родителем, только для определения его геометрии. Такое объявление сообщает платформе, что родителю этого объекта визуализации не нужно повторно вычислять его макет, когда дочерний элемент пересчитывает свой макет, даже если ограничения не являются жесткими и даже если макет родителя зависит от размера дочернего элемента, поскольку дочерний элемент не может измениться размер без новых ограничений от его родителя.
В результате этих оптимизаций, когда дерево объектов рендеринга содержит грязные узлы, только те узлы и ограниченная часть поддерева вокруг них посещаются во время макета. Построение сублинейных виджетов
Подобно алгоритму компоновки, алгоритм построения виджетов Flutter является сублинейным. После сборки виджеты удерживаются деревом элементов, которое сохраняет логическую структуру пользовательского интерфейса. Дерево элементов необходимо, потому что сами виджеты являются неизменяемыми, что означает (среди прочего), что они не могут помнить свои родительские или дочерние отношения с другими виджетами. Дерево элементов также содержит объекты состояния, связанные с виджетами с состоянием.
В ответ на пользовательский ввод (или другие воздействия) элемент может испачкаться, например, если разработчик вызывает setState () для связанного объекта состояния. Фреймворк хранит список грязных элементов и переходит непосредственно к ним на этапе сборки, пропуская чистые элементы. На этапе построения информация течет однонаправленно вниз по дереву элементов, что означает, что каждый элемент посещается не более одного раза на этапе построения. После очистки элемент не может снова стать грязным, потому что по индукции все его элементы-предки также очищаются.
Поскольку виджеты являются неизменяемыми, если элемент не пометил себя как «грязный», элемент может немедленно вернуться из сборки, отключив прогулку, если родительский элемент перестраивает элемент с идентичным виджетом. Кроме того, элемент должен сравнивать только идентичность объекта двух ссылок на виджеты, чтобы установить, что новый виджет совпадает со старым виджетом. Разработчики используют эту оптимизацию для реализации шаблона перепроецирования, в котором виджет включает в себя встроенный дочерний виджет, хранящийся в виде переменной-члена в своей сборке. Во время сборки Flutter также избегает обхода родительской цепочки с помощью InheritedWidgets. Если виджеты обычно обходят свою родительскую цепочку, например, для определения цвета текущей темы, фаза сборки станет глубиной дерева O (N²), которая может быть довольно большой из-за агрессивной композиции. Чтобы избежать этих родительских обходов, инфраструктура помещает информацию в дерево элементов, поддерживая хэш-таблицу InheritedWidgets для каждого элемента. Как правило, многие элементы ссылаются на одну и ту же хеш-таблицу, которая изменяется только в тех элементах, которые представляют новый InheritedWidget.
Линейное согласование
Вопреки распространенному мнению, Flutter не использует алгоритм дерева. Вместо этого платформа решает, использовать ли элементы повторно, изучая дочерний список для каждого элемента независимо, используя алгоритм O (N). Алгоритм согласования дочерних списков оптимизируется для следующих случаев:
- Старый дочерний список пуст.
- Два списка идентичны.
- Существует вставка или удаление одного или нескольких виджетов в одном месте списка.
- Если каждый список содержит виджет с одинаковым ключом5, два виджета совпадают. Общий подход состоит в том, чтобы сопоставить начало и конец обоих дочерних списков путем сравнения типа времени выполнения и ключа каждого виджета, потенциально находя непустой диапазон в середине каждого списка, который содержит все не сопоставленные дочерние элементы. Затем инфраструктура помещает дочерние элементы в диапазоне в старом дочернем списке в хэш-таблицу на основе их ключей. Затем платформа просматривает диапазон в новом дочернем списке и запрашивает хеш-таблицу по ключу для совпадений. Несовпадающие дочерние элементы отбрасываются и восстанавливаются с нуля, тогда как сопоставленные дочерние элементы восстанавливаются с их новыми виджетами.
Оперирование дерева виджетов
Повторное использование элементов важно для производительности, поскольку элементы владеют двумя критическими частями данных: состоянием для виджетов с состоянием и базовыми объектами рендеринга. Когда инфраструктура может повторно использовать элемент, состояние для этой логической части пользовательского интерфейса сохраняется, и информация компоновки, вычисленная ранее, может использоваться повторно, часто избегая полных обходов поддерева. Фактически, повторное использование элементов настолько ценно, что Flutter поддерживает нелокальные мутации дерева, которые сохраняют информацию о состоянии и компоновке.
Разработчики могут выполнить нелокальную мутацию дерева, связав GlobalKey с одним из своих виджетов. Каждый глобальный ключ уникален во всем приложении и регистрируется в хеш-таблице для конкретного потока. На этапе сборки разработчик может переместить виджет с глобальным ключом в произвольное место в дереве элементов. Вместо того, чтобы создавать новый элемент в этом месте, фреймворк проверит хеш-таблицу и переопределяет существующий элемент из своего предыдущего местоположения в его новое местоположение, сохраняя все поддерево.
Объекты рендеринга в переотображенном поддереве могут сохранять информацию о макете, поскольку ограничения макета являются единственной информацией, которая передается от родителя к потомку в дереве рендеринга. Новый родитель помечен как грязный для макета, потому что его дочерний список изменился, но если новый родительский элемент передает дочерний элемент, те же ограничения макета, которые дочерний элемент получил от своего старого родителя, могут немедленно вернуться из макета, отключив обход.
Глобальные ключи и нелокальные мутации дерева широко используются разработчиками для достижения таких эффектов, как переходы героев и навигация.
Оптимизация с постоянным коэффициентом
В дополнение к этим алгоритмическим оптимизациям достижение агрессивной компоновки также зависит от нескольких важных оптимизаций с постоянным коэффициентом. Эти оптимизации наиболее важны на листьях основных алгоритмов, рассмотренных выше.
- Ребенок-модель(Child-model) агностик. В отличие от большинства наборов инструментов, которые используют дочерние списки, дерево рендеринга Flutter не фиксирует конкретную дочернюю модель. Например, класс RenderBox имеет абстрактный метод visitChildren (), а не конкретный интерфейс firstChild и nextSibling. Многие подклассы поддерживают только одного дочернего элемента, который хранится непосредственно как переменную-член, а не список дочерних элементов. Например, RenderPadding поддерживает только один дочерний элемент и, как результат, имеет более простой метод компоновки, выполнение которого занимает меньше времени.
- Визуальное дерево рендеринга, логическое дерево виджетов. В Flutter дерево рендеринга работает в независимой от устройства визуальной системе координат, что означает, что меньшие значения в координате x всегда направлены влево, даже если текущее направление чтения справа налево. Дерево виджетов обычно работает в логических координатах, то есть с начальными и конечными значениями, визуальная интерпретация которых зависит от направления чтения. Преобразование из логических в визуальные координаты выполняется при передаче обслуживания между деревом виджетов и деревом визуализации. Этот подход более эффективен, поскольку вычисления макета и рисования в дереве рендеринга происходят чаще, чем перенос дерева виджетов в рендеринг, и могут избежать повторных преобразований координат.
- Текст обрабатывается специализированным объектом рендеринга. Подавляющее большинство объектов визуализации не знают о сложностях текста. Вместо этого текст обрабатывается специализированным объектом рендеринга RenderParagraph, который является листом в дереве рендеринга. Вместо того чтобы создавать подклассы текстового объекта визуализации, разработчики включают текст в свой пользовательский интерфейс, используя композицию. Этот шаблон означает, что RenderParagraph может избежать повторного вычисления своего текстового макета, если его родительский элемент предоставляет те же ограничения макета, что является обычным даже во время операции на дереве.
- Наблюдаемые объекты. Флаттер использует и модель наблюдения, и реактивные парадигмы. Очевидно, что реактивная парадигма является доминирующей, но Флаттер использует наблюдаемые объекты модели для некоторых конечных структур данных. Например, анимации уведомляют список наблюдателей, когда их значение изменяется. Flutter передает эти наблюдаемые объекты из дерева виджетов в дерево визуализации, которое непосредственно наблюдает за ними и делает недействительным только соответствующую стадию конвейера, когда они изменяются. Например, изменение _Animation_ может вызвать только фазу рисования, а не фазы сборки и рисования.
Взятые вместе и суммированные по большим деревьям, созданным агрессивной композицией, эти оптимизации оказывают существенное влияние на производительность.
Заключение
Исходя из вышеизложенного хочу, заключить, что, будущее кроссплатформенной разработки, именно за платформой Flutter.
В настоящее время все кроссплатформенные фреймворки, не делают акцент на оптимизации и производительности приложения, в основном делают акцент на красоте и все возможных красивых анимаций, эту сторону тоже можно понять, потому как основная задача подобных платформ создать красивый и удобный интерфейс для пользователя и заботиться о скорости и производительности это дело не первое.
Помимо всего этого Flutter и Dart имеют простой синтаксис, который понижает порог вхождения в разработку на данной платформе.
Поэтому смело могу сказать, что будущее кроссплатформенной мобильной разработки стоит именно за этой платформой, и как пример китайское глобальное приложение Alibaba, было переписано на Flutter.
Список использованных источников:
- 1. https://dart.dev/guides/
- 2. https://flutter.dev/docs/
- 3. Flutter in Action (Flutter в действии) 2019г. Eric Windmill
- 4. Dart in Action (Dart в действии) 2016г. Chris Buckett