Flutter под капотом

    Всем привет! Меня зовут Михаил Зотьев, я работаю Flutter-разработчиком в Surf. Мне, как, наверное, большинству других разработчиков, которые работают с Flutter, больше всего нравится то, как просто создавать с его помощью красивые и удобные приложения. Чтобы войти во Flutter разработку, нужно совсем немного времени. Недавно я работал в геймдеве, а теперь полностью переключился на кроссплатформенную мобильную разработку на Flutter.

    В чём простота? С помощью десятка базовых виджетов можно собирать вполне приличные пользовательские интерфейсы. А со временем, когда багаж используемого скопится вполне приличный, вряд ли какая-то задача поставит вас в тупик: будь то необычный дизайн или изощренная анимация. А самое интересное — скорее всего вы сможете этим пользоваться, даже не задумываясь над вопросом: «А как оно вообще работает?».

    Поскольку у Flutter открытые исходники, я решил разобраться с тем, что же там под капотом (on the Dart side of the Force), и поделиться этим с вами.



    Widget


    Все мы не раз слышали фразу от команды разработчиков фреймворка: «Всё во Flutter — это виджеты». Давайте посмотрим, так ли это на самом деле. Для этого обратимся к классу Widget (далее — виджет) и начнем постепенно знакомиться с содержимым.

    Первое, что мы прочитаем в документации к классу:
    Describes the configuration for an [Element].

    Оказывается, сам виджет — это лишь описание некоторого Element (далее — элемент).
    Widgets are the central class hierarchy in the Flutter framework. A widget is an immutable description of part of a user interface. Widgets can be inflated into elements, which manage the underlying render tree.
    Если подытожить, то фраза «Всё во Flutter — это виджеты» — минимальный уровень понимания как всё устроено, чтобы пользоваться Flutter. Виджет является центральным классом в иерархии Flutter. В то же время вокруг него множество дополнительных механизмов, которые помогают фреймворку справляться со своей задачей.

    Итак, мы узнали ещё несколько фактов:

    • виджет — неизменяемое описание части пользовательского интерфейса;
    • виджет связан с некоторым расширенным представлением, которое называется элемент;
    • элемент управляет некоторой сущностью дерева рендеринга.

    Вы наверняка заметили странность. Пользовательский интерфейс и неизменяемость очень плохо вяжутся между собой, я бы даже сказал, что это совсем несовместимые понятия. Но к этому мы ещё вернёмся, когда будет вырисовываться более полная картина устройства мира Flutter, а пока продолжим знакомиться с документацией виджета.
    Widgets themselves have no mutable state (all their fields must be final).
    If you wish to associate mutable state with a widget, consider using a [StatefulWidget], which creates a [State] object (via [StatefulWidget.createState]) whenever it is inflated into an element and incorporated into the tree.
    Этот абзац немного дополняет первый пункт: если нам нужна изменяемая конфигурация, для этого используется специальная сущность State (далее — состояние), которая как раз и описывает текущее состояние этого виджета. Однако, состояние связано не с виджетом, а с его элементным представлением.
    A given widget can be included in the tree zero or more times. In particular a given widget can be placed in the tree multiple times. Each time a widget is placed in the tree, it is inflated into an [Element], which means a widget that is incorporated into the tree multiple times will be inflated multiple times.
    Один и тот же виджет может быть включен в дерево виджетов множество раз, или вовсе не быть включенным. Но каждый раз, когда виджет включается в дерево виджетов, ему сопоставляется элемент.

    Итак, на данном этапе с виджетами почти покончено, давайте подведем итоги:

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

    Element


    Из того, что мы узнали, напрашивается вопрос «Что это за элементы такие, которые всем управляют?» Поступим аналогичным образом — откроем документацию класса Element.
    An instantiation of a [Widget] at a particular location in the tree.
    Элемент — некоторое представление виджета в определенном месте дерева.
    Widgets describe how to configure a subtree but the same widget can be used to configure multiple subtrees simultaneously because widgets are immutable. An [Element] represents the use of a widget to configure a specific location in the tree. Over time, the widget associated with a given element can change, for example, if the parent widget rebuilds and creates a new widget for this location.
    Виджет описывает конфигурацию некоторой части пользовательского интерфейса, но как мы уже знаем, один и тот же виджет может использоваться в разных местах дерева. Каждое такое место будет представлено соответствующим элементом. Но со временем, виджет, который связан с элементом может поменяться. Это означает, что элементы более живучие и продолжают использоваться, лишь обновляя свои связи.

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

    Чтобы понять каким образом это осуществляется, рассмотрим жизненный цикл элемента:

    • Элемент создаётся посредством вызова метода Widget.createElement и конфигурируется экземпляром виджета, у которого был вызван метод.
    • С помощью метода mount созданный элемент добавляется в заданную позицию родительского элемента. При вызове данного метода также ассоциируются дочерние виджеты и элементам сопоставляются объекты дерева рендеринга.
    • Виджет становится активным и должен появиться на экране.
    • В случае изменения виджета, связанного с элементом (например, если родительский элемент изменился), есть несколько вариантов развития событий. Если новый виджет имеет такой же runtimeType и key, то элемент связывается с ним. В противном случае, текущий элемент удаляется из дерева, а для нового виджета создаётся и ассоциируется с ним новый элемент.
    • В случае, если родительский элемент решит удалить дочерний элемент, или промежуточный между ними, это приведет к удалению объекта рендеринга и переместит данный элемент в список неактивных, что приведет к деактивации элемента (вызов метода deactivate).
    • Когда элемент считается неактивным, он не находится на экране. Элемент может находиться в неактивном состоянии только до конца текущего фрейма, если за это время он остается неактивным, он демонтируется (unmount), после этого считается несуществующим и больше не будет включен в дерево.
    • При повторном включении в дерево элементов, например, если элемент или его предки имеют глобальный ключ, он будет удален из списка неактивных элементов, будет вызван метод activate, и рендер объект, сопоставленный данному элементу, снова будет встроен в дерево рендеринга. Это означает, что элемент должен снова появиться на экране.

    В объявлении класса мы видим, что элемент имплементирует BuildContext интерфейс. BuildContext — это нечто, что управляет позицией виджета в дереве виджетов, как следует из его документации. Почти в точности совпадает с описанием элемента. Данный интерфейс используется, чтобы избежать прямого манипулирования элементом, но при этом дать доступ к необходимым методам контекста. Например, findRenderObject, который позволит найти объект дерева рендера, соответствующий данному элементу.

    RenderObject


    Осталось разобраться с последним звеном данной триады — RenderObject. Как следует из названия — это объект дерева визуализации. У него есть родительский объект, а также поле с данными, которое родительский объект использует для хранения специфичной информации, касающейся самого этого объекта, например, его позицию. Данный объект отвечает за реализацию базовых протоколов отрисовки и расположения.

    RenderObject не ограничивает модель использования дочерних объектов: их может не быть, может быть один или множество. Также не ограничивается система позиционирования: картезианская система, полярные координаты, всё это и многое другое доступно для использования. Нет ограничений и на использование протоколов расположения: подстройка по ширине или высоте, ограничение размера, задание размеров и расположения родителем или при необходимости использование данных родительского объекта.

    Картина мира Flutter


    Попытаемся построить общую картину, как всё работает вместе.

    Мы уже отметили выше, виджет — это неизменяемое описание, но пользовательский интерфейс совсем не статичен. Убирается данное несоответствие разделением на 3 уровня объектов и разделение зон ответственности.

    • Дерево виджетов, в зону ответственности которого относится декларирование конфигураций и хранение свойств.
    • Дерево элементов, которые управляют жизненным циклом и связывают виджеты в некоторую иерархию и объекты рендеринга с ними.
    • Дерево объектов визуализации, ответственность которых — отрисовка, учет размеров положения и ограничений.

    image

    Рассмотрим, как выглядят данные деревья на простом примере:

    image

    В данном случае мы имеем некоторый StatelessWidget, обёрнутый в виджет Padding и содержащий внутри текст.

    Давайте поставим себя на место Flutter — нам дали данное дерево виджетов.

    Flutter: «Эй, Padding, мне нужен твой элемент»
    Padding: «Конечно, держи SingleChildRenderObjectElement»

    image

    Flutter: «Элемент, вот твое место, располагайся»
    SingleChildRenderObjectElement: «Ребят, все ок, но мне нужен RenderObject»
    Flutter: «Padding, как тебя вообще отрисовывать?»
    Padding: «Держи, RenderPadding»
    SingleChildRenderObjectElement: «Отлично, приступаю к работе»

    image

    Flutter: «Так, кто там следующий? StatelessWidget, теперь ты давай элемент»
    StatelessWidget: «Вот StatelessElement»
    Flutter: «StatelessElement, ты будешь в подчинении у SingleChildRenderObjectElement, вот место, приступай»
    StatelessElement: «Ок»

    image

    Flutter: «RichText, элементик предъявите, пожалуйста»
    RichText отдает MultiChildRenderObjectElement
    Flutter: «MultiChildRenderObjectElement, вот твоё место, приступай»
    MultiChildRenderObjectElement: «Мне для работы нужен рендер»
    Flutter: «RichText, нам нужен рендер объект»
    RichText: «Вот RenderParagraph»
    Flutter: «RenderParagraph будешь получать указания RenderPadding, а контролировать тебя будет MultiChildRenderObjectElement»
    MultiChildRenderObjectElement: «Теперь всё ок, я готов»

    image

    Наверняка вы зададите закономерный вопрос: «А где объект рендеринга для StatelessWidget, почему его нет, мы же выше определились, что элементы связывают конфигурации с отображением?» Обратим внимание на базовую реализацию метода mount, про который шла речь в этом пункте описания жизненного цикла.

    void mount(Element parent, dynamic newSlot) {
        assert(_debugLifecycleState == _ElementLifecycle.initial);
        assert(widget != null);
        assert(_parent == null);
        assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active);
        assert(slot == null);
        assert(depth == null);
        assert(!_active);
        _parent = parent;
        _slot = newSlot;
        _depth = _parent != null ? _parent.depth + 1 : 1;
        _active = true;
        if (parent != null)
            _owner = parent.owner;
        if (widget.key is GlobalKey) {
            final GlobalKey key = widget.key;
            key._register(this);
        }
        _updateInheritance();
        assert(() {
            _debugLifecycleState = _ElementLifecycle.active;
            return true;
        }());
    }

    Мы не увидим в нём создания объекта рендеринга. Но элемент имплементирует BuildContext, в котором есть метод поиска объекта визуализации findRenderObject, который приведёт нас к следующему геттеру:

    RenderObject get renderObject {
        RenderObject result;
        void visit(Element element) {
            assert(result == null); 
            if (element is RenderObjectElement)
                result = element.renderObject;
            else
                element.visitChildren(visit);
        }
        visit(this);
        return result;
    }

    В базовом случае элемент может не создавать объект рендеринга, это обязаны делать только RenderObjectElement и его наследники, однако в таком случае у элемента на каком-то из уровней вложенности обязательно должен быть дочерний элемент, имеющий объект рендеринга.

    Казалось бы зачем все эти сложности. Целых 3 дерева, разные зоны ответственности и т.д. Ответ довольно прост — именно на этом строится производительность Flutter. Виджеты неизменяемые конфигурации, поэтому довольно часто пересоздаются, но при этом они довольно лёгкие, что не сказывается на производительности. А вот тяжелые элементы Flutter пытается максимально переиспользовать.

    Рассмотрим на примере.

    Текст посередине экрана. Код в данном случае будет выглядеть примерно так:

    body: Center(
        child: Text(“Hello world!”)
    ),

    В данном случае дерево виджетов будет выглядеть следующим образом:

    image

    После того, как Flutter построит все 3 дерева, мы получим следующую картину:

    image

    Что же произойдет, если у нас изменится текст, который мы собираемся отобразить?

    image

    У нас теперь есть новое дерево виджетов. Выше мы говорили про максимально возможное переиспользование элементов. Взглянем на метод класса Widget, под говорящим названием canUpdate.

    static bool canUpdate(Widget oldWidget, Widget newWidget) {
        return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
    }

    Мы проверяем тип предыдущего виджета и нового, а также их ключи. Если они одинаковы, то нет необходимости менять элемент.

    Итак, до обновления первый элемент это Center, после обновления также Center. У обоих нет ключей, полное совпадение. Можем обновить ссылку элемента на новый виджет.

    image

    Но помимо типа и ключа, виджет же является описанием и конфигурацией, и значения параметров, которые необходимы для отображения могли поменяться. Именно поэтому элемент после того, как обновил ссылку на виджет, должен инициировать обновления объекта рендеринга. В случае Center ничего не поменялось, и мы продолжаем сравнивать дальше.

    И снова тип и ключ говорят нам о том, что нет смысла пересоздавать элемент. Текст является наследником StatelessWidget, у него нет прямого объекта отображения.

    image

    Переходи к RichText. Виджет также не поменял свой тип, по ключам нет расхождений. Элемент обновляет свою связь с новым виджетом.

    image

    Связь обновлена, осталось лишь актуализировать свойства. В результате RenderParagraph отобразит новое значение текста.

    image

    И как только подойдёт время следующего фрейма отрисовки, мы увидим ожидаемый нами результат.

    Благодаря подобной работе и достигается такая высокая производительность Flutter.

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

    Рассмотрим пару примеров. И для того, чтобы удостовериться в вышесказанном используем инструмент Android Studio — Flutter Inspector.

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            body: Center(
                child: _isFirst ? first() : second(),
            ),
            floatingActionButton: FloatingActionButton(
                child: Text("Switch"),
                onPressed: () {
                    setState(() {
                        _isFirst = !_isFirst;
                    });
                },
            ),
        );
    }
    
    Widget first() => Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
            Text(
                "test",
                style: TextStyle(fontSize: 25),
            ),
            SizedBox(
                width: 5,
            ),
            Icon(
                Icons.error,
            ),
        ],
    );
    
    Widget second() => Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
            Text(
                "one more test",
                style: TextStyle(fontSize: 25),
            ),
            Padding(
                padding: EdgeInsets.only(left: 5),
            ),
            Icon(
                Icons.error,
            ),
        ],
    );

    В данном случае по нажатии на кнопку один из виджетов поменяется. Посмотрим, что покажет нам инспектор.

    image

    image

    Как мы видим, Flutter пересоздал рендер только для Padding, остальные просто переиспользовал.

    Рассмотри ещё 1 вариант, в котором структура поменяется более глобальным образом — поменяем уровни вложенности.

    Widget second() => Container(child: first(),);

    image

    image

    Несмотря на то, что визуально дерево совершенно не поменялось, были пересозданы элементы и объекты дерева рендеринга. Это произошло, потому что Flutter сравнивает по уровням (в данном случае неважно, что большая часть дерева не изменилась), отсеивание этой части прошло на момент сравнения Container и Row. Однако из этой ситуации можно выкрутиться. В этом нам помогут GlobalKey. Добавим такой ключ для Row.

    var _key = GlobalKey(debugLabel: "testLabel");
    
    Widget first() => Row(
        key: _key,
        …
    );

    image

    image

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

    Вывод


    Мы чуть ближе познакомились с магией Flutter и теперь знаем, что она не только в виджетах.

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

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

    Спасибо за внимание!

    Ресурсы


    Flutter
    «How Flutter renders Widgets» Andrew Fitz Gibbon, Matt Sullivan
    Surf
    Компания

    Комментарии 15

      +2
      Классная статья. Хотел бы ещё уточнить что у каждого элемента есть свой Lifecycle.

      /// Tracks the lifecycle of [State] objects when asserts are enabled.
      enum _StateLifecycle {
      /// The [State] object has been created. [State.initState] is called at this
      /// time.
      created,

      /// The [State.initState] method has been called but the [State] object is
      /// not yet ready to build. [State.didChangeDependencies] is called at this time.
      initialized,

      /// The [State] object is ready to build and [State.dispose] has not yet been
      /// called.
      ready,

      /// The [State.dispose] method has been called and the [State] object is
      /// no longer able to build.
      defunct,
      }


      BuildOwner — отвечает за отслеживание, какие виджеты нуждаются в перестройке, и обрабатывает другие задачи это относится к деревьям виджетов в целом, например к управлению списком неактивных элементов для дерева.

      Вызов метода setState, приводит к вызову у ElementObject метода markNeedsBuild().
      Этот метод в свою очередь помечает ElementObject «dirty». И добавляется в список который хранится в BuildOwner.
      _dirty = true;

      Далее вызывается метод buildScope у BuildOwner который вызывает RenderObjectToWidgetAdapter. Там происходит вызов метода rebuild у каждого ElementObject который стал dirty.
      После этого rebuild вызывает performRebuild в котором происходит проверка canUpdate и вызывается updateChild который в свою очередь возвращает новый ElementObject.

      Далее после того как все новые ElementObject были созданы Flutter SDK начинает рендрить новые элементы в редеете всех виджетов.

      Про алгоритмы рендеринга можно почитать тут: flutter.dev/docs/resources/inside-flutter
        +3
        Спасибо за отзыв и полезное дополнение!
        Как я понял, в нем вы имеете в виду именно жизненный цикл State сущности StatefullWidget. Да, все действительно так, как вы описали, и это очень полезный материал о внутреннем устройстве. Но поскольку это все таки частный случай поведения, я лишь вскользь упомянул об этом в статье, чтобы не раздувать статью частностями. Мне хотелось в этой статье подробнее разобрать общий механизм триады Widget-Element-RenderObject. Как я написал в конце статьи, есть планы продолжать подобного рода статьи о внутреннем устройстве Flutter, в рамках которых можно будет подробно рассмотреть подобные частности.
      0

      Большое спасибо за статью!
      Для переиспользования части дерева можно использовать ValueKey или ObjectKey вместо GlobalKey, чтобы не создавать для Key отдельное поле.

        +1
        Рад, что вам понравилась статья.
        Насчет ключа, увы, так сделать не получится. Вы можете легко это проверить просто скопировав мой пример и заменив ключ. Если там будет любой ключ кроме глобального, это не повлияет на переиспользование элементов. Я, до того как решил разобраться, тоже думал что можно будет использовать любой ключ. Когда проверил, был удивлен — не работало. Ни один вариант не приводил к переиспользованию, кроме глобального ключа. Когда я полез разбираться в реализацию, нашел этому объяснение.
        Если вы посмотрите в реализацию ключей, то у GlobalKey заметите интересную особенность, которой нет у других ключей.
        void _register(Element element)
        В нем происходит регистрация в статической мапе, для соответствия глобальный ключ -> элемент.
        У элемента в методе mount есть следующий код:
        if (widget.key is GlobalKey) {
          final GlobalKey key = widget.key;
          key._register(this);
        }

        Когда мы монтируем в дерево наш элемент мы его регистрируем на соответствие.
        А в методе

        Element inflateWidget(Widget newWidget, dynamic newSlot)

        перед созданием элемента есть проверка — если виджет с глобальным ключом, тогда мы пытаемся его переиспользовать.
        Element inflateWidget(Widget newWidget, dynamic newSlot) {
          assert(newWidget != null);
          final Key key = newWidget.key;
          if (key is GlobalKey) {
            final Element newChild = _retakeInactiveElement(key, newWidget);
            ...
          }
        }


        И там же мы увидим документацию.
        /// If the given widget has a global key and an element already exists that
        /// has a widget with that global key, this function will reuse that element
        /// (potentially grafting it from another location in the tree or reactivating
        /// it from the list of inactive elements) rather than creating a new element.

        Я указывал эту особенность в статье, в описании жизненного цикла.
        При повторном включении в дерево элементов, например, если элемент или его предки имеют глобальный ключ, он будет удален из списка неактивных элементов.
          0

          Ещё раз спасибо за подробное объяснение!


          Сейчас разобрался — при изменении уровня вложенности (родителя виджета) на самом деле помочь может только GlobalKey. Но если виджеты просто меняются местами, оставаясь потомками одного родителя, то для сохранения Elements подойдут и ValueKey.


          Как сказано в этом видео, "Flutter's element-to-widget algorithm looks at one level of tree at a time.", поэтому ValueKey не позволяет сохранить element при изменении уровня вложеннности.


          Демо приложения с использованием ValueKey
          import 'package:flutter/material.dart';
          import 'package:flutter/rendering.dart';
          
          void main() {
            runApp(MyApp());
          }
          
          class MyApp extends StatelessWidget {
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                title: 'Keys & Elements',
                theme: ThemeData(
                  primarySwatch: Colors.blue,
                  visualDensity: VisualDensity.adaptivePlatformDensity,
                ),
                home: MyHomePage(),
              );
            }
          }
          
          class MyHomePage extends StatefulWidget {
            @override
            _MyHomePageState createState() => _MyHomePageState();
          }
          
          class _MyHomePageState extends State<MyHomePage> {
            bool flag = false;
          
            @override
            Widget build(BuildContext context) {
              var children = flag ? [first(), second()] : [second(), first()];
              return Scaffold(
                appBar: AppBar(title: Text("Keys & Elements")),
                body: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: children,
                  ),
                ),
                floatingActionButton: FloatingActionButton(
                  onPressed: () => setState(() => flag = !flag),
                  child: Icon(Icons.swap_horiz),
                ),
              );
            }
          
            Widget first() => Container(
                  key: ValueKey(1),
                  height: 100,
                  width: 100,
                  color: Colors.green,
                );
          
            Widget second() => Transform(
                  key: ValueKey(2),
                  transform: Matrix4.rotationZ(1),
                  child: Container(
                    height: 100,
                    width: 100,
                    color: Colors.orange,
                  ),
                );
          }

          Дальнейшие эксперименты привели меня к тому, что если меняется тип родительского виджета, то снова только GlobalKey не помогает сохранить element дочернего виджета, который не изменился. И это выглядит логично, так как если element родительского виджета сохранить не получилось, то не сохранятся и дочерние elements. В этом плане GlobalKey куда удобнее, но его главный минус в том, что его не получится использовать в StatelessWidget, а также "Global keys are relatively expensive."

            +1
            Да, вы абсолютно правы. И Emily Fortuna в этом видео действительно очень хорошо объясняет то, как Flutter работает с ключами.
            Поскольку поиск в каждый момент времени одноуровневый, мы сначала на одном уровне найдем те элементы которые не потеряли связи, затем попытаемся сопоставить новые виджеты с потерявшими связи элементы, а потом создадим новые. Все же неиспользованные станут неактивными. Поэтому при смене вложенности они уже будут потеряны. Если поменяется тип родительского, то причина ровно в том же. Просто это произойдет на предыдущем уровне. Родительский виджет (под родительским я имею ввиду тот, который расположен на один уровень выше) поменял тип, и не смотря на ключ, не будет создана связь с родительским элементом, соответственно тот, со всеми своими вложенными элементам (фактически ветвь дерева) будет удален. И другого выхода, кроме как воспользоваться механизмом своебразного «кэша» через глобальные ключи не остается.
            Тут стоит отметить, что он тоже имеет довольно много ограничений, которые стоит учитывать.
            Вы правильно заметили — GlobalKey довольно дорого обходятся.
            Помимо этого, неактивные элементы в этом кэше будут жить всего лишь до конца фрейма, если они не стали активными к этому моменту, они считаются больше не существующими. Когда вызывается buildOwner.finalizeTree() во время отрисовки фрейма, берется список неактивных элементов и вызывается unmount. И в нем уже, если есть глобальный ключ будет вызвана дерегистрация.
            Ну и конечно — это проблема менеджмента. Неаккуратно использовав GlobalKey механизм, можно легко получить ошибку из-за того что несколько виджетов с одним GlobalKey включены в дерево дважды.
            static void _debugVerifyIllFatedPopulation()
            содержит
            information.add(ErrorSummary('Multiple widgets used the same GlobalKey.'));
            подобный код, если проверка на дубликаты провалится.
        0
        Но помимо типа и ключа, виджет же является описанием и конфигурацией, и значения параметров, которые необходимы для отображения могли поменяться. Именно поэтому элемент после того, как обновил ссылку на виджет, должен инициировать обновления объекта рендеринга. В случае Center ничего не поменялось, и мы продолжаем сравнивать дальше.

        Подождите, но это же… VirtualDom / reconcillation?
        И там вроде не то чтобы всё хорошо иногда бывает с производительностью сравнивания глубоко вложенных деревьев с большим количеством элементов, нет?


        Было бы интересно узнать, какие дополнительные эвристики используются для ускорения этого процесса, ну или в чём отличие рендеринга флаттера от аналогичного в реакте, что делает его быстрее (если это вообще так)

          0
          Объективное сравнение с ReactNative я не смогу провести, потому что я вообще не работал на ReactNative, и уж тем более не разбираюсь в том каким образом он работает на уровне фреймворка. Да, в статье, которую вы бросили, описан похожий алгоритм, но это лишь часть обновления связи Widget — Element во Flutter. Первое что сразу бросается в глаза — dom элемент, это все таки не аналог Widget во Flutter, а нечто более тяжеловесное, как мне кажется. Касаемо Flutter, как я писал в статье, виджеты это очень легкая конфигурация, которая является лишь описанием. Отрисовкой занимается RenderObject. У него есть свой алгоритм построения лейаута, оптимизации отрисовки и тд. В этой статье я не стал все это подробно разбирать, потому что от этого сильно бы увеличился объем статьи, да и в ней я хотел сделать акцент на более общих аспектах устройства фреймворка.
          Помимо всего этого Flutter использует Skia для отрисовки, которая тоже позволяет ему довольно продуктивно работать. В свою очередь, насколько я знаю, ReactNative использует для рендеринга схему NativeModule-Bridge-JS. Могу ошибаться, но мне уже этот момент кажется более «дорогим».
          В ту же копилку — Flutter приложение при релизе использует AOT компиляцию, ReactNative насколько я понимаю — JIT.
          Не сказать что напрямую относится к вопросу, но еще и однопоточность JS тоже тут позитивную роль не сыграет.
          Естественно, у Flutter тоже есть рекомендации по тому, как писать более производительные приложения. И этим рекомендациям лучше следовать. Игнорирование их, может привести к просеву производительности, но это скорее всего ошибка разработчика, нежели проблема Flutter.
            +1
            Если просто, то RN и Flutter очень похожи по концепциям. Когда общался с React разработчиками у себя в компании, то различия очень маленькие. В React/RN есть дом, в Flutter есть дерево виджетов. Ключевая разница в методе отрисовки UI компонентов.

            В Flutter есть тоже бриджинг для прокалывания данных: Flutter -> Native -> Skia Canvas.

            Но разница в том что Flutter сразу рисует всё на канаве, а вот RN прокидывает данные о UI так: RN -> Native -> Compose Native UI -> Render Native with Skia Android.
              0
              В целом соглашусь, концепции очень похожие. Но я бы не стал приравнивать дом и дерево виджетов, вот тут они, как мне кажется, кардинально разнятся. Дом элемент это не только набор свойств, он еще знает своих потомков, в нем есть методы поиска, замены и тд. Это больше похоже на элемент + виджет. Не знаю, отвечает ли дом элемент за отрисовку, поэтому Render Object не буду приплюсовывать. Но даже без него связка а-ля элемент + виджет — это «дорогие» и «тяжелые» объекты. Как раз подобного пытались избежать во Flutter, разделив деревья на 3 параллельных уровня.
              И касаемо бриджинга во Flutter, все таки это бриджинг напрямую через плюсовую часть фреймворка. Тут, по-моему не такая потеря производительности все-таки как в RN. Спасибо, что развернули схему, я не очень удачно видимо ее описал выше, действительно получилось похожим на равноценное во Flutter. А еще в дополнение к этой схеме, слышал вроде бы RN по мосту гоняет данные в JSON формате и при увеличении количества данных, мост начинает проседать.
                0


                Это больше похоже на элемент + виджет.

                Ответ:
                виджет — описание элемента. Смотрим исходники и видим:
                «Widget — Describes the configuration for an [Element].»
                Виджеты были сделаны только для того чтобы абстрагировать программистов от жизненного цикла и канаваса. Глупо их просто разделять, так как вообще Element это одно из филдов в abstract class Widget.



                Но даже без него связка а-ля элемент + виджет — это «дорогие» и «тяжелые» объекты.

                Ответ: С чего вы это взяли? Откуда у вас такие данные? Можно больше данных?


                Как раз подобного пытались избежать во Flutter, разделив деревья на 3 параллельных уровня.

                Ответ: нет никаких 3-х параллельных деревьев.
                Это все статьи сделаны для более простого понимания устройства фреймворка.
                Тем более Element есть только у StatefulWidget.

                А вот если посмотреть то:
                StatefulWidget — это виджет, у него есть State.
                В State храниться Element.
                BuildContext — это на самом деле и есть наш Element, но в нём при помощи приватных методов скрыта многая функциональность.

                А где же RenderObject???

                Да вот на самом деле есть ещё RenderObjectWidget и RenderObjectElement. У меня как у программиста нет к ним доступа, так как это приватные классы.

                Так вот эти RenderObjectWidget — это наследник Widget, так же как и StatefulWidget.

                А вот RenderObject один из филдов в RenderObjectElement. У каждого наследника RenderObject переопределяется метод paint который и вызывает рисование на Canvas.

                У Canvas есть native вызовы, которые стучатся в canvas.cc в Flutter Engine.

                Так что рассказы про то что в Flutter 3 параллельных дерева — это полный буллщит.
                Дерево — это на самом деле не больше чем один большой объект в который вложены другие. там сразу и widget и element и render object.
                это вы можете проверить просто вызвав метод debugDumpApp()


                А еще в дополнение к этой схеме, слышал вроде бы RN по мосту гоняет данные в JSON формате и при увеличении количества данных, мост начинает проседать.

                Ответ: в точку!!!

                  +1
                  Смотрим исходники и видим:
                  «Widget — Describes the configuration for an [Element].»
                  Глупо их просто разделять, так как вообще Element это одно из филдов в abstract class Widget.

                  В части того что написано в исходниках, вы абсолютно правы. Виджет это описание, а элемент, это уже экземпляр этого виджета в дереве.
                  Если можно провести такую аналогию из общего программирования — виджет это класс (просто описание некоторой сущности), а элемент — это конкретная инстанция такого класса.
                  Мы можем взять один виджет и декларативно его вставить множество раз, и на каждое такое вставление будет создан свой Элемент, то есть экземпляр виджета уже в дереве, конкретная сущность.

                  Кстати в Widget нет поля Element, там есть метод для создания Элемента, если опять возвращаться к моей грубой аналогии — конструктор этого класса. Это вот как раз связано с тем что я писал выше. Виджет не знает ничего про элементы. Потому что сколько раз бы он не был вставлен, он будет создавать новые экземпляры. Это как сам класс не знает про существование всех экземпляров которые созданы с его типом.

                  С чего вы это взяли? Откуда у вас такие данные? Можно больше данных?

                  В целом, я пока пытался разобраться с исходниками уже склонялся к этим выводам, но это не является пруфом даже для меня самого. Окончательно я в этом убедился, когда услышал данный факт от разработчиков флаттер команды. В материалах я указал видео «How Flutter renders Widgets» в нем Andrew Fitz Gibbon, Matt Sullivan очень классно изложили это тему, оно мне помогло до конца привести понимание того в чем я пытался разобраться в какую-то структуру. И я очень многим вдохновился из него при написании статьи. Всем советую, очень крутой доклад, стоит посмотреть.

                  нет никаких 3-х параллельных деревьев.
                  Это все статьи сделаны для более простого понимания устройства фреймворка.

                  Вот с этим не соглашусь — такие абстракции не рождаются без причины.
                  Дерево виджетов — мы объявляем его просто декларативно. Плюс подтверждение из кода
                  abstract class Widget extends DiagnosticableTree

                  Дерево элементов строится по виджетам и там в базовом классе как раз тоже
                  abstract class Element extends DiagnosticableTree implements BuildContext

                  Посмотрим базовый класс
                  abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget

                  Это тоже дерево.
                  Так же у него есть метод
                  /// Calls visitor for each immediate child of this render object.
                  ///
                  /// Override in subclasses with children and call the visitor for each child.
                  void visitChildren(RenderObjectVisitor visitor) { }

                  Он знает своих потомков и обрабатывает их.

                  Касаемо StatefulWidget — это частный случай. Нельзя только на нем строить представление о всем устройстве. Хотя его работа из общего устройства и не выбивается, а лишь немного расширяет. И опять же давайте подробно посмотрим на устройство.

                  Вы писали:
                  StatefulWidget — это виджет, у него есть State.

                  Вы не совсем правы — нет у него стейта. Он умеет его создать и отдать.
                  Вот подтверждение — весь код
                  StatefulWidget
                  abstract class StatefulWidget extends Widget {
                    /// Initializes [key] for subclasses.
                    const StatefulWidget({ Key key }) : super(key: key);
                  
                    /// Creates a [StatefulElement] to manage this widget's location in the tree.
                    ///
                    /// It is uncommon for subclasses to override this method.
                    @override
                    StatefulElement createElement() => StatefulElement(this);
                  
                    /// Creates the mutable state for this widget at a given location in the tree.
                    ///
                    /// Subclasses should override this method to return a newly created
                    /// instance of their associated [State] subclass:
                    ///
                    /// ```dart
                    /// @override
                    /// _MyState createState() => _MyState();
                    /// ```
                    ///
                    /// The framework can call this method multiple times over the lifetime of
                    /// a [StatefulWidget]. For example, if the widget is inserted into the tree
                    /// in multiple locations, the framework will create a separate [State] object
                    /// for each location. Similarly, if the widget is removed from the tree and
                    /// later inserted into the tree again, the framework will call [createState]
                    /// again to create a fresh [State] object, simplifying the lifecycle of
                    /// [State] objects.
                    @protected
                    State createState();
                  }
                  


                  Не хранит он свой стейт — хранится стейт уже в
                  Элементе
                  /// An [Element] that uses a [StatefulWidget] as its configuration.
                  class StatefulElement extends ComponentElement {
                    /// Creates an element that uses the given widget as its configuration.
                    StatefulElement(StatefulWidget widget)
                        : _state = widget.createState(),
                          super(widget) {
                      assert(() {
                        if (!_state._debugTypesAreRight(widget)) {
                          throw FlutterError.fromParts(<DiagnosticsNode>[
                            ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
                            ErrorDescription(
                              'The createState function for ${widget.runtimeType} returned a state '
                              'of type ${_state.runtimeType}, which is not a subtype of '
                              'State<${widget.runtimeType}>, violating the contract for createState.'
                            ),
                          ]);
                        }
                        return true;
                      }());
                      assert(_state._element == null);
                      _state._element = this;
                      assert(
                        _state._widget == null,
                        'The createState function for $widget returned an old or invalid state '
                        'instance: ${_state._widget}, which is not null, violating the contract '
                        'for createState.',
                      );
                      _state._widget = widget;
                      assert(_state._debugLifecycleState == _StateLifecycle.created);
                    }
                  }
                  


                  Более того, тут видно что сам Стейт знает про виджет который его создал, а виджет про него опять же нет. Стейт знает про элемент, и почему тоже написано в документации к исходникам (в классе State).
                  BuildContext get context
                  /// The location in the tree where this widget builds.
                  ///
                  /// The framework associates [State] objects with a [BuildContext] after
                  /// creating them with [StatefulWidget.createState] and before calling
                  /// [initState]. The association is permanent: the [State] object will never
                  /// change its [BuildContext]. However, the [BuildContext] itself can be moved
                  /// around the tree.
                  ///
                  /// After calling [dispose], the framework severs the [State] object's
                  /// connection with the [BuildContext].
                  BuildContext get context => _element;
                  



                  BuildContext — это на самом деле и есть наш Element, но в нём при помощи приватных методов скрыта многая функциональность.
                  А вот тут абсолютно правильно, почему так, я пояснил выше.

                  Да вот на самом деле есть ещё RenderObjectWidget и RenderObjectElement. У меня как у программиста нет к ним доступа, так как это приватные классы.


                  Ну почему же.
                  /// RenderObjectWidgets provide the configuration for [RenderObjectElement]s,
                  /// which wrap [RenderObject]s, which provide the actual rendering of the
                  /// application.
                  abstract class RenderObjectWidget extends Widget


                  abstract class RenderObjectElement extends Element


                  Есть они, и наследуются именно от базовых, про что я и говорил — не стоит фокусироваться на частности StatefulWidget.
                  Более того, вы ими пользуетесь постоянно. Тот же Padding это по факту именно RenderObjectWidget.

                  А то что вы можете унаследоваться от RenderObjectWidget и сделать любое кастомное отображение — только плюс Flutter. Мы недавно проводили вебинар, и когда я готовил приложение к нему, встретил кейс с отображением внутренней тени. Так вот я его решил комбинацией обычных контейнеров с декорациями, а мог сделать то же самое написав кастомный виджет, который бы отрисовывал тень на канве.
                  Вот за одно это можно уже любить Flutter — можно сделать виджет любой сложности, вопрос понимания того как все работает и наличия времени.

                  это вы можете проверить просто вызвав метод debugDumpApp()

                  А вот за это, огромное спасибо — никогда не пользовался данной возможностью, на досуге обязательно попробую и разберусь с этим.
                  Пока увы ничего не могу сказать по нему.
                  Кстати если уж пошел разговор об инструментах — та же студия в инспекторе тоже бьет на несколько деревьев — Widgets и RenderTree.
                    –1
                    Я не буду с вами спорить. Советую более детально изучить Фреймворк. Видно что вы верхнеуровневого глянули на исходники и не видите всей картины.

                    Просто просьба не вводить людей в заблуждение:
                    1) У Flutter нет 3х параллельных деревьев! Это просто вложенные друг в друга абстракции. Созданы для разделения обязанностей между собой.
                    2) Где что храниться?
                    — Кол флу такой что создаётся StatefulWidget у него BuildOwner вызывает метод createElement и передаёт туда ссылку на виджет с помощью this.
                    Дальше в кострукторе StatefulElement вызывает метод createState у виджета.
                    Дальше в State прикидывает себя и виджет при помощи всё того же this ``` _state._element = this;``` и ``` _state._widget = widget;```.

                    Таким образом:
                    StatefulElement знает о widget и state и о своём родителе.
                    State знает о widget и element.
                    StatefuWidget — хранит ключ (Key).

                    3) Доступа к RenderObjectWidgets RenderObjectElement у меня нет.
                    То что вы привели — это просто абстракции, я говорил про конкретный пример.
                    Например _CupertinoDialogRenderWidget()
                    Я реализовывал полностью на канве свой StatefulWidget и прекрасно знаю как это всё устроено.

                    4) ```Глупо их просто разделять, так как вообще Element это одно из филдов в abstract class Widget.```
                    Тут согласен, имел ввиду конечно же State.

                    4) и опять к 3м параллельным деревьям
                    То что вы привели пример c абстрактными классами, это забавно. Изучите более детально.

                    Если у вас есть желание то можете со мной связаться в телеграм(kaparray) и я более детально вам скажу куда копать.

                    Хорошего дня)
                      +1
                      Ну это уже как минимум некрасиво. Я не общаюсь с вами с позиции, что вы не разбираетесь полностью в данном вопросе, или я разбираюсь лучше, я лишь предоставляю вам факты и пруфы. Поэтому с вашей стороны строить диалог с позиции того, что вы лучше разбираетесь в данном вопросе неуместно. Как минимум я предоставил пруф в котором разработчики самого фреймворка, обрисовывают схожую с моей картину. То, что она не совпадает с вашим видением не делает ее неправильной. Наверное, они, будучи авторами данного фреймворка, в любом случае разбираются лучше в том как он устроен и чем я и чем вы.
                      Я вас тоже прошу не вводить всех в заблуждение. Откройте фреймворк и посмотрите внимательнее, желательно не только реализацию StatefulWidget. Те абстрактные классы — это есть базовая реализация всех остальных элементов, виджетов, рендеров, потому что все частные случаи наследуются именно от них.

                      Касаемо 3 деревьев. Еще раз повторюсь.
                      Первое дерево — дерево виджетов, вы объявляете декларативно. То есть описываете структуру, вложенности данных виджетов. Иными словами описываете дерево. И передаете это дерево, точнее верхний его узел в метод runApp.

                      Сразу прочитаем документацию и найдем там.
                      ///  * [WidgetsBinding.attachRootWidget], which creates the root widget for the
                      ///    widget hierarchy.
                      ///  * [RenderObjectToWidgetAdapter.attachToRenderTree], which creates the root
                      ///    element for the element hierarchy.
                      ///  * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to
                      ///    ensure the widget, element, and render trees are all built.
                      


                      Казалось бы уже достаточно, но мы все же посмотрим на код.

                      В этом методе происходит связь через биндинг (фактически прослойка между высокоуровневой частью фреймворка, написанной на дарт и движком фреймворка). И у этого биндинга вызывается метод привязки, который приводит нас к

                        /// Takes a widget and attaches it to the [renderViewElement], creating it if
                        /// necessary.
                        ///
                        /// This is called by [runApp] to configure the widget tree.
                        ///
                        /// See also [RenderObjectToWidgetAdapter.attachToRenderTree].
                        void attachRootWidget(Widget rootWidget) {
                          _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
                            container: renderView,
                            debugShortDescription: '[root]',
                            child: rootWidget,
                          ).attachToRenderTree(buildOwner, renderViewElement);
                        }
                      


                      Идем по ссылке в документации и смотрим, что же это за поле

                        /// The [Element] that is at the root of the hierarchy (and which wraps the
                        /// [RenderView] object at the root of the rendering hierarchy).
                        ///
                        /// This is initialized the first time [runApp] is called.
                        Element get renderViewElement => _renderViewElement;
                      


                      Да, это рут дерева элементов, вот уже вторая иерархия, то есть второе дерево.

                      А теперь внимательно посмотрим что происходит в этом методе. Там создается специальный виджет — рут для дерева которое мы передали.

                      Элемент, который он для себя создает это
                      RenderObjectToWidgetElement<T>(this);


                      Смотрим его.

                      /// A [RootRenderObjectElement] that is hosted by a [RenderObject].
                      ///
                      /// This element class is the instantiation of a [RenderObjectToWidgetAdapter]
                      /// widget. It can be used only as the root of an [Element] tree (it cannot be
                      /// mounted into another [Element]; it's parent must be null).
                      ///
                      /// In typical usage, it will be instantiated for a [RenderObjectToWidgetAdapter]
                      /// whose container is the [RenderView] that connects to the Flutter engine. In
                      /// this usage, it is normally instantiated by the bootstrapping logic in the
                      /// [WidgetsFlutterBinding] singleton created by [runApp].
                      class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObjectElement
                      


                      Обратите внимание на вот эту строчку. It can be used only as the root of an [Element] tree. Может быть использован только в качестве корневого объекта дерева элементов.

                      А еще в RenderObjectToWidgetAdapter, это тот, который специальный рут дерева виджетов есть такой интересный метод.
                        /// Inflate this widget and actually set the resulting [RenderObject] as the
                        /// child of [container].
                        ///
                        /// If `element` is null, this function will create a new element. Otherwise,
                        /// the given element will have an update scheduled to switch to this widget.
                        ///
                        /// Used by [runApp] to bootstrap applications.
                        RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
                      


                      Присоединить данный виджет к дереву рендера. А что мы там передаем ему в контейнер — renderView.

                      /// The render tree that's attached to the output surface.
                        RenderView get renderView => _pipelineOwner.rootNode;
                      


                      Вот вам еще одно подтверждение того, что деревьев реально 3.
                      1 декларативно объявлено, 2 других созданы и находятся в WidgetsBinding.

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

                      Касаемо вашего второго пункта. Еще раз советую изучить вопрос в целом, а не в частности — StatefulWidget это далеко не весь фреймворк, даже не вся его высокоуровневая часть, поэтому делать выводы на основе частного случая — плохая практика. StatefulElement, который является его элементным представлением является лишь одним из компонентных элементов. Это означает, что его виджет представление самостоятельно не отрисовывает ничего, а лишь компонует другие.
                      В противовес таким есть RenderObjectElement, которые должны именно отрисоваться. Их виджеты как раз создают RenderObject. И они не StatefulWidget и даже не StatelessWidget, они RenderObjectWidget. И их множество, и вы ими пользуетесь. Тот же Opacity, ну нет там никаких стейтов, как бы вы их там не искали, поэтому флоу который описан у вас не верен. А вот флоу, описанный мной вполне укладывается.

                      То что вы привели — это просто абстракции, я говорил про конкретный пример.

                      Это не просто некоторые абстракции — это базовые классы, от них вы можете унаследоваться, от них унаследованы кучу виджетов которые вы используете повсеместно, выше уже писал пример. А _CupertinoDialogRenderWidget, ну да приватный класс, что в этом для вас удивительного?

                      Я реализовывал полностью на канве свой StatefulWidget и прекрасно знаю как это всё устроено.
                      StatefulWidget вы реализовывали на канвасе? Компоновочный виджет вы реализовывали на канвасе? А можете пример кода скинуть, пожалуйста, вот честно заинтересовали.
                      Я бы понял если вы сказали что вы реализовали RenderObjectWidget свой кастомный. Уже тут стоит все таки задуматься, так ли вы хорошо разбираетесь в устройстве фреймворка, как об этом пишите, без обид.

                      Я не возвожу в абсолют, что я великолепно на 100 процентов знаю, как Flutter устроен, я прекрасно понимаю, что могу в чем то ошибаться, я все таки человек а не машина, и это чужой проект. Если вы действительно можете опровергнуть, то что я разобрал в статье и комментариях, пожалуйста, сделайте это с пруфами, подробно, хотя бы на уровне того, что я сейчас написал. Я с удовольствием почерпну новую информацию, которую пропустил. Если же нет, то не вижу смысла продолжать дискуссию. В случае если хотите обсудить какую-то часть данного вопроса лично, то мой телеграмм mbixjkee.

                      Всего вам доброго.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое