А вам не кажется, что вы попросту переизобрели flutter_hooks?
Нет не кажется, я был бы очень растроен если бы сделал что-то вроде flutter_hooks. Выкладывать бы точно не стал, скрыл бы и никому не показывал.
Только хуки от Реми намного логичнее и проще в использовании.
Хуки от Реми это, как бы помягче, весьма странный велосипед который идет в разрез с логикой работы Flutter. Если реально интересно в чем там проблемы, советую не статьи читать про то какие они классные, а залезть в исходники его либы и хорошенько познакомиться с тем что там и как работает. Но если лень, то на самом деле не раз уже обсуждалось почему эти хуки не то, что стоит использовать — из примеров подобных обсуждений, которое у меня под рукой и могу поделиться — в чате моего канала t.me/chat_ohmyflutter/1680 вот где-то тут примерно оно начинается.
Всё то же самое: кастомная обертка над виджетом, переиспользование UI-логики (а бизнес-логика остается на откуп программисту).
А можно подробнее, где именно у меня вы это нашли? Ну кроме того что бизнес логика отдается на откуп разработчика. Я имею ввиду кастомную обертку над виджетом (и каким), переиспользование UI-логики.
Мы в Surf с уважением относимся к творению дядюшки Боба и стараемся придерживаться в построении наших проектов. Elementary в данном случае лишь часть ее, как вы правильно заметили, близкая к ui. Если интересно подробнее, то скоро на канале SurfTech на ютубе выйдет серия шоу Записки юного техлида, в котором мы с Женей Сатуровым обсуждаем деятельность техлида и платформенной команды нашего отдела. Серия будет посвящена шаблону для приложений который мы используем. В рамках этого обсуждения можно будет узнать больше и задать какие-то вопросы в комментариях, если они появятся, на которые мы ответим.
Наверное, мне не стоит пересказывать доклад Кирилла и эту ветвь дискуссии не стоит развивать, если и вы не посмотрите его. Не хотел бы переврать тезисы.
В любом случае заинтересовали и теперь в список докладов, которые не успел посмотреть сразу на DartUP и посмотрю позже добавился доклад Кирилла.
Касаемо моего решения, 1 из важных моментов было чтобы Elementary не просто следовал MVVM, а еще и укладывался в том числе в логику работы Flutter. В статье я как раз про это буду рассказывать. Ну и собственно если посмотрите исходники - он укладывается, используется для представления интерфейса виджет, источником правды для которого является сущность с презентационным состоянием и презентационной логикой. И живет она вместе с Element-ом. Вобщем реализация настолько близка к Flutter (к Stateful виджету если быть точнее) что местами они как два брата близнеца.
В докладах Surf лишь кратко упоминалась причина разработки MWWM
Ну мы кстати довольно много про это говорили в различных выступлениях. Не было блока когда отдел создавался. Да и если бы был скорее всего ничего бы не поменялось. 1ое чисто наша история - нужна была легкая конверсия разработчиков из андроида и потенциальная назад если ничего не получится. 2 - блок это часть бизнес логики, стейт машина по конкретному кейсу. Стейтмашина это только часть вашей архитектуры. А учитывая что мобилки это в большинстве тонкие клиенты - порой тот же блок на мой взгляд в огромном количестве кейсов просто избыточен. Есть кейсы когда он жизненно необходим, но это не запросы которые мы по сети делаем. В случае что MWWM что Elementary это более широкие вопросы чем просто организация стейтмашины, к слову я вообще туда не стал тянуть ее. Как мне кажется разработчик достаточно самостоятелен чтоб ему не диктовали сверху как ему писать бизнес логику в конкретной ситуации.
Приходят ли вам на ум статьи, где вы сравниваете Elementary с другими пакетами?
Вот тут вряд ли) мне это чужеродно, даже если я буду делать это объективно, это не будет выглядеть таковым. При этом я не люблю искать сор в чужом глазу, поэтому тыкать в проблемы каких то решений ну как-то не по мне. У любого решения есть сильные и слабые стороны. Если вам говорят что оно идеально, save some ram и в 9 раз быстрее всего остального, есть большая вероятность что вас обманывают. Поэтому я могу лишь честно анализировать свое решение. В нем есть слабая сторона - многословность. Поэтому мы создали тулинг чтобы уменьшить действие этой слабости. Опять же Elementary это инструмент, которым надо понять как пользоваться.
Здравствуйте, я как автор библиотеки тоже немного подключусь к обсуждению. Скоро выйдет статья в которой я многие из этих проблем расскажу и опишу подробнее, но пока что вкратце постараюсь ответить.
На недавнем DartUp был доклад о том, что в случае Flutter mvvm - это антипаттерн, так как эта архитектура создана, чтобы решать проблему, которой нет во Flutter.
А какой конкретно проблемы нет во Flutter? Нет необходимости иметь презентационную логику, или презентационное состояние? Есть. Иначе зачем было бы разработчикам добавлять например StatefulWidget? Как раз для того что есть такая необходимость, стейт просто берет этот момент на себя. И для небольших приложений вам вообще ничего не нужно, ни стейтмашины отдельные, ни либы которые вам позволят имплементировать конкретные паттерны.
Касаемо самого MVVM - он хорош удобен и полезен, когда у нас есть возможность реагировать на изменение свойств в отображении и у Flutter эта возможность есть.
Правильно ли я понимаю ваш тезис о тестировании, как главный и единственный аргумент за предлагаемый подход?
Нет не едиснтвенный - разделение отвтетственностей делает код чище, проще, в том числе и проще для тестирования. Ну и естественно за это приходится платить тем что нужно написать чуть больше сущностей. Это решаемая проблема - как темплейтами, так и мы уже провели работу над этим и добавили плагины которые генерируют бойлерплейт код, разработчику нужно лишь писать действительно ценное содержание.
Ваш пакет Elementary, который вы продвигаете, наследует MWWM. Это просто переименование или всё же обратно несовместимые пакеты?
Это разные пакеты, но в Elementary я постарался забрать удобные вещи из mwwm на основе опыта личного использования и избежать болей которые могли возникать при его использовании. Не смотря на то что это разные пакеты, миграция проходит довольно легко, @vlkonoshenko может это подтвердить, у него есть такой опыт.
Если так, то пишете ли вы сами UI-тесты в таком объёме, чтобы оправдать сопутствующие издержки?
Да мы активно пишем как юнит тесты, так и виджет/голден. Чуть реже используется e2e тестирование, но в данном случае не разработчиками а qa отделом. А издержки не такие большие от многословности, как вам кажется, а с учетом автоматизаций они становятся еще меньше. При этом Elementary изначально проектировался с таким учетом, чтобы тесты было писать просто, быстро и это лишь подталкивало к тому чтобы это делать еще полнее и качественнее на проектах.
Выбор стейтменеджмента советую осуществлять из нужд и особенностей проекта, или личных предпочтений, если речь идет про пет. У любой архитектуры есть и плюсы и минусы. К тому же стоит еще учитывать размер и состав команды, ее бэкграунд.
Riverpod вообще ни разу не использовал. Bloc пробовал в пет проекте, вполне приятная, понятная и простая архитектура. В бизнес разработке в Surf мы используем свою архитектуру — mwwm.
dolpheen Спасибо за отзыв! Рад что серия нравится, есть планы продолжать ее дальше. Надеюсь для сообщества будет полезной.
С вашим комментарием согласен, многие решения действительно очень похожи, ну это и не удивительно — нет смысла изобретать велосипед, когда есть уже рабочей подход, который можно просто модернизировать, улучшать в каких то узких моментах.
В полной мере от социальной инженерии невозможно защититься.
Именно про это и речь, нельзя полагаться на сознательность пользователя. Поэтому самостоятельное определение того, что является доверенной зоной приложением, а не системой, является частой реализацией пиннинга. Нужно использовать все возможности которые доступны для защиты. Это как с входной дверью — вы можете держать ее открытой, закрыть, или поставить как в банковском хранилище, и в любом случае будет возможность, что к вам проберутся. Но оставлять дверь открытой не самый лучший выход, правда?
По поводу сертификата буду благодарен за сравнение кода с примером сертификата и кода сертификата как list, в том смысле насколько они разные/узнаваемые и тяжело ли будет их найти.
Вы можете просто почитать про AOT компиляцию. Если вкратце, то декомпиляция данного кода невозможна, возможно дизассемблирование и при этом придется приложить достаточно много усилий. Да, это не стопроцентная гарантия от взлома, но это уже далеко не простая задача.
Кстати Flutter использует AOT только для релизной сборки, при debug сборке используется JIT компиляция, и вот оттуда можно довольно легко выдернуть сам код. Это на случай, если вы решите попробовать декомпилировать приложение.
От митма спасут сами ОС которые не дадут установить контакт если сертификат не подходит/произошел даунгрейд, пытаются подменить.
Тут вы скорее неправы, ОС спасут только в 1 случае — если пользователь не начнет чудить. Допустим он самостоятельно может поставить сертификат злоумышленника себе в систему. К слову, мы недавно проходили пин-тесты в одном из бизнес проектов. Так вот в требованиях по безопасности было четко прописано, что приложение должно корректно защититься в случае если «злоумышленник с помощью методов социальной инженерии убедит пользователя установить скомпрометированный сертификат». И это абсолютно правильно — никогда не надо полагаться на пользователя. Закон Мёрфи — всё, что может пойти не так, пойдёт не так.
Если ваше приложение захотят атаковать надо просто заменить файл сертификата и пересобарать приложение, на андроиде это делалось за пару минут.
Вы недостаточно внимательно прочитали статью, я писал об этом. В той реализации которую предлагаю я, для этого придется приложить довольно много усилий, потому что сертификат вшит в код, а не положен в ресурсы. А код в релизной сборке Flutter компилируется AOT способом.
А что вы собираетесь делать если истечет срок жизни сертификата, о таких вещах частенько забывают?
С этим соглашусь, тут стоит учитывать время жизни сертификата и подобрать оптимальное решение для вас. Обеспечить ко времени его протухания обновление вашего приложения у пользователей. Или если вы не можете гарантировать подобного, стоит смотреть в направлении установки цепочек доверенных сертификатов. Принцип в целом тот же — вы настраиваете внутри приложения свою среду «доверия/недоверия» не полагаясь на системные настройки.
Извините, а можете пояснить подробнее? Вы в целом алгоритмы SSL и TLS считаете ненадежными, или же ситуацию когда пользователь старается испортить себе жизнь? Если второе, так я вроде это и написал — пользователь сам с удовольствием ищет себе проблемы, поэтому мы и вынуждены реализовывать pinning на уровне приложения.
А во второй части — полностью с Вами согласен. Разработчикам стоит не только задумываться о безопасности на уровне приложения, но и том, как именно уведомить пользователя о потенциальной атаке. А еще желательно предоставить ему в таком случае инструкцию как поступить в максимально простом виде. Я бы даже сказал, что это продолжение механизма защиты от атаки. Если не объяснить пользователю что он в опасности — он продолжит и обязательно найдет себе неприятности, например таким образом, который вы описали.
Ну это уже как минимум некрасиво. Я не общаюсь с вами с позиции, что вы не разбираетесь полностью в данном вопросе, или я разбираюсь лучше, я лишь предоставляю вам факты и пруфы. Поэтому с вашей стороны строить диалог с позиции того, что вы лучше разбираетесь в данном вопросе неуместно. Как минимум я предоставил пруф в котором разработчики самого фреймворка, обрисовывают схожую с моей картину. То, что она не совпадает с вашим видением не делает ее неправильной. Наверное, они, будучи авторами данного фреймворка, в любом случае разбираются лучше в том как он устроен и чем я и чем вы.
Я вас тоже прошу не вводить всех в заблуждение. Откройте фреймворк и посмотрите внимательнее, желательно не только реализацию 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.
Смотрим исходники и видим:
«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.
В целом соглашусь, концепции очень похожие. Но я бы не стал приравнивать дом и дерево виджетов, вот тут они, как мне кажется, кардинально разнятся. Дом элемент это не только набор свойств, он еще знает своих потомков, в нем есть методы поиска, замены и тд. Это больше похоже на элемент + виджет. Не знаю, отвечает ли дом элемент за отрисовку, поэтому Render Object не буду приплюсовывать. Но даже без него связка а-ля элемент + виджет — это «дорогие» и «тяжелые» объекты. Как раз подобного пытались избежать во Flutter, разделив деревья на 3 параллельных уровня.
И касаемо бриджинга во Flutter, все таки это бриджинг напрямую через плюсовую часть фреймворка. Тут, по-моему не такая потеря производительности все-таки как в RN. Спасибо, что развернули схему, я не очень удачно видимо ее описал выше, действительно получилось похожим на равноценное во Flutter. А еще в дополнение к этой схеме, слышал вроде бы RN по мосту гоняет данные в JSON формате и при увеличении количества данных, мост начинает проседать.
Объективное сравнение с ReactNative я не смогу провести, потому что я вообще не работал на ReactNative, и уж тем более не разбираюсь в том каким образом он работает на уровне фреймворка. Да, в статье, которую вы бросили, описан похожий алгоритм, но это лишь часть обновления связи Widget — Element во Flutter. Первое что сразу бросается в глаза — dom элемент, это все таки не аналог Widget во Flutter, а нечто более тяжеловесное, как мне кажется. Касаемо Flutter, как я писал в статье, виджеты это очень легкая конфигурация, которая является лишь описанием. Отрисовкой занимается RenderObject. У него есть свой алгоритм построения лейаута, оптимизации отрисовки и тд. В этой статье я не стал все это подробно разбирать, потому что от этого сильно бы увеличился объем статьи, да и в ней я хотел сделать акцент на более общих аспектах устройства фреймворка.
Помимо всего этого Flutter использует Skia для отрисовки, которая тоже позволяет ему довольно продуктивно работать. В свою очередь, насколько я знаю, ReactNative использует для рендеринга схему NativeModule-Bridge-JS. Могу ошибаться, но мне уже этот момент кажется более «дорогим».
В ту же копилку — Flutter приложение при релизе использует AOT компиляцию, ReactNative насколько я понимаю — JIT.
Не сказать что напрямую относится к вопросу, но еще и однопоточность JS тоже тут позитивную роль не сыграет.
Естественно, у Flutter тоже есть рекомендации по тому, как писать более производительные приложения. И этим рекомендациям лучше следовать. Игнорирование их, может привести к просеву производительности, но это скорее всего ошибка разработчика, нежели проблема Flutter.
Да, вы абсолютно правы. И Emily Fortuna в этом видео действительно очень хорошо объясняет то, как Flutter работает с ключами.
Поскольку поиск в каждый момент времени одноуровневый, мы сначала на одном уровне найдем те элементы которые не потеряли связи, затем попытаемся сопоставить новые виджеты с потерявшими связи элементы, а потом создадим новые. Все же неиспользованные станут неактивными. Поэтому при смене вложенности они уже будут потеряны. Если поменяется тип родительского, то причина ровно в том же. Просто это произойдет на предыдущем уровне. Родительский виджет (под родительским я имею ввиду тот, который расположен на один уровень выше) поменял тип, и не смотря на ключ, не будет создана связь с родительским элементом, соответственно тот, со всеми своими вложенными элементам (фактически ветвь дерева) будет удален. И другого выхода, кроме как воспользоваться механизмом своебразного «кэша» через глобальные ключи не остается.
Тут стоит отметить, что он тоже имеет довольно много ограничений, которые стоит учитывать.
Вы правильно заметили — GlobalKey довольно дорого обходятся.
Помимо этого, неактивные элементы в этом кэше будут жить всего лишь до конца фрейма, если они не стали активными к этому моменту, они считаются больше не существующими. Когда вызывается buildOwner.finalizeTree() во время отрисовки фрейма, берется список неактивных элементов и вызывается unmount. И в нем уже, если есть глобальный ключ будет вызвана дерегистрация.
Ну и конечно — это проблема менеджмента. Неаккуратно использовав GlobalKey механизм, можно легко получить ошибку из-за того что несколько виджетов с одним GlobalKey включены в дерево дважды. static void _debugVerifyIllFatedPopulation()
содержит information.add(ErrorSummary('Multiple widgets used the same GlobalKey.'));
подобный код, если проверка на дубликаты провалится.
Рад, что вам понравилась статья.
Насчет ключа, увы, так сделать не получится. Вы можете легко это проверить просто скопировав мой пример и заменив ключ. Если там будет любой ключ кроме глобального, это не повлияет на переиспользование элементов. Я, до того как решил разобраться, тоже думал что можно будет использовать любой ключ. Когда проверил, был удивлен — не работало. Ни один вариант не приводил к переиспользованию, кроме глобального ключа. Когда я полез разбираться в реализацию, нашел этому объяснение.
Если вы посмотрите в реализацию ключей, то у 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.
Я указывал эту особенность в статье, в описании жизненного цикла.
При повторном включении в дерево элементов, например, если элемент или его предки имеют глобальный ключ, он будет удален из списка неактивных элементов.
Спасибо за отзыв и полезное дополнение!
Как я понял, в нем вы имеете в виду именно жизненный цикл State сущности StatefullWidget. Да, все действительно так, как вы описали, и это очень полезный материал о внутреннем устройстве. Но поскольку это все таки частный случай поведения, я лишь вскользь упомянул об этом в статье, чтобы не раздувать статью частностями. Мне хотелось в этой статье подробнее разобрать общий механизм триады Widget-Element-RenderObject. Как я написал в конце статьи, есть планы продолжать подобного рода статьи о внутреннем устройстве Flutter, в рамках которых можно будет подробно рассмотреть подобные частности.
Куда уж дальше от реальной разработки, чем тот же пример из флаттера. Согласен, лучше закончить эту дискуссию, видимо Вы из тех зашоренных людей, у которых все вокруг «неправы, один он Д'Артаньян».
Но с Вашей стороны я не вижу потребности это понять
Зря Вы считаете, что я не понимаю то что вы написали, у меня тоже достаточно опыта реальной разработки. И я не пытаюсь доказать что пример ваш не правилен, я пытался Вам показать, что возведение в абсолют чего-либо это неправильная позиция, и узкость. Но увы, видимо Вы не имеете нужды это увидеть, Вам в своём мирке уютно, и ничего кроме него Вы не хотите видеть. Это Ваше право. На этом всё, в одном мы сошлись — каждый при своём.
Только вот незадача, киллер помимо того что Shooter он еще и Bandit и немного болтает на фене, как и Thief который от того же Bandit наследуется, но предвкушая наследование бандита от стрелка, Thief очень искусный щипач и с его слов «в гробу он эту стрельбу видел». А биатлонист, который Shooter и еще спортсмен, который по дефолту имеет метод тренироваться и хранит медальки. А еще у нас есть стрелок стендовик, который тоже спортсмен и тоже стреляет, но увы без лыж, говорит не удобно ему в них стрелять. И бухгалтер Оля, которая в свободное время любит ходить в тир, и жизни своей без того чтоб засадить фигурке медведя куда-нибудь не представляет. И да, конечно же у нас есть интерфейсы, я не оспариваю что интерфейсы это хорошо, это очень удобно и сам их юзаю. Пусть в жизни они все стреляют по разному, кто-то человечков валит, кто-то в мишени палит, а кто-то по тарелочкам садит. Но вот что если на уровне абстракции который выбран нами, вся эта стрельба имеет абсолютно одинаковую реализацию. Мы получаем дубль абсолютно одинакового кода, который мы пишем реализуя интерфейсы. Так что функционал существенный, но для всех один и тот же. Кстати использование примеси не исключает использование интерфейсов, это к слову.
А касаемо примера из Флаттера, ну вы бы хотя бы открыли что ли его для приличия. Ну право, он ограничение на State имеет, ну причем здесь виджеты. И юзается в куче его наследников, в том числе те которые напрямую относятся к стейтам для которых анимация не важное а существенное свойство, например ImplicitlyAnimatedWidgetState. Это даже из названия следует. И в таких для которых это не является существенным свойством, например _HelperErrorState. Видимо команде флаттера надо как можно скорее переписывать эту часть, ведь она не покрывается вашим случаем на все случаи жизни.
Допустим все предыдущие статьи плохо охватывали материал и вы решили восполнить это, вот конкретная ваша фраза:
в каких случаях их использование более предпочтительно, чем обычное наследование или реализация интерфейсов. Эта статья является попыткой восполнить этот пробел.
В итоге вы приводите всего 1 пример и делаете вывод:
если имеется несколько различных иерархий, которым нужно добавить один и тот же функционал, определяющий некоторое несущественное свойство для сущностей этих иерархий. Либо это может быть одна иерархия, но мы имеем дело с разными ее ветками.
То есть вынесение важной части функционала, которая по факту одинакова у обоих элементов вы считаете не возможным?
Киллер и биатлонист, оба стреляют у обоих это важное свойство, невозможно юзать миксин?
Слишком абстрактно?
Я привел пример, который вы судя по всему даже не открыли, раз пишите что я не представляю вам конструктивной критики.
Вы указали как тег своей статьи Флаттер в том числе. Наверное реализации в самом Флаттере тоже могут быть аргументом в таком случае.
Я привел вам пример SingleTickerProviderStateMixin это довольно важная часть функционала по реализации анимаций, и это миксин можно прицепить к различным стейтам.
Анимация для визуального класса не значимая вещь?
И да, я не предлагаю все подряд выносить в миксины, это то еще зло в красивой обертке. Любым инструментом надо пользоваться аккуратно и к месту, можно и фугасом попытаться гвоздь забить, может получиться. А может и нет.
Следующий момент, касаемо целей — вы поставили цель восполнить пробел, ну так показали бы разные кейсы, а то выходит кейс 1 со спорным выводом. Так и позиционировали бы статью как один из кейсов который мне кажется правильным.
А по поводу критики моего примера, я и не приводил его как вариант реального использования или великолепной архитектуры, я приводил его как абстрактный пример, для демонстрации конкретной ситуации, то что вы этого не увидели и захотели в нем видеть что-то другое, это как говорил один футболист ваши ожидания и ваши проблемы.
Нет не кажется, я был бы очень растроен если бы сделал что-то вроде flutter_hooks. Выкладывать бы точно не стал, скрыл бы и никому не показывал.
Хуки от Реми это, как бы помягче, весьма странный велосипед который идет в разрез с логикой работы Flutter. Если реально интересно в чем там проблемы, советую не статьи читать про то какие они классные, а залезть в исходники его либы и хорошенько познакомиться с тем что там и как работает. Но если лень, то на самом деле не раз уже обсуждалось почему эти хуки не то, что стоит использовать — из примеров подобных обсуждений, которое у меня под рукой и могу поделиться — в чате моего канала t.me/chat_ohmyflutter/1680 вот где-то тут примерно оно начинается.
А можно подробнее, где именно у меня вы это нашли? Ну кроме того что бизнес логика отдается на откуп разработчика. Я имею ввиду кастомную обертку над виджетом (и каким), переиспользование UI-логики.
Спасибо за фидбкек!
В любом случае заинтересовали и теперь в список докладов, которые не успел посмотреть сразу на DartUP и посмотрю позже добавился доклад Кирилла.
Касаемо моего решения, 1 из важных моментов было чтобы Elementary не просто следовал MVVM, а еще и укладывался в том числе в логику работы Flutter. В статье я как раз про это буду рассказывать. Ну и собственно если посмотрите исходники - он укладывается, используется для представления интерфейса виджет, источником правды для которого является сущность с презентационным состоянием и презентационной логикой. И живет она вместе с Element-ом. Вобщем реализация настолько близка к Flutter (к Stateful виджету если быть точнее) что местами они как два брата близнеца.
Ну мы кстати довольно много про это говорили в различных выступлениях. Не было блока когда отдел создавался. Да и если бы был скорее всего ничего бы не поменялось. 1ое чисто наша история - нужна была легкая конверсия разработчиков из андроида и потенциальная назад если ничего не получится. 2 - блок это часть бизнес логики, стейт машина по конкретному кейсу. Стейтмашина это только часть вашей архитектуры. А учитывая что мобилки это в большинстве тонкие клиенты - порой тот же блок на мой взгляд в огромном количестве кейсов просто избыточен. Есть кейсы когда он жизненно необходим, но это не запросы которые мы по сети делаем. В случае что MWWM что Elementary это более широкие вопросы чем просто организация стейтмашины, к слову я вообще туда не стал тянуть ее. Как мне кажется разработчик достаточно самостоятелен чтоб ему не диктовали сверху как ему писать бизнес логику в конкретной ситуации.
Вот тут вряд ли) мне это чужеродно, даже если я буду делать это объективно, это не будет выглядеть таковым. При этом я не люблю искать сор в чужом глазу, поэтому тыкать в проблемы каких то решений ну как-то не по мне. У любого решения есть сильные и слабые стороны. Если вам говорят что оно идеально, save some ram и в 9 раз быстрее всего остального, есть большая вероятность что вас обманывают. Поэтому я могу лишь честно анализировать свое решение. В нем есть слабая сторона - многословность. Поэтому мы создали тулинг чтобы уменьшить действие этой слабости. Опять же Elementary это инструмент, которым надо понять как пользоваться.
Здравствуйте, я как автор библиотеки тоже немного подключусь к обсуждению. Скоро выйдет статья в которой я многие из этих проблем расскажу и опишу подробнее, но пока что вкратце постараюсь ответить.
А какой конкретно проблемы нет во Flutter? Нет необходимости иметь презентационную логику, или презентационное состояние? Есть. Иначе зачем было бы разработчикам добавлять например StatefulWidget? Как раз для того что есть такая необходимость, стейт просто берет этот момент на себя. И для небольших приложений вам вообще ничего не нужно, ни стейтмашины отдельные, ни либы которые вам позволят имплементировать конкретные паттерны.
Касаемо самого MVVM - он хорош удобен и полезен, когда у нас есть возможность реагировать на изменение свойств в отображении и у Flutter эта возможность есть.
Нет не едиснтвенный - разделение отвтетственностей делает код чище, проще, в том числе и проще для тестирования. Ну и естественно за это приходится платить тем что нужно написать чуть больше сущностей. Это решаемая проблема - как темплейтами, так и мы уже провели работу над этим и добавили плагины которые генерируют бойлерплейт код, разработчику нужно лишь писать действительно ценное содержание.
Это разные пакеты, но в Elementary я постарался забрать удобные вещи из mwwm на основе опыта личного использования и избежать болей которые могли возникать при его использовании. Не смотря на то что это разные пакеты, миграция проходит довольно легко, @vlkonoshenko может это подтвердить, у него есть такой опыт.
Да мы активно пишем как юнит тесты, так и виджет/голден. Чуть реже используется e2e тестирование, но в данном случае не разработчиками а qa отделом. А издержки не такие большие от многословности, как вам кажется, а с учетом автоматизаций они становятся еще меньше. При этом Elementary изначально проектировался с таким учетом, чтобы тесты было писать просто, быстро и это лишь подталкивало к тому чтобы это делать еще полнее и качественнее на проектах.
Riverpod вообще ни разу не использовал. Bloc пробовал в пет проекте, вполне приятная, понятная и простая архитектура. В бизнес разработке в Surf мы используем свою архитектуру — mwwm.
С вашим комментарием согласен, многие решения действительно очень похожи, ну это и не удивительно — нет смысла изобретать велосипед, когда есть уже рабочей подход, который можно просто модернизировать, улучшать в каких то узких моментах.
Именно про это и речь, нельзя полагаться на сознательность пользователя. Поэтому самостоятельное определение того, что является доверенной зоной приложением, а не системой, является частой реализацией пиннинга. Нужно использовать все возможности которые доступны для защиты. Это как с входной дверью — вы можете держать ее открытой, закрыть, или поставить как в банковском хранилище, и в любом случае будет возможность, что к вам проберутся. Но оставлять дверь открытой не самый лучший выход, правда?
Вы можете просто почитать про AOT компиляцию. Если вкратце, то декомпиляция данного кода невозможна, возможно дизассемблирование и при этом придется приложить достаточно много усилий. Да, это не стопроцентная гарантия от взлома, но это уже далеко не простая задача.
Кстати Flutter использует AOT только для релизной сборки, при debug сборке используется JIT компиляция, и вот оттуда можно довольно легко выдернуть сам код. Это на случай, если вы решите попробовать декомпилировать приложение.
Тут вы скорее неправы, ОС спасут только в 1 случае — если пользователь не начнет чудить. Допустим он самостоятельно может поставить сертификат злоумышленника себе в систему. К слову, мы недавно проходили пин-тесты в одном из бизнес проектов. Так вот в требованиях по безопасности было четко прописано, что приложение должно корректно защититься в случае если «злоумышленник с помощью методов социальной инженерии убедит пользователя установить скомпрометированный сертификат». И это абсолютно правильно — никогда не надо полагаться на пользователя. Закон Мёрфи — всё, что может пойти не так, пойдёт не так.
Вы недостаточно внимательно прочитали статью, я писал об этом. В той реализации которую предлагаю я, для этого придется приложить довольно много усилий, потому что сертификат вшит в код, а не положен в ресурсы. А код в релизной сборке Flutter компилируется AOT способом.
С этим соглашусь, тут стоит учитывать время жизни сертификата и подобрать оптимальное решение для вас. Обеспечить ко времени его протухания обновление вашего приложения у пользователей. Или если вы не можете гарантировать подобного, стоит смотреть в направлении установки цепочек доверенных сертификатов. Принцип в целом тот же — вы настраиваете внутри приложения свою среду «доверия/недоверия» не полагаясь на системные настройки.
Извините, а можете пояснить подробнее? Вы в целом алгоритмы SSL и TLS считаете ненадежными, или же ситуацию когда пользователь старается испортить себе жизнь? Если второе, так я вроде это и написал — пользователь сам с удовольствием ищет себе проблемы, поэтому мы и вынуждены реализовывать pinning на уровне приложения.
А во второй части — полностью с Вами согласен. Разработчикам стоит не только задумываться о безопасности на уровне приложения, но и том, как именно уведомить пользователя о потенциальной атаке. А еще желательно предоставить ему в таком случае инструкцию как поступить в максимально простом виде. Я бы даже сказал, что это продолжение механизма защиты от атаки. Если не объяснить пользователю что он в опасности — он продолжит и обязательно найдет себе неприятности, например таким образом, который вы описали.
Я вас тоже прошу не вводить всех в заблуждение. Откройте фреймворк и посмотрите внимательнее, желательно не только реализацию StatefulWidget. Те абстрактные классы — это есть базовая реализация всех остальных элементов, виджетов, рендеров, потому что все частные случаи наследуются именно от них.
Касаемо 3 деревьев. Еще раз повторюсь.
Первое дерево — дерево виджетов, вы объявляете декларативно. То есть описываете структуру, вложенности данных виджетов. Иными словами описываете дерево. И передаете это дерево, точнее верхний его узел в метод runApp.
Сразу прочитаем документацию и найдем там.
Казалось бы уже достаточно, но мы все же посмотрим на код.
В этом методе происходит связь через биндинг (фактически прослойка между высокоуровневой частью фреймворка, написанной на дарт и движком фреймворка). И у этого биндинга вызывается метод привязки, который приводит нас к
Идем по ссылке в документации и смотрим, что же это за поле
Да, это рут дерева элементов, вот уже вторая иерархия, то есть второе дерево.
А теперь внимательно посмотрим что происходит в этом методе. Там создается специальный виджет — рут для дерева которое мы передали.
Элемент, который он для себя создает это
Смотрим его.
Обратите внимание на вот эту строчку. It can be used only as the root of an [Element] tree. Может быть использован только в качестве корневого объекта дерева элементов.
А еще в RenderObjectToWidgetAdapter, это тот, который специальный рут дерева виджетов есть такой интересный метод.
Присоединить данный виджет к дереву рендера. А что мы там передаем ему в контейнер — renderView.
Вот вам еще одно подтверждение того, что деревьев реально 3.
1 декларативно объявлено, 2 других созданы и находятся в WidgetsBinding.
Так что, пожалуйста, не путайте людей и разберитесь в вопросе прежде чем делать такие заявления.
Касаемо вашего второго пункта. Еще раз советую изучить вопрос в целом, а не в частности — StatefulWidget это далеко не весь фреймворк, даже не вся его высокоуровневая часть, поэтому делать выводы на основе частного случая — плохая практика. StatefulElement, который является его элементным представлением является лишь одним из компонентных элементов. Это означает, что его виджет представление самостоятельно не отрисовывает ничего, а лишь компонует другие.
В противовес таким есть RenderObjectElement, которые должны именно отрисоваться. Их виджеты как раз создают RenderObject. И они не StatefulWidget и даже не StatelessWidget, они RenderObjectWidget. И их множество, и вы ими пользуетесь. Тот же Opacity, ну нет там никаких стейтов, как бы вы их там не искали, поэтому флоу который описан у вас не верен. А вот флоу, описанный мной вполне укладывается.
Это не просто некоторые абстракции — это базовые классы, от них вы можете унаследоваться, от них унаследованы кучу виджетов которые вы используете повсеместно, выше уже писал пример. А _CupertinoDialogRenderWidget, ну да приватный класс, что в этом для вас удивительного?
Я реализовывал полностью на канве свой StatefulWidget и прекрасно знаю как это всё устроено.
StatefulWidget вы реализовывали на канвасе? Компоновочный виджет вы реализовывали на канвасе? А можете пример кода скинуть, пожалуйста, вот честно заинтересовали.
Я бы понял если вы сказали что вы реализовали RenderObjectWidget свой кастомный. Уже тут стоит все таки задуматься, так ли вы хорошо разбираетесь в устройстве фреймворка, как об этом пишите, без обид.
Я не возвожу в абсолют, что я великолепно на 100 процентов знаю, как Flutter устроен, я прекрасно понимаю, что могу в чем то ошибаться, я все таки человек а не машина, и это чужой проект. Если вы действительно можете опровергнуть, то что я разобрал в статье и комментариях, пожалуйста, сделайте это с пруфами, подробно, хотя бы на уровне того, что я сейчас написал. Я с удовольствием почерпну новую информацию, которую пропустил. Если же нет, то не вижу смысла продолжать дискуссию. В случае если хотите обсудить какую-то часть данного вопроса лично, то мой телеграмм mbixjkee.
Всего вам доброго.
В части того что написано в исходниках, вы абсолютно правы. Виджет это описание, а элемент, это уже экземпляр этого виджета в дереве.
Если можно провести такую аналогию из общего программирования — виджет это класс (просто описание некоторой сущности), а элемент — это конкретная инстанция такого класса.
Мы можем взять один виджет и декларативно его вставить множество раз, и на каждое такое вставление будет создан свой Элемент, то есть экземпляр виджета уже в дереве, конкретная сущность.
Кстати в Widget нет поля Element, там есть метод для создания Элемента, если опять возвращаться к моей грубой аналогии — конструктор этого класса. Это вот как раз связано с тем что я писал выше. Виджет не знает ничего про элементы. Потому что сколько раз бы он не был вставлен, он будет создавать новые экземпляры. Это как сам класс не знает про существование всех экземпляров которые созданы с его типом.
В целом, я пока пытался разобраться с исходниками уже склонялся к этим выводам, но это не является пруфом даже для меня самого. Окончательно я в этом убедился, когда услышал данный факт от разработчиков флаттер команды. В материалах я указал видео «How Flutter renders Widgets» в нем Andrew Fitz Gibbon, Matt Sullivan очень классно изложили это тему, оно мне помогло до конца привести понимание того в чем я пытался разобраться в какую-то структуру. И я очень многим вдохновился из него при написании статьи. Всем советую, очень крутой доклад, стоит посмотреть.
Вот с этим не соглашусь — такие абстракции не рождаются без причины.
Дерево виджетов — мы объявляем его просто декларативно. Плюс подтверждение из кода
Дерево элементов строится по виджетам и там в базовом классе как раз тоже
Посмотрим базовый класс
Это тоже дерево.
Так же у него есть метод
Он знает своих потомков и обрабатывает их.
Касаемо StatefulWidget — это частный случай. Нельзя только на нем строить представление о всем устройстве. Хотя его работа из общего устройства и не выбивается, а лишь немного расширяет. И опять же давайте подробно посмотрим на устройство.
Вы писали:
Вы не совсем правы — нет у него стейта. Он умеет его создать и отдать.
Вот подтверждение — весь код
Не хранит он свой стейт — хранится стейт уже в
Более того, тут видно что сам Стейт знает про виджет который его создал, а виджет про него опять же нет. Стейт знает про элемент, и почему тоже написано в документации к исходникам (в классе State).
BuildContext — это на самом деле и есть наш Element, но в нём при помощи приватных методов скрыта многая функциональность.
А вот тут абсолютно правильно, почему так, я пояснил выше.
Ну почему же.
Есть они, и наследуются именно от базовых, про что я и говорил — не стоит фокусироваться на частности StatefulWidget.
Более того, вы ими пользуетесь постоянно. Тот же Padding это по факту именно RenderObjectWidget.
А то что вы можете унаследоваться от RenderObjectWidget и сделать любое кастомное отображение — только плюс Flutter. Мы недавно проводили вебинар, и когда я готовил приложение к нему, встретил кейс с отображением внутренней тени. Так вот я его решил комбинацией обычных контейнеров с декорациями, а мог сделать то же самое написав кастомный виджет, который бы отрисовывал тень на канве.
Вот за одно это можно уже любить Flutter — можно сделать виджет любой сложности, вопрос понимания того как все работает и наличия времени.
А вот за это, огромное спасибо — никогда не пользовался данной возможностью, на досуге обязательно попробую и разберусь с этим.
Пока увы ничего не могу сказать по нему.
Кстати если уж пошел разговор об инструментах — та же студия в инспекторе тоже бьет на несколько деревьев — Widgets и RenderTree.
И касаемо бриджинга во Flutter, все таки это бриджинг напрямую через плюсовую часть фреймворка. Тут, по-моему не такая потеря производительности все-таки как в RN. Спасибо, что развернули схему, я не очень удачно видимо ее описал выше, действительно получилось похожим на равноценное во Flutter. А еще в дополнение к этой схеме, слышал вроде бы RN по мосту гоняет данные в JSON формате и при увеличении количества данных, мост начинает проседать.
Помимо всего этого Flutter использует Skia для отрисовки, которая тоже позволяет ему довольно продуктивно работать. В свою очередь, насколько я знаю, ReactNative использует для рендеринга схему NativeModule-Bridge-JS. Могу ошибаться, но мне уже этот момент кажется более «дорогим».
В ту же копилку — Flutter приложение при релизе использует AOT компиляцию, ReactNative насколько я понимаю — JIT.
Не сказать что напрямую относится к вопросу, но еще и однопоточность JS тоже тут позитивную роль не сыграет.
Естественно, у Flutter тоже есть рекомендации по тому, как писать более производительные приложения. И этим рекомендациям лучше следовать. Игнорирование их, может привести к просеву производительности, но это скорее всего ошибка разработчика, нежели проблема Flutter.
Поскольку поиск в каждый момент времени одноуровневый, мы сначала на одном уровне найдем те элементы которые не потеряли связи, затем попытаемся сопоставить новые виджеты с потерявшими связи элементы, а потом создадим новые. Все же неиспользованные станут неактивными. Поэтому при смене вложенности они уже будут потеряны. Если поменяется тип родительского, то причина ровно в том же. Просто это произойдет на предыдущем уровне. Родительский виджет (под родительским я имею ввиду тот, который расположен на один уровень выше) поменял тип, и не смотря на ключ, не будет создана связь с родительским элементом, соответственно тот, со всеми своими вложенными элементам (фактически ветвь дерева) будет удален. И другого выхода, кроме как воспользоваться механизмом своебразного «кэша» через глобальные ключи не остается.
Тут стоит отметить, что он тоже имеет довольно много ограничений, которые стоит учитывать.
Вы правильно заметили — GlobalKey довольно дорого обходятся.
Помимо этого, неактивные элементы в этом кэше будут жить всего лишь до конца фрейма, если они не стали активными к этому моменту, они считаются больше не существующими. Когда вызывается buildOwner.finalizeTree() во время отрисовки фрейма, берется список неактивных элементов и вызывается unmount. И в нем уже, если есть глобальный ключ будет вызвана дерегистрация.
Ну и конечно — это проблема менеджмента. Неаккуратно использовав GlobalKey механизм, можно легко получить ошибку из-за того что несколько виджетов с одним GlobalKey включены в дерево дважды.
static void _debugVerifyIllFatedPopulation()
содержит
information.add(ErrorSummary('Multiple widgets used the same GlobalKey.'));
подобный код, если проверка на дубликаты провалится.
Насчет ключа, увы, так сделать не получится. Вы можете легко это проверить просто скопировав мой пример и заменив ключ. Если там будет любой ключ кроме глобального, это не повлияет на переиспользование элементов. Я, до того как решил разобраться, тоже думал что можно будет использовать любой ключ. Когда проверил, был удивлен — не работало. Ни один вариант не приводил к переиспользованию, кроме глобального ключа. Когда я полез разбираться в реализацию, нашел этому объяснение.
Если вы посмотрите в реализацию ключей, то у GlobalKey заметите интересную особенность, которой нет у других ключей.
void _register(Element element)
В нем происходит регистрация в статической мапе, для соответствия глобальный ключ -> элемент.
У элемента в методе mount есть следующий код:
Когда мы монтируем в дерево наш элемент мы его регистрируем на соответствие.
А в методе
Element inflateWidget(Widget newWidget, dynamic newSlot)
перед созданием элемента есть проверка — если виджет с глобальным ключом, тогда мы пытаемся его переиспользовать.
И там же мы увидим документацию.
Я указывал эту особенность в статье, в описании жизненного цикла.
Как я понял, в нем вы имеете в виду именно жизненный цикл State сущности StatefullWidget. Да, все действительно так, как вы описали, и это очень полезный материал о внутреннем устройстве. Но поскольку это все таки частный случай поведения, я лишь вскользь упомянул об этом в статье, чтобы не раздувать статью частностями. Мне хотелось в этой статье подробнее разобрать общий механизм триады Widget-Element-RenderObject. Как я написал в конце статьи, есть планы продолжать подобного рода статьи о внутреннем устройстве Flutter, в рамках которых можно будет подробно рассмотреть подобные частности.
Зря Вы считаете, что я не понимаю то что вы написали, у меня тоже достаточно опыта реальной разработки. И я не пытаюсь доказать что пример ваш не правилен, я пытался Вам показать, что возведение в абсолют чего-либо это неправильная позиция, и узкость. Но увы, видимо Вы не имеете нужды это увидеть, Вам в своём мирке уютно, и ничего кроме него Вы не хотите видеть. Это Ваше право. На этом всё, в одном мы сошлись — каждый при своём.
А касаемо примера из Флаттера, ну вы бы хотя бы открыли что ли его для приличия. Ну право, он ограничение на State имеет, ну причем здесь виджеты. И юзается в куче его наследников, в том числе те которые напрямую относятся к стейтам для которых анимация не важное а существенное свойство, например ImplicitlyAnimatedWidgetState. Это даже из названия следует. И в таких для которых это не является существенным свойством, например _HelperErrorState. Видимо команде флаттера надо как можно скорее переписывать эту часть, ведь она не покрывается вашим случаем на все случаи жизни.
В итоге вы приводите всего 1 пример и делаете вывод:
То есть вынесение важной части функционала, которая по факту одинакова у обоих элементов вы считаете не возможным?
Киллер и биатлонист, оба стреляют у обоих это важное свойство, невозможно юзать миксин?
Слишком абстрактно?
Я привел пример, который вы судя по всему даже не открыли, раз пишите что я не представляю вам конструктивной критики.
Вы указали как тег своей статьи Флаттер в том числе. Наверное реализации в самом Флаттере тоже могут быть аргументом в таком случае.
Я привел вам пример SingleTickerProviderStateMixin это довольно важная часть функционала по реализации анимаций, и это миксин можно прицепить к различным стейтам.
Анимация для визуального класса не значимая вещь?
И да, я не предлагаю все подряд выносить в миксины, это то еще зло в красивой обертке. Любым инструментом надо пользоваться аккуратно и к месту, можно и фугасом попытаться гвоздь забить, может получиться. А может и нет.
Следующий момент, касаемо целей — вы поставили цель восполнить пробел, ну так показали бы разные кейсы, а то выходит кейс 1 со спорным выводом. Так и позиционировали бы статью как один из кейсов который мне кажется правильным.
А по поводу критики моего примера, я и не приводил его как вариант реального использования или великолепной архитектуры, я приводил его как абстрактный пример, для демонстрации конкретной ситуации, то что вы этого не увидели и захотели в нем видеть что-то другое, это как говорил один футболист ваши ожидания и ваши проблемы.