1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-jg-meet.md 47 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Привет всем, меня зовут Гуо Шую, автор серии книг "Полное руководство по разработке приложений с использованием Flutter" на Juejin, а также поддержка открытых проектов серии GSY на GitHub. Серия включает такие проекты как GSYVideoPlayer, GSYGithubApp (Flutter, ReactNative, Kotlin, Weex), в четырёх версиях, а также электронные книги GSYFlutterBook. Общее количество звёзд на этих проектах составляет около 25 тысяч, а мой рейтинг среди подписчиков из Китая на GitHub занимает 67 место. Я занимаюсь разработкой мобильных приложений, преимущественно в области переднего края, работаю с такими технологиями, как Android, Flutter, React Native, Weex и мини-приложениями.

Основной темой моего выступления будут следующие вопросы: развитие кросс-платформенной разработки мобильных приложений, реализация принципов работы виджетов Flutter, практикующие навыки разработки с использованием Flutter и текущее состояние Flutter Web. Весь рассказ будет сосредоточен на работе виджетов.

1. Развитие кросс-платформенной разработки мобильных приложенийКак обычно, мы начнем с исторического обзора. С развитием различных типов устройств пользователя, кросс-платформенная разработка стала одним из самых популярных направлений в мобильной сфере. Разработка кросс-платформенных мобильных приложений представляет собой стремление разработчиков к производительности, повторному использованию и эффективности.Разработка кросс-платформенных мобильных приложений может быть разделена на три этапа, основными представителями которых являются: Cordova, React Native, Flutter. Ниже приведена диаграмма развития кросс-платформенной разработки мобильных приложений:

Cordova

Cordova является наиболее широко используемым кросс-платформенным фреймворком в ранние годы, известным среди фронтенд-разработчиков. Основной принцип его работы заключается в том, что:

Web-код упаковывается локально и загружается через WebView платформы, используя заранее согласованное API JavaScript для взаимодействия с плагинами, предоставляющими возможности платформы.

Cordova позволяет фронтенд-разработчикам быстро создавать мобильные приложения, получать доступ к платформе и обеспечивать быстрое подключение к таким возможностям, как камера, кэширование данных, чтение и запись файлов.

На ранних этапах рынка мобильных приложений помимо Android и iOS существовали Windows Phone и BlackBerry. Простая и практичная идея Cordova сделала его одним из самых популярных кросс-платформенных фреймворков того времени. Даже сегодня, фреймворк Ionic, который был создан на основе Cordova, продолжает развиваться и обновляться.

React NativeХотя Cordova удобен в использовании, его производительность ограничивается возможностями WebView. Разработчики начали стремиться к более высокой производительности и платформенным особенностям с появлением открытого доступа React Native, который начал новую волну развития.

React Native позволяет JavaScript-коду выполнять внутри встроенного движка JavaScript (JavaScriptCore) и использовать этот движок для реализации кросс-платформенной возможности. В то же время он преобразует JavaScript-компоненты в нативные компоненты платформы для рендера, что обеспечивает оптимизацию и повышение производительности.

С распространением React-фреймворка, React Native стал одним из лучших вариантов расширения способностей разработчиков React до мобильной разработки. Также React Native предоставляет отличную возможность для разработчиков приложений познакомиться с фронтендом.

Позднее компания Alibaba представила Weex-фреймворк, дизайн которого был похож, но использовал V8 движок для кросс-платформенной работы и концепцию Vue.js. Однако из-за различных причин, Weex так и не получил широкого распространения.

FlutterНа самом деле, 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, где виджет является ключевым элементом.

2. Принцип работы Flutter виджета

Flutter представляет собой UI-фреймворк, где всё представлено в виде виджета. Каждый виджет представляет состояние одного кадра, и он является неизменяемым. Как же работает виджет?

На следующей картинке показана простая страница Flutter, содержащая заголовок и контент. Когда страница строится (build), она отображается на экране. Но как это влияет на производительность? И что такое виджет? Мы постепенно раскроем эти вопросы.

Пример кода FlutterСначала рассмотрим код на приведённой выше картинке. Этот код больше похож на конфигурационный файл, чем на код уровня представления.

Чтобы понять, как работает виджет, стоит обратить внимание на три ключевых компонента Flutter: виджет, элемент и объект рендера. Эти три компонента вместе обеспечивают базовый цикл рендеринга в Flutter.

Структура Flutter

Как показано на этой картинке, когда виджет "загружается", он не сразу рисуется, а сначала создаётся его элемент, который затем преобразует конфигурацию виджета в объект рендера для рисования.

Поэтому большую часть времени в Flutter мы пишем виджеты, но роль виджета скорее напоминает конфигурационный файл, тогда как реальная работа выполняется объектами рендера.

Подведём итоги:

  • Виджет — это конфигурационный файл.
  • Элемент — это мост и хранилище.
  • Объект рендера — это рендеринг и размещение после парсинга.

Подробное объяснение:

  • Поэтому наш Widget, который мы пишем, должен быть преобразован в соответствующий RenderObject, чтобы работать;
  • Element хранит Widget и RenderObject, выступая в роли моста между ними и сохраняя некоторые состояния. BuildContext, который мы часто видим в фреймворке Flutter, является абстракцией Element;
  • В конце концов, фреймворк преобразует конфигурационные данные Widget в RenderObject, указывая Canvas, какую область (Rect) и размер (Size) данных он должен отрисовать.Таким образом, Widget отличается от наших привычных понятий о разметке тем, что Widget является неизменяемым (immutable), имеет одну кадровую единицу времени и не является реальным рабочим объектом. При каждом изменении экрана некоторые Widgets заново строятся методом build.

И вот здесь возникают вопросы о производительности: Как Flutter обеспечивает высокую производительность?

image

1.1 Легковесность Widget

Это просто вопрос о том, что такое Widget. Как "конфигурационный файл", изменение Widget обязательно приведет к созданию новых Element и RenderObject?

Ответ – нет, Widget служит лишь для конфигурирования данных RenderObject и является очень легковесным.

Однако RenderObject уже другой случай, так как он связан с реальными операциями размещения (layout) и рисования (paint). Можно сказать, что это настоящий "View". Частое создание таких объектов может привести к проблемам с производительностью.

Поэтому в Flutter используется ряд проверок для управления производительностью при переходе от Widget к RenderObject. Эти действия обычно выполняются внутри Element, например, при вызове updateChild происходит следующее:

image- Когда 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, который будет рассмотрен позже.*

1.2 Классификация Widget

Здесь мы разделим Widget на следующие категории: наличие State и наличие RenderObject.

Классификация

На самом деле, можно было бы разделить по типам RenderBox и RenderSliver, но из-за ограничений объема материала это будет рассматриваться позднее.*

1.2.1 Существование состояния (state)

В 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. Он может активно меняться, сохранять переменные, а не просто находиться в состоянии "только чтение".

1.2.2 Контейнерные виджеты/рендеринг виджеты

В Flutter также различаются контейнерные виджеты и рендеринг виджеты. Общие случаи использования:

  • Text, Slider, ListTile и другие относятся к рендеринг виджетам, внутренняя структура которых основана на RenderObjectElement, имеющем параметр RenderObject.

  • StatelessWidget, StatefulWidget и другие относятся к контейнерным виджетам, внутренняя структура которых основана на ComponentElement, в котором нет RenderObject.Поэтому, как контейнерные Widget, они могут получить доступ к своим RenderObject только после того, как дерево будет построено, и этот RenderObject будет принадлежать верхнему уровню рендера.

> Как показано в реализации findRenderObject, в конечном итоге получается renderObject. При встрече с ComponentElement выполняется метод element.visitChildren(visit);, рекурсивно продолжая выполнение до тех пор, пока не будет найден RenderObjectElement, после чего возвращается его renderObject.

Получение RenderObject является важной частью Flutter, так как для получения позиций и размеров компонентов требуется обращаться к RenderObject.

1.3 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. Подробнее мы рассмотрим это позже.

3. Практические советы по использованию Flutter

3.1. InheritedWidget

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 для разделения состояния.

3.2. Поддержка нативных контролов

Ранее мы говорили, что если 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.

3.3 Ошибки

Одной интересной особенностью Flutter является то, что некоторые ошибки в Dart не приводят к завершению работы приложения, а вместо этого отображаются красным стеком UI. Различные области ошибок могут быть полностью красными или частично красными, что отличает это состояние от "падения" традиционного приложения.

Иллюстрация

Это удобно во время разработки, но не подходит для выпуска версий в продакшене, поэтому мы обычно предпочитаем использовать кастомные сообщения об ошибках. Как показано ниже, обычно мы можем настроить свои страницы ошибок следующим образом и собирать информацию об ошибках.

Переопределим метод builder в ErrorWidget, а затем соберём информацию в области выполнения (Zone) и вернём свою пользовательскую страницу с информацией об ошибке. В конце концов, используем onError внутри Zone для унифицированной обработки ошибок.

Примечание: понятие Zone здесь не рассматривается подробно; если вас интересует эта тема, вы можете найти более детальную информацию в предыдущих статьях.

Четвёртый раздел: Flutter Web

Наконец, коротко поговорим о 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 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master