Привет всем, меня зовут Гуо Шую, автор серии книг "Полное руководство по разработке приложений с использованием Flutter" на Juejin, а также поддержка открытых проектов серии GSY на GitHub. Серия включает такие проекты как GSYVideoPlayer,
GSYGithubApp
(Flutter, ReactNative, Kotlin, Weex), в четырёх версиях, а также электронные книги GSYFlutterBook. Общее количество звёзд на этих проектах составляет около 25 тысяч, а мой рейтинг среди подписчиков из Китая на GitHub занимает 67 место. Я занимаюсь разработкой мобильных приложений, преимущественно в области переднего края, работаю с такими технологиями, как Android, Flutter, React Native, Weex и мини-приложениями.
Основной темой моего выступления будут следующие вопросы: развитие кросс-платформенной разработки мобильных приложений, реализация принципов работы виджетов Flutter, практикующие навыки разработки с использованием Flutter и текущее состояние Flutter Web. Весь рассказ будет сосредоточен на работе виджетов.
Cordova
, React Native
, Flutter
. Ниже приведена диаграмма развития кросс-платформенной разработки мобильных приложений:Cordova
является наиболее широко используемым кросс-платформенным фреймворком в ранние годы, известным среди фронтенд-разработчиков. Основной принцип его работы заключается в том, что:
Web-код упаковывается локально и загружается через WebView платформы, используя заранее согласованное API JavaScript для взаимодействия с плагинами, предоставляющими возможности платформы.
Cordova
позволяет фронтенд-разработчикам быстро создавать мобильные приложения, получать доступ к платформе и обеспечивать быстрое подключение к таким возможностям, как камера, кэширование данных, чтение и запись файлов.
На ранних этапах рынка мобильных приложений помимо Android и iOS существовали Windows Phone и BlackBerry. Простая и практичная идея
Cordova
сделала его одним из самых популярных кросс-платформенных фреймворков того времени. Даже сегодня, фреймворкIonic
, который был создан на основеCordova
, продолжает развиваться и обновляться.
Cordova
удобен в использовании, его производительность ограничивается возможностями WebView
. Разработчики начали стремиться к более высокой производительности и платформенным особенностям с появлением открытого доступа React Native
, который начал новую волну развития.React Native
позволяет JavaScript-коду выполнять внутри встроенного движка JavaScript (JavaScriptCore) и использовать этот движок для реализации кросс-платформенной возможности. В то же время он преобразует JavaScript-компоненты в нативные компоненты платформы для рендера, что обеспечивает оптимизацию и повышение производительности.
С распространением React
-фреймворка, React Native
стал одним из лучших вариантов расширения способностей разработчиков React
до мобильной разработки. Также React Native
предоставляет отличную возможность для разработчиков приложений познакомиться с фронтендом.
Позднее компания Alibaba представила
Weex
-фреймворк, дизайн которого был похож, но использовал V8 движок для кросс-платформенной работы и концепцию Vue.js. Однако из-за различных причин,Weex
так и не получил широкого распространения.
JS Bridge
также имеет свои ограничения по производительности и другим аспектам, поэтому Facebook активно работает над их решением, например, через HermesJS
и глобальную реорганизацию. Тем не менее, маппинг JS на платформенные контролы приводит к чрезмерному связыванию фреймворка и платформы, что усложняет поддержку версий и системных обновлений.Здесь Google представила Flutter
, которая идёт своим путём, требуя от платформы лишь поверхности (Surface
) и холста (Canvas
). Остальное Flutter
берёт на себя: "Вы можете просто лежать, мы сами всё сделаем".Кросс-платформенная идеология Flutter
быстро сделала её новым лидером, даже старший брат кросс-платформенной разработки — язык JS
— предпочёл игнорировать её, выбрав вместо этого Dart
. Это вызвало много споров во время ранней фазы продвижения Flutter
.
За короткий период времени, без учёта открытых запросов, количество закрытых и открытых задач
Flutter
составило около 18 000 и 8 000 соответственно, что указывает на её популярность и одновременно на проблемы и вызовы, с которыми она сталкивается.Однако одно можно сказать точно:
Flutter
полностью победилаReact Native
по версионности.
Итак, можно заметить, что развитие кросс-платформенной мобильной разработки прошло от простого обёртывания кода до создания высокоэффективных кросс-платформенных контролёров, до современного подхода, где контролёры отделены от платформы. Этот процесс является постоянным поиском производительности, повторного использования и эффективности.#### Вне темы, зачем учиться кросс-платформенным технологиям?
1. Разработка
Можно ли сразу учиться Java/Kotlin, Objective-C/Swift, JavaScript/CSS для написания кода для разных платформ?Конечно, такой подход обеспечивает максимальную производительность, но главное преимущество кросс-платформенного программирования заключается в повторном использовании логики кода, что позволяет снизить затраты на разработку одинаковых функциональностей для различных платформ.
2. Обучение
Обычно разработчики склонны ограничиваться своим специализированием, а как разработчики приложений, кросс-платформенные технологии предоставляют возможность плавного перехода к работе с другой платформой или областью.
Теперь переходим к сегодняшней теме — Flutter. Flutter включает множество компонентов, поэтому в данной статье мы сосредоточимся на одном виджете. Flutter является кросс-платформенной библиотекой UI, где виджет является ключевым элементом.
Flutter представляет собой UI-фреймворк, где всё представлено в виде виджета. Каждый виджет представляет состояние одного кадра, и он является неизменяемым. Как же работает виджет?
На следующей картинке показана простая страница Flutter, содержащая заголовок и контент. Когда страница строится (build
), она отображается на экране. Но как это влияет на производительность? И что такое виджет? Мы постепенно раскроем эти вопросы.
Сначала рассмотрим код на приведённой выше картинке. Этот код больше похож на конфигурационный файл, чем на код уровня представления.
Чтобы понять, как работает виджет, стоит обратить внимание на три ключевых компонента Flutter: виджет, элемент и объект рендера. Эти три компонента вместе обеспечивают базовый цикл рендеринга в Flutter.
Как показано на этой картинке, когда виджет "загружается", он не сразу рисуется, а сначала создаётся его элемент, который затем преобразует конфигурацию виджета в объект рендера для рисования.
Поэтому большую часть времени в Flutter мы пишем виджеты, но роль виджета скорее напоминает конфигурационный файл, тогда как реальная работа выполняется объектами рендера.
Подведём итоги:
Widget
, который мы пишем, должен быть преобразован в соответствующий RenderObject
, чтобы работать;Element
хранит Widget
и RenderObject
, выступая в роли моста между ними и сохраняя некоторые состояния. BuildContext
, который мы часто видим в фреймворке Flutter, является абстракцией Element
;
Widget
в RenderObject
, указывая Canvas
, какую область (Rect
) и размер (Size
) данных он должен отрисовать.Таким образом, Widget
отличается от наших привычных понятий о разметке тем, что Widget
является неизменяемым (immutable
), имеет одну кадровую единицу времени и не является реальным рабочим объектом. При каждом изменении экрана некоторые Widgets
заново строятся методом build
.И вот здесь возникают вопросы о производительности: Как Flutter обеспечивает высокую производительность?
Widget
Это просто вопрос о том, что такое Widget
. Как "конфигурационный файл", изменение Widget
обязательно приведет к созданию новых Element
и RenderObject
?
Ответ – нет, Widget
служит лишь для конфигурирования данных RenderObject
и является очень легковесным.
Однако RenderObject
уже другой случай, так как он связан с реальными операциями размещения (layout
) и рисования (paint
). Можно сказать, что это настоящий "View". Частое создание таких объектов может привести к проблемам с производительностью.
Поэтому в Flutter используется ряд проверок для управления производительностью при переходе от Widget
к RenderObject
. Эти действия обычно выполняются внутри Element
, например, при вызове updateChild
происходит следующее:
- Когда
element.child.widget == widget.build()
, то update
не запускается;
update
, если canUpdate(element.child.widget, newWidget)
возвращает true
, Element
будет обновлен;isRelayoutBoundary
и isRepaintBoundary
, для локального обновления. Например, когда markNeedsPaint()
запускает рисование, через isRepaintBoundary
определяется область обновления, а затем requestVisualUpdate
запускает обновление ниже.> Через параметр isRepaintBoundary
соответствующий RenderObject
может стать частью Layer
.И это отвечает на некоторые вопросы новичков: если вложены много Widget
, будет ли производительность проблемой?
Это демонстрирует отличие Flutter в размещении от других фреймворков — Widget
, который вы пишете, является конфигурационным файлом, а стек с множеством компонентов влияет на конечный RenderObject
лишь увеличивая количество вычислений Offset
и Size
.
С учетом вышеописанного можно понять, что большинство времени Widget
представляет собой легкую конфигурацию. В отношении производительности вам следует больше заботиться о таких действиях, как Clip
, Overlay
, прозрачное соединение и т.д., поскольку они могут вызвать операцию saveLayer
, которая очистит кэширование GPU.
Наконец, вот основные моменты:
Один и тот же Widget
может одновременно описывать несколько узлов дерева рендера, что позволяет использовать его как конфигурационный файл. Обычно отношение между Widget
и RenderObject
многомножественно. (Предполагается наличие RenderObject
для Widget
.)
Element
является конкретной версией Widget
, которая соответствует одному RenderObject
. (Предполагается наличие RenderObject
для Element
.)
Внутренний параметр isRepaintBoundary
в RenderObject
делает возможным создание областей Layer
.
Когда isRepaintBoundary
равен true
, эта область становится обновляемым регионом рендера, и при формировании этой области создается новый Layer
. Однако не каждый RenderObject
имеет свой Layer
, так как это зависит от значения isRepaintBoundary
.
Обратите внимание, что часто используемый в Flutter
BuildContext
фактически является абстракциейElement
. ЧерезBuildContext
мы обычно можем получить доступ кElement
, то есть получить "ключ" к хранилищу, через который можно получить содержимое внутриElement
, такие как ранее упомянутыйRenderObject
, а такжеState
, который будет рассмотрен позже.*
Widget
Здесь мы разделим Widget
на следующие категории: наличие State
и наличие RenderObject
.
На самом деле, можно было бы разделить по типам
RenderBox
иRenderSliver
, но из-за ограничений объема материала это будет рассматриваться позднее.*
В Flutter мы часто используем виджеты StatelessWidget
и StatefulWidget
.
На следующем рисунке показана простая реализация StatelessWidget
. Поскольку Widget
является неизменяемым объектом, переданный текст (text
) определяет содержимое, отображаемое этим виджетом, а сам text
также считается final
.
Обратите внимание на желтый предупреждающий знак в
DemoPage
, это связано с тем, что мы определилиint i = 0
, который не являетсяfinal
. ВStatelessWidget
использование **неfinal
переменных может привести к путанице, так какWidget
сам по себе является неизменяемым объектом.**Ранее мы говорили, что всеWidgets
являются неизменяемыми. На этой основе,State
вStatefulWidget
помогает нам реализовать перерисовкуWidget
между кадрами, то есть при каждом перестроенииWidget
, можно использоватьState
, чтобы снова назначить необходимые конфигурационные данные дляWidget
. Здесь объектState
существует внутри каждогоElement
.
Также, ранее мы говорили, что
BuildContext
внутри Flutter фактически представляет собой абстракциюElement
, что позволяет нам черезcontext
получать информацию внутриElement
, такие какState
,RenderObject
,Widget
.
Widget ancestorWidgetOfExactType;
State ancestorStateOfType;
State rootAncestorStateOfType;
RenderObject ancestorRenderObjectOfType;
На следующем рисунке показано, как текст, хранящийся в State
, помечается как "изменился", когда нажимается кнопка и вызывается метод setState
. Он может активно меняться, сохранять переменные, а не просто находиться в состоянии "только чтение".
В Flutter также различаются контейнерные виджеты и рендеринг виджеты. Общие случаи использования:
Text
, Slider
, ListTile
и другие относятся к рендеринг виджетам, внутренняя структура которых основана на RenderObjectElement
, имеющем параметр RenderObject
.
StatelessWidget
, StatefulWidget
и другие относятся к контейнерным виджетам, внутренняя структура которых основана на ComponentElement
, в котором нет RenderObject
.Поэтому, как контейнерные Widget
, они могут получить доступ к своим RenderObject
только после того, как дерево будет построено, и этот RenderObject
будет принадлежать верхнему уровню рендера.
> Как показано в реализации
findRenderObject
, в конечном итоге получается renderObject
. При встрече с ComponentElement
выполняется метод element.visitChildren(visit);
, рекурсивно продолжая выполнение до тех пор, пока не будет найден RenderObjectElement
, после чего возвращается его renderObject
.
Получение RenderObject
является важной частью Flutter, так как для получения позиций и размеров компонентов требуется обращаться к RenderObject
.
Реализация различных типов RenderObject
в Flutter обычно очень детальна и имеет ограниченную функциональность:
Однако студенты, знакомые с Flutter, должны знать Container
— этот Widget
, однако функциональность Container
не выглядит такой простой. Почему это происходит?
Как показано на следующей диаграмме, Container
действительно является контейнерным виджетом, который просто повторно упаковывает другие "однотипные" виджеты и достигает эффекта "многофункциональности" путём настройки параметров.
**Поэтому при разработке в Flutter мы часто создаем различные шаблоны, такие как Container
, Scaffold
, чтобы обеспечить гибкость и переиспользуемость интерфейса.**Возвращаясь к RenderObject
, фактически RenderObject
находится на более "низком" уровне, поскольку для отображения на экране требуется координатная система и соглашение о макете. Поэтому большинство RenderObject
виджетов являются подклассами RenderBox
(RenderSliver
является исключением), потому что RenderObject
сам реализует базовый layout
и paint
, а для отображения на экране требуются координаты и размеры, которые начинают реализовываться в RenderBox
.
RenderSliver
主要用于滚动组件中的继承使用。
Примером может служить компонент, отрисованный в положении x=10, y=20
, затем размер которого ограничивается родителем. RenderBox
наследует RenderObject
и реализует координатную систему Карно и соглашение о макете на её основе.
Для демонстрации логики реализации подкласса RenderBox
этого Widget
используем Offstage
. Offstage
используется для управления отображением child
, как показано на следующей диаграмме, можно заметить внутреннюю логику RenderOffstage
относительно флага offstage
:
Что такое соглашение о макете в Flutter?
Кратко говоря, это то, как размеры child
и parent
должны отображаться и кто решает область отображения. Уверены, что студенты переходящие с Android на Flutter сталкиваются с вопросом, как следует настроить логику match_parent
и wrap_content
. При анализе простого кода, как показано ниже, возникает вопрос: почему Row
макет не имеет установленного размера, но сам определяет свой размер?
Перевод:
Возвращаясь к RenderObject
, фактически RenderObject
находится на более "низком" уровне, поскольку для отображения на экране требуется координатная система и соглашение о макете. Поэтому большинство RenderObject
виджетов являются подклассами RenderBox
(RenderSliver
является исключением), потому что RenderObject
сам реализует базовый layout
и paint
, а для отображения на экране требуются координаты и размеры, которые начинают реализовываться в RenderBox
.
RenderSliver
主要用于滚动组件中的继承使用。
Примером может служить компонент, отрисованный в положении x=10, y=20
, затем размер которого ограничивается родителем. RenderBox
наследует RenderObject
и реализует координатную систему Карно и соглашение о макете на её основе.
Для демонстрации логики реализации подкласса RenderBox
этого Widget
используем Offstage
. Offstage
используется для управления отображением child
, как показано на следующей диаграмме, можно заметить внутреннюю логику RenderOffstage
относительно флага offstage
:
Каково соглашение о макете в Flutter?
Кратко говоря, это то, как размеры child
и parent
должны отображаться и кто решает область отображения. Уверены, что студенты переходящие с Android на Flutter сталкиваются с вопросом, как следует настроить логику match_parent
и wrap_content
. При анализе простого кода, как показано ниже, возникает вопрос: почему Row
макет не имеет установленного размера, но сам определяет свой размер?
Анализируя исходный код, можно заметить, что часто используемые в Flutter компоненты, такие как Row
и Column
, являются подклассами Flex
. Они просто имеют некоторые базовые конфигурации.
По нашему пониманию, чтобы понять логику реализации Widget
, следует обратиться к его RenderObject
, а в случае с Flex
, это будет RenderFlex
. В этом объекте мы видим следующий фрагмент кода:
Как видно, при выполнении макета RenderFlex
требует, чтобы constraints != null
, то есть на верхнем уровне макета Flex
должны существовать ограничения, иначе произойдет ошибка.
Затем, во время макетирования, направление Row
горизонтальное, поэтому maxMainSize
представляет максимальную ширину родительского макета. Затем, в зависимости от значения параметра mainAxisSize
:
mainAxisSize
равно max
, горизонтальный макет Row
равен maxMainSize
;mainAxisSize
равно min
, горизонтальный макет Row
равен allocatedSize
;maxMainSize
известен как максимальная ширина родительского макета, а allocatedSize
— сумма ширин всех детей. Таким образом, становится очевидным:
**Для Row
, когда mainAxisSize
равно max
, это эквивалентно match_parent
; когда mainAxisSize
равно min
, это эквивалентно wrap_content
.**Что касается высоты crossSize
, она определяется как math.max(crossSize, _getCrossSize(child))
, то есть высота самого высокого ребенка является высотой макета.
Наконец, стоит отметить один важный момент:
Макетирование обычно происходит сверху вниз через передачу Constraints
, а затем вверх через возврат Size
.
Как можно создать собственный RenderObject
макет?
Отбросив все, что Flutter предоставляет нам в виде трех ключевых компонентов — Widget
, Element
и RenderObject
, конечно же, Flutter предлагает множество готовых решений для экономии кода.
Обычно для создания собственного RenderObject
макета: - мы будем наследовать два абстрактных класса — MultiChildRenderObjectWidget
и RenderBox
, чтобы реализовать свои объекты Widget
и RenderObject
;
MultiChildRenderObjectElement
, чтобы связать эти объекты;ContainerRenderObjectMixin
, RenderBoxContainerDefaultsMixin
и ContainerBoxParentData
, которые помогут вам сократить количество кода.**Итог заключается в том, что для Flutter весь экран представляет собой холст, мы используем различные Offset
и Rect
, чтобы определить положение, а затем рисуем с помощью Canvas
. Цель — область всего экрана, весь экран является одним кадром, каждый раз при изменении происходит полное перерисование.> В данном разделе не рассматривается RenderSliver
, его входные и выходные данные отличаются от RenderBox
. Подробнее мы рассмотрим это позже.
InheritedWidget
является одним из ключевых компонентов Flutter.
InheritedWidget
делится между Widget
, но этот Widget
является ProxyWidget
, который сам по себе ничего не рисует, однако он делится данными, сохранёнными внутри этого Widget
, тем самым обеспечивая разделение состояния.
На следующем рисунке показана часто встречающаяся в Flutter тема Theme
, которая реализуется с помощью _InheritedTheme
как InheritedWidget
для глобального разделения темы.
Как же InheritedWidget
обеспечивает глобальное разделение?
На самом деле, внутри Element
есть параметр Map<Type, InheritedElement> _inheritedWidgets;
, который обычно пустой, но инициализируется только тогда, когда родительский контрол или сам контрол являются InheritedWidget
. Когда родительский контрол является InheritedWidget
, этот Map
передаётся и объединяется уровнем ниже.
Поэтому, когда мы вызываем inheritFromWidgetOfExactType
через context
, мы можем использовать этот Map
для поиска вверх, тем самым находя верхний InheritedWidget
.
Например, наши
Theme
/ThemeData
, Text
/DefaultTextStyle
, Slider
/ SliderTheme
и так далее. Как показано на следующих кодовых примерах, мы можем определять глобальные ThemeData
или локальные DefaultTextStyle
, тем самым обеспечивая глобальную и локальную настройку разделения.
Кроме того, большинство компонентов управления состоянием в Flutter также используют
InheritedWidget
для разделения состояния.
Ранее мы говорили, что если Flutter не зависит от нативных контролов, то как можно интегрировать некоторые существующие платформенные контролы? Например, WebView
и Map
? Приведем пример с использованием WebView
:
До официальной поддержки компонента WebView
третьи стороны накладывали новый native контрол на FlutterView
, используя placeholder в Dart для передачи размера и положения.
Как показано на следующей图为:
в Flutter приложении
push
'ится SingleChildRenderObjectWidget
с заранее установленным размером и положением, чтобы получить нужный размер и положение для отображения. Эта информация затем передается через MethodChannel
на native уровень, где WebView
добавляется с указанным размером и положением методом addContentView
.
На этом этапе WebView
и SingleChildRenderObjectWidget
имеют одинаковый размер и положение, а пустое пространство заполняется AppBar
Flutter.
При переходе на другой Flutter экран, WebView
может скрываться; также могут возникнуть проблемы с анимациями открытия страницы, AppBar
и WebView
, поскольку они трудно согласуются между собой.После того, как официальная поддержка WebView
была внедрена, она используется вместе с дизайном PlatformView
, что позволяет интегрировать native компоненты без выхода за рамки Flutter рендера.
Например, на платформе Android используется логика вторичного экрана, создавая виртуальный экран с помощью класса VirtualDisplay
. Для этого требуется вызвать метод createVirtualDisplay()
класса DisplayManager
, который создаёт уникальный textureId
для поверхности Surface
.
Затем этот textureId
передаётся на Dart уровень, где движок рендера использует textureId
для получения уже отрендеренного содержимого из памяти и отрисовки его на AndroidView
.
Одной интересной особенностью Flutter является то, что некоторые ошибки в Dart не приводят к завершению работы приложения, а вместо этого отображаются красным стеком UI. Различные области ошибок могут быть полностью красными или частично красными, что отличает это состояние от "падения" традиционного приложения.
Это удобно во время разработки, но не подходит для выпуска версий в продакшене, поэтому мы обычно предпочитаем использовать кастомные сообщения об ошибках. Как показано ниже, обычно мы можем настроить свои страницы ошибок следующим образом и собирать информацию об ошибках.
Переопределим метод builder
в ErrorWidget
, а затем соберём информацию в области выполнения (Zone
) и вернём свою пользовательскую страницу с информацией об ошибке. В конце концов, используем onError
внутри Zone
для унифицированной обработки ошибок.
Примечание: понятие
Zone
здесь не рассматривается подробно; если вас интересует эта тема, вы можете найти более детальную информацию в предыдущих статьях.
Наконец, коротко поговорим о Flutter Web. Преимуществом Flutter при поддержке веб-платформ является низкая связанность его UI с платформой, а Dart был создан специально для работы в вебе, поэтому совмещение этих технологий было логичным шагом для поддержки Flutter в вебе.
Однако веб-платформа не может обойти JavaScript. На веб-платформе фактический компонент Image
в итоге преобразуется в тег <img>
через dart2js
и отображается через атрибут src
.
Кроме того, наличие ещё одной платформы требует её совместимости. Несмотря на то что многие проблемы уже решены и включены в основной проект, всё ещё остаются вопросы относительно совместимости и производительности. Например, в Flutter Web использование canvas.drawColor(Colors.black, BlendMode.clear)
приведёт к ошибке выполнения, так как режим смешивания BlendMode.clear
не поддерживается.
Полное руководство по разработке на Flutter
Глубокий анализ мобильных кросс-платформенных технологий
Полное сравнение Flutter и React Native
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )