Pull to refresh

Comments 15

Классная статья. Хотел бы ещё уточнить что у каждого элемента есть свой 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
Спасибо за отзыв и полезное дополнение!
Как я понял, в нем вы имеете в виду именно жизненный цикл State сущности StatefullWidget. Да, все действительно так, как вы описали, и это очень полезный материал о внутреннем устройстве. Но поскольку это все таки частный случай поведения, я лишь вскользь упомянул об этом в статье, чтобы не раздувать статью частностями. Мне хотелось в этой статье подробнее разобрать общий механизм триады Widget-Element-RenderObject. Как я написал в конце статьи, есть планы продолжать подобного рода статьи о внутреннем устройстве Flutter, в рамках которых можно будет подробно рассмотреть подобные частности.

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

Рад, что вам понравилась статья.
Насчет ключа, увы, так сделать не получится. Вы можете легко это проверить просто скопировав мой пример и заменив ключ. Если там будет любой ключ кроме глобального, это не повлияет на переиспользование элементов. Я, до того как решил разобраться, тоже думал что можно будет использовать любой ключ. Когда проверил, был удивлен — не работало. Ни один вариант не приводил к переиспользованию, кроме глобального ключа. Когда я полез разбираться в реализацию, нашел этому объяснение.
Если вы посмотрите в реализацию ключей, то у 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.

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

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


Сейчас разобрался — при изменении уровня вложенности (родителя виджета) на самом деле помочь может только 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."

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

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


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

Объективное сравнение с ReactNative я не смогу провести, потому что я вообще не работал на ReactNative, и уж тем более не разбираюсь в том каким образом он работает на уровне фреймворка. Да, в статье, которую вы бросили, описан похожий алгоритм, но это лишь часть обновления связи Widget — Element во Flutter. Первое что сразу бросается в глаза — dom элемент, это все таки не аналог Widget во Flutter, а нечто более тяжеловесное, как мне кажется. Касаемо Flutter, как я писал в статье, виджеты это очень легкая конфигурация, которая является лишь описанием. Отрисовкой занимается RenderObject. У него есть свой алгоритм построения лейаута, оптимизации отрисовки и тд. В этой статье я не стал все это подробно разбирать, потому что от этого сильно бы увеличился объем статьи, да и в ней я хотел сделать акцент на более общих аспектах устройства фреймворка.
Помимо всего этого Flutter использует Skia для отрисовки, которая тоже позволяет ему довольно продуктивно работать. В свою очередь, насколько я знаю, ReactNative использует для рендеринга схему NativeModule-Bridge-JS. Могу ошибаться, но мне уже этот момент кажется более «дорогим».
В ту же копилку — Flutter приложение при релизе использует AOT компиляцию, ReactNative насколько я понимаю — JIT.
Не сказать что напрямую относится к вопросу, но еще и однопоточность JS тоже тут позитивную роль не сыграет.
Естественно, у Flutter тоже есть рекомендации по тому, как писать более производительные приложения. И этим рекомендациям лучше следовать. Игнорирование их, может привести к просеву производительности, но это скорее всего ошибка разработчика, нежели проблема Flutter.
Если просто, то 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.
В целом соглашусь, концепции очень похожие. Но я бы не стал приравнивать дом и дерево виджетов, вот тут они, как мне кажется, кардинально разнятся. Дом элемент это не только набор свойств, он еще знает своих потомков, в нем есть методы поиска, замены и тд. Это больше похоже на элемент + виджет. Не знаю, отвечает ли дом элемент за отрисовку, поэтому Render Object не буду приплюсовывать. Но даже без него связка а-ля элемент + виджет — это «дорогие» и «тяжелые» объекты. Как раз подобного пытались избежать во Flutter, разделив деревья на 3 параллельных уровня.
И касаемо бриджинга во Flutter, все таки это бриджинг напрямую через плюсовую часть фреймворка. Тут, по-моему не такая потеря производительности все-таки как в RN. Спасибо, что развернули схему, я не очень удачно видимо ее описал выше, действительно получилось похожим на равноценное во Flutter. А еще в дополнение к этой схеме, слышал вроде бы RN по мосту гоняет данные в JSON формате и при увеличении количества данных, мост начинает проседать.


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

Ответ:
виджет — описание элемента. Смотрим исходники и видим:
«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 формате и при увеличении количества данных, мост начинает проседать.

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

Смотрим исходники и видим:
«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) У 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) и я более детально вам скажу куда копать.

Хорошего дня)
Ну это уже как минимум некрасиво. Я не общаюсь с вами с позиции, что вы не разбираетесь полностью в данном вопросе, или я разбираюсь лучше, я лишь предоставляю вам факты и пруфы. Поэтому с вашей стороны строить диалог с позиции того, что вы лучше разбираетесь в данном вопросе неуместно. Как минимум я предоставил пруф в котором разработчики самого фреймворка, обрисовывают схожую с моей картину. То, что она не совпадает с вашим видением не делает ее неправильной. Наверное, они, будучи авторами данного фреймворка, в любом случае разбираются лучше в том как он устроен и чем я и чем вы.
Я вас тоже прошу не вводить всех в заблуждение. Откройте фреймворк и посмотрите внимательнее, желательно не только реализацию 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.

Всего вам доброго.
Only those users with full accounts are able to leave comments. Log in, please.