Flutter: Повторное использование логики состояния либо слишком многословно, либо слишком сложно

Созданный на 2 мар. 2020  ·  420Комментарии  ·  Источник: flutter/flutter

.

Относится к обсуждению хуков # 25280

TL; DR: сложно повторно использовать логику State . Мы либо получаем сложный и глубоко вложенный метод build либо нам приходится копировать и вставлять логику в несколько виджетов.

Повторное использование такой логики через примеси или функции невозможно.

Проблема

Повторное использование логики State в нескольких StatefulWidget очень сложно, поскольку эта логика опирается на несколько жизненных циклов.

Типичным примером может служить логика создания TextEditingController (но также AnimationController , неявная анимация и многое другое). Эта логика состоит из нескольких шагов:

  • определение переменной в State .
    dart TextEditingController controller;
  • создание контроллера (обычно внутри initState) с потенциально значением по умолчанию:
    dart <strong i="25">@override</strong> void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • удалил контроллер при удалении State :
    dart <strong i="30">@override</strong> void dispose() { controller.dispose(); super.dispose(); }
  • делаем все, что захотим, с этой переменной внутри build .
  • (необязательно) выставить это свойство в debugFillProperties :
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

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

Копирование этой логики повсюду "работает", но создает слабость в нашем коде:

  • легко забыть переписать один из шагов (например, забыть вызвать dispose )
  • это добавляет много шума в код

Проблема с миксином

Первой попыткой разложить эту логику на множители было бы использование миксина:

mixin TextEditingControllerMixin<T extends StatefulWidget> on State<T> {
  TextEditingController get textEditingController => _textEditingController;
  TextEditingController _textEditingController;

  <strong i="11">@override</strong>
  void initState() {
    super.initState();
    _textEditingController = TextEditingController();
  }

  <strong i="12">@override</strong>
  void dispose() {
    _textEditingController.dispose();
    super.dispose();
  }

  <strong i="13">@override</strong>
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty('textEditingController', textEditingController));
  }
}

Затем использовали этот способ:

class Example extends StatefulWidget {
  <strong i="17">@override</strong>
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example>
    with TextEditingControllerMixin<Example> {
  <strong i="18">@override</strong>
  Widget build(BuildContext context) {
    return TextField(
      controller: textEditingController,
    );
  }
}

Но у этого есть другие недостатки:

  • Примесь можно использовать только один раз в классе. Если нашему StatefulWidget нужно несколько TextEditingController , тогда мы больше не сможем использовать подход микширования.

  • «Состояние», объявленное миксином, может конфликтовать с другим миксином или самим State .
    В частности, если два миксина объявляют члена с одним и тем же именем, возникнет конфликт.
    В худшем случае, если конфликтующие элементы имеют один и тот же тип, это автоматически завершится ошибкой.

Это делает миксины неидеальными и слишком опасными, чтобы быть истинным решением.

Использование паттерна "строитель"

Другим решением может быть использование того же шаблона, что и StreamBuilder & co.

Мы можем создать виджет TextEditingControllerBuilder , который управляет этим контроллером. Тогда наш метод build может использовать его свободно.

Такой виджет обычно реализуется следующим образом:

class TextEditingControllerBuilder extends StatefulWidget {
  const TextEditingControllerBuilder({Key key, this.builder}) : super(key: key);

  final Widget Function(BuildContext, TextEditingController) builder;

  <strong i="12">@override</strong>
  _TextEditingControllerBuilderState createState() =>
      _TextEditingControllerBuilderState();
}

class _TextEditingControllerBuilderState
    extends State<TextEditingControllerBuilder> {
  TextEditingController textEditingController;

  <strong i="13">@override</strong>
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(
        DiagnosticsProperty('textEditingController', textEditingController));
  }

  <strong i="14">@override</strong>
  void dispose() {
    textEditingController.dispose();
    super.dispose();
  }

  <strong i="15">@override</strong>
  Widget build(BuildContext context) {
    return widget.builder(context, textEditingController);
  }
}

Затем использовали как таковые:

class Example extends StatelessWidget {
  <strong i="19">@override</strong>
  Widget build(BuildContext context) {
    return TextEditingControllerBuilder(
      builder: (context, controller) {
        return TextField(
          controller: controller,
        );
      },
    );
  }
}

Это решает проблемы, возникающие с миксинами. Но это создает другие проблемы.

  • Использование очень многословное. Фактически это 4 строки кода + два уровня отступа для объявления одной переменной.
    Это еще хуже, если мы хотим использовать его несколько раз. Хотя мы можем создать TextEditingControllerBuilder внутри другого один раз, это резко снизит читаемость кода:

    <strong i="28">@override</strong>
    Widget build(BuildContext context) {
    return TextEditingControllerBuilder(
      builder: (context, controller1) {
        return TextEditingControllerBuilder(
          builder: (context, controller2) {
            return Column(
              children: <Widget>[
                TextField(controller: controller1),
                TextField(controller: controller2),
              ],
            );
          },
        );
      },
    );
    }
    

    Это код с очень большим отступом для объявления двух переменных.

  • Это добавляет некоторые накладные расходы, поскольку у нас есть дополнительный экземпляр State и Element .

  • Трудно использовать TextEditingController вне build .
    Если мы хотим, чтобы жизненные циклы State выполняли некоторую операцию с этими контроллерами, нам понадобится GlobalKey для доступа к ним. Например:

    class Example extends StatefulWidget {
    <strong i="43">@override</strong>
    _ExampleState createState() => _ExampleState();
    }
    
    class _ExampleState extends State<Example> {
    final textEditingControllerKey =
        GlobalKey<_TextEditingControllerBuilderState>();
    
    <strong i="44">@override</strong>
    void didUpdateWidget(Example oldWidget) {
      super.didUpdateWidget(oldWidget);
    
      if (something) {
        textEditingControllerKey.currentState.textEditingController.clear();
      }
    }
    
    <strong i="45">@override</strong>
    Widget build(BuildContext context) {
      return TextEditingControllerBuilder(
        key: textEditingControllerKey,
        builder: (context, controller) {
          return TextField(controller: controller);
        },
      );
    }
    }
    
P5 crowd framework passed first triage proposal new feature

Самый полезный комментарий

Я добавлю несколько мыслей с точки зрения React.
Простите, если они не относятся к делу, но я хотел вкратце объяснить, что мы думаем о крючках.

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

Эта неявность не присуща модели. Вы можете представить, как аргумент явно проходит через все вызовы - от самого компонента через пользовательские хуки, вплоть до каждого примитивного хука. Но на практике мы обнаружили, что это шумно и бесполезно. Таким образом, мы сделали выполняющийся в данный момент Component неявным глобальным состоянием. Это похоже на то, как throw в виртуальной машине ищет ближайший блок catch вверх вместо того, чтобы передавать errorHandlerFrame в коде.

Итак, это функции с неявным скрытым состоянием внутри них, это кажется плохим? Но в React и Компоненты в целом. В этом весь смысл компонентов. Это функции, с которыми связано время жизни (что соответствует позиции в дереве пользовательского интерфейса). Причина, по которой сами компоненты не являются бесполезным оружием в отношении состояния, заключается в том, что вы не вызываете их просто из случайного кода. Вы вызываете их из других компонентов. Таким образом, их время жизни имеет смысл, потому что вы остаетесь в контексте кода пользовательского интерфейса.

Однако не все проблемы имеют компонентную форму. Компоненты сочетают в себе две способности: состояние + эффекты и время жизни, привязанное к положению в дереве. Но мы обнаружили, что первая способность полезна сама по себе. Точно так же, как функции в целом полезны, потому что они позволяют вам инкапсулировать код, нам не хватало примитива, который позволил бы нам инкапсулировать (и повторно использовать) связки состояние + эффекты без обязательного создания нового узла в дереве. Вот что такое крючки. Компоненты = хуки + возвращаемый интерфейс.

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

По сути, мы не рассматриваем вызовы Hook как обычные вызовы функций. Например, useState() больше use State() если бы у нас был синтаксис. Это была бы языковая особенность. Вы можете смоделировать что-то вроде крючков с алгебраическими эффектами на языках, в которых есть отслеживание эффектов. В этом смысле они будут обычными функциями, но тот факт, что они «используют» State, будет частью их сигнатуры типа. Тогда вы можете думать о React как о «обработчике» этого эффекта. В любом случае, это очень теоретически, но я хотел указать на предшествующий уровень техники с точки зрения модели программирования.

С практической точки зрения здесь есть несколько моментов. Во-первых, стоит отметить, что хуки не являются «дополнительным» API для React. Они РЕАКТ API для написания компонентов в этой точке. Думаю, я согласен с тем, что в качестве дополнительной функции они не будут очень привлекательными. Так что я не знаю, действительно ли они имеют смысл для Flutter, у которого, возможно, другая общая парадигма.

Что касается того, что они позволяют, я думаю, что ключевой особенностью является способность инкапсулировать состояние + эффективную логику, а затем связывать их вместе, как если бы вы делали обычную композицию функций. Поскольку примитивы предназначены для компоновки, вы можете взять некоторый вывод Hook, например useState() , передать его как вход в cusom useGesture(state) , а затем передать его как вход в несколько пользовательских useSpring(gesture) вызовы, которые выдают значения в шахматном порядке и т. д. Каждая из этих частей совершенно не знает других и может быть написана разными людьми, но они хорошо сочетаются друг с другом, потому что состояние и эффекты инкапсулируются и «прикрепляются» к включающему Компоненту. Вот небольшая демонстрация чего-то вроде этого и статья, в которой я кратко описываю, что такое хуки.

Я хочу подчеркнуть, что речь идет не о сокращении шаблона, а о возможности динамически составлять конвейеры инкапсулированной логики с отслеживанием состояния. Обратите внимание, что он полностью реактивен - то есть он не запускается один раз, но реагирует на все изменения свойств с течением времени. Один из способов думать о них - это как плагины в конвейере аудиосигнала. Несмотря на то, что я полностью осознаю, что на практике «функции, у которых есть память», мы не обнаружили в этом проблемы, потому что они полностью изолированы. Фактически, эта изоляция - их главная черта. В противном случае он развалился бы. Таким образом, любая взаимозависимость должна быть выражена явно путем возврата и передачи значений следующему элементу в цепочке. И тот факт, что любой пользовательский хук может добавлять или удалять состояние или эффекты, не нарушая (или даже не затрагивая) своих потребителей, является еще одной важной особенностью с точки зрения сторонней библиотеки.

Я не знаю, было ли это вообще полезно, но надеюсь, что это проливает некоторую перспективу на модель программирования.
С удовольствием отвечу на другие вопросы.

Все 420 Комментарий

cc @dnfield @Hixie
В соответствии с запросом, это полная информация о проблемах, решаемых с помощью хуков.

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

Кажется, что кое-что из этого можно было бы улучшить для авторов библиотек, если бы мы строго типизировали классы, которые необходимо удалить с помощью какого-то типа abstract class Disposable . В таком случае вам будет легче написать более простой класс, подобный этому, если вы так склонны:

class AutomaticDisposingState<T> extends State<T> {
  List<Disposable> _disposables;

  void addDisposable(Disposable disposable) {
    assert(!_disposables.contains(disposable));
    _disposables.add(disposable);
  }

  <strong i="8">@override</strong>
  void dispose() {
    for (final Disposable disposable in _disposables)
      disposable.dispose();
    super.dispose();
  }
}

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

<strong i="12">@override</strong>
void initState() {
  super.initState();
  controller = TextEditingController(text: 'Hello world');
  addDisposable(controller);
  addProperty('controller', controller);
}

Нам просто не хватает такой информации о типе для одноразовых классов?

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

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

В конце концов, пользователи сами могут разложить его на множители, как они хотят.


Проблема не только в одноразовых предметах.

Это забывает часть проблемы с обновлением. Логика этапа также может полагаться на жизненные циклы, такие как didChangeDependencies и didUpdateWidget.

Несколько конкретных примеров:

  • SingleTickerProviderStateMixin, у которого есть логика внутри didChangeDependencies .
  • AutomaticKeepAliveClientMixin, который использует super.build(context)

В фреймворке есть много примеров, в которых мы хотим повторно использовать логику состояния:

  • StreamBuilder
  • TweenAnimationBuilder
    ...

Это не что иное, как способ повторного использования состояния с механизмом обновления.

Но они страдают той же проблемой, что и упомянутые в части «строителя».

Это вызывает множество проблем.
Например, одна из наиболее распространенных проблем в Stackoverflow - это люди, пытающиеся использовать StreamBuilder для побочных эффектов, таких как «продвижение маршрута при изменении».

И в конечном итоге их единственное решение - «выбросить» StreamBuilder.
Это включает в себя:

  • преобразование виджета в состояние с отслеживанием состояния
  • вручную прослушивать поток в initState + didUpdateWidget + didChangeDependencies
  • отменить предыдущую подписку на didChangeDependencies / didUpdateWidget при изменении потока
  • отменить подписку при утилизации

Это _ большой объем работы_, и его нельзя использовать повторно.

Проблема

Повторное использование логики State в нескольких StatefulWidget очень сложно, поскольку эта логика опирается на несколько жизненных циклов.

Типичным примером может служить логика создания TextEditingController (но также AnimationController , неявная анимация и многое другое). Эта логика состоит из нескольких шагов:

  • определение переменной в State .
    dart TextEditingController controller;
  • создание контроллера (обычно внутри initState) с потенциально значением по умолчанию:
    dart <strong i="19">@override</strong> void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • удалил контроллер при удалении State :
    dart <strong i="24">@override</strong> void dispose() { controller.dispose(); super.dispose(); }
  • делаем все, что захотим, с этой переменной внутри build .
  • (необязательно) выставить это свойство в debugFillProperties :
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

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

Копирование этой логики повсюду "работает", но создает слабость в нашем коде:

  • легко забыть переписать один из шагов (например, забыть вызвать dispose )
  • это добавляет много шума в код

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

Я согласен с тем, что миксин для каждого типа собственности не работает. Я согласен, что шаблон построителя не годится (он буквально использует то же количество строк, что и в наихудшем сценарии, описанном выше).

С NNBD (особенно с late final чтобы инициализаторы могли ссылаться на this ) мы сможем сделать что-то вроде этого:

typedef Initializer<T> = T Function();
typedef Disposer<T> = void Function(T value);

mixin StateHelper<T extends StatefulWidget> on State<T> {
  bool _active = false;
  List<Property<Object>> _properties = <Property<Object>>[];

  <strong i="8">@protected</strong>
  void registerProperty<T>(Property<T> property) {
    assert(T != Object);
    assert(T != dynamic);
    assert(!_properties.contains(property));
    _properties.add(property);
    if (_active)
      property._initState();
  }

  <strong i="9">@override</strong>
  void initState() {
    _active = true;
    super.initState();
    for (Property<Object> property in _properties)
      property._initState();
  }

  <strong i="10">@override</strong>
  void dispose() {
    for (Property<Object> property in _properties)
      property._dispose();
    super.dispose();
    _active = false;
  }

  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    for (Property<Object> property in _properties)
      property._debugFillProperties(properties);
  }
}

class Property<T> {
  Property(this.owner, this.initializer, this.disposer, [ this.debugName ]) {
    owner.registerProperty(this);
  }

  final StateHelper<StatefulWidget> owner;
  final Initializer<T> initializer;
  final Disposer<T> disposer;
  final String debugName;

  T value;

  void _initState() {
    if (initializer != null)
      value = initializer();
  }

  void _dispose() {
    if (disposer != null)
      disposer(value);
    value = null;
  }

  void _debugFillProperties(DiagnosticPropertiesBuilder properties) {
    properties.add(DiagnosticsProperty(debugName ?? '$T property', value));
  }
}

Вы бы использовали это так:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  <strong i="14">@override</strong>
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with StateHelper<MyHomePage> {
  late final Property<int> _counter = Property<int>(this, null, null);
  late final Property<TextEditingController> _text = Property<TextEditingController>(this,
    () => TextEditingController(text: 'button'),
    (TextEditingController value) => value.dispose(),
  );

  void _incrementCounter() {
    setState(() {
      _counter.value += 1;
    });
  }

  <strong i="15">@override</strong>
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the ${_text.value.text} this many times:',
            ),
            Text(
              '${_counter.value}',
              style: Theme.of(context).textTheme.headline4,
            ),
            TextField(
              controller: _text.value,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Похоже, что на самом деле ситуация не улучшается. Осталось четыре строчки.

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

Что они скрывают?

Проблема не в количестве строк, а в том, что это за строки.

StreamBuilder может содержать столько же строк, сколько stream.listen + setState + subscription.close .
Но написать StreamBuilder можно, так сказать, без какого-либо отражения.
В этом процессе нет ошибки. Это просто «передать поток и построить из него виджеты».

В то время как написание кода вручную требует гораздо большего количества мыслей:

  • Может ли поток со временем измениться? Если мы забыли с этим справиться, у нас есть ошибка.
  • Мы забыли закрыть подписку? Еще одна ошибка
  • Какое имя переменной я использую для подписки? Это имя может быть недоступно
  • А как насчет тестирования? Мне нужно дублировать тест? С StreamBuilder нет необходимости писать модульные тесты для прослушивания потока, которые были бы избыточными. Но если мы все время будем писать вручную, вполне возможно ошибиться.
  • Если мы слушаем два потока одновременно, теперь у нас есть несколько переменных с очень похожими именами, загрязняющих наш код, это может вызвать некоторую путаницу.

Что они скрывают?

  • FutureBuilder / StreamBuilder скрывает механизм прослушивания и отслеживает текущий моментальный снимок.
    Логика переключения между двумя Future тоже довольно сложна, учитывая, что в ней нет subscription.close() .
  • AnimatedContainer скрывает логику создания промежуточного кадра между предыдущим и новым значениями.
  • Listview скрывает логику «монтировать виджет, как он появляется».

приложениям обычно не нужно беспокоиться о добавлении состояния в свойства отладки

Они этого не делают, потому что не хотят иметь дело со сложностью обслуживания метода debugFillProperties.
Но если бы мы сказали разработчикам: «Хотели бы вы, чтобы все ваши параметры и свойства состояния были доступны из коробки в инструменте разработки Flutter?» Я уверен, что они скажут да

Многие люди выразили мне желание получить настоящий эквивалент инструмента разработчика React. Разработчика Flutter пока нет.
В React мы можем видеть все состояние виджета + его параметры и редактировать его, ничего не делая.

Точно так же люди были очень удивлены, когда я сказал им, что при использовании provider + некоторых других моих пакетов по умолчанию все состояние их приложения отображается для них без необходимости что-либо делать ( по модулю этой досадной ошибки devtool )

Я должен признать, что я не большой поклонник FutureBuilder, он вызывает множество ошибок, потому что люди не думают, когда запускать FutureBuilder. Я думаю, что для нас было бы разумным отказаться от его поддержки. StreamBuilder в порядке, я думаю, но я думаю, что сами потоки слишком сложны (как вы упомянули в своем комментарии выше), поэтому ...

Почему кто-то должен думать о сложности создания подростков?

ListView на самом деле не скрывает логику монтирования виджета в том виде, в каком он появляется; это большая часть API.

Проблема не в количестве строк, а в том, что это за строки.

Я действительно не понимаю, что здесь беспокоит. Строки очень похожи на простой шаблон. Объявите вещь, инициализируйте вещь, утилизируйте вещь. Если дело не в количестве строк, то в чем проблема?

Я согласен с вами, что FutureBuilder проблематичен.

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

Почему кто-то должен думать о сложности создания подростков?

ListView на самом деле не скрывает логику монтирования виджета в том виде, в каком он появляется; это большая часть API.

Мы согласны с этим. Моя точка зрения заключалась в том, что мы не можем критиковать что-то вроде хуков с «скрытой логикой», поскольку то, что делают хуки, строго эквивалентно тому, что делает TweenAnimationBuilder / AnimatedContainer / ....
Логика не скрывается

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

Когда мы переводим эту концепцию на прослушивание потоков, StreamBuilder - это _неявное прослушивание_, тогда как stream.listen - _explicit_.

В частности, с помощью StreamBuilder вы _не__ нельзя забыть обработать сценарий, при котором поток изменяется, или забыть закрыть подписку.
Вы также можете объединить несколько StreamBuilder вместе

stream.listen немного более продвинутый и более подверженный ошибкам.

Строители могут упростить приложение.
Но, как мы уже договорились ранее, паттерн Строитель не идеален. Его сложно писать и использовать.
Эта проблема и то, что решают перехватчики, связаны с альтернативным синтаксисом для * Builders.

Например, flutter_hooks имеет строгий эквивалент FutureBuilder и StreamBuilder :

Widget build(context) {
  final AsyncSnapshot<T> snapshot = useStream(stream);
}

В продолжении AnimatedContainer & alike может быть представлено символом useAnimatedSize / useAnimatedDecoractedBox / ... таким образом, что мы имеем:

double opacity;

Widget build(context) {
  final double animatedOpacity = useAnimatedDouble(opacity, duration: Duration(milliseconds: 200));
  return Opacity(
    opacity: animatedOpacity,
    child: ...,
  );
}

Я хотел сказать, что мы не можем критиковать что-то вроде крючков со словами «это скрывает логику»,

Это не аргумент. Аргумент таков: «это скрывает логику, о которой должны думать разработчики ».

У вас есть пример такой логики, о которой стоит задуматься разработчикам?

Например, кому принадлежит TextEditingController (кто его создает, кто им распоряжается).

Как с этим кодом?

Widget build(context) {
  final controller = useTextEditingController();
  final focusNode = useFocusNode();
}

Крюк создает это и избавляется от него.

Я не уверен, что в этом непонятного.

Да, точно. Я понятия не имею, каков жизненный цикл контроллера с этим кодом. Длится ли он до конца лексической области? Время жизни государства? Что-то другое? Кому это принадлежит? Если я передам его кому-то другому, смогут ли они стать собственниками? Ничего из этого не видно в самом коде.

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

Я понятия не имею, каков жизненный цикл контроллера с этим кодом

Тебе не нужно об этом думать. Разработчик больше не несет ответственности за это.

Длится ли он до конца лексической области? Время жизни государства

Время жизни государства

Кому это принадлежит?

Хук владеет контроллером. Это часть API useTextEditingController которым он владеет.
Это относится к useFocusNode , useScrollController , useAnimationController , ...

В некотором смысле эти вопросы относятся к StreamBuilder :

  • Нам не нужно думать о жизненных циклах StreamSubscription.
  • Подписка действует в течение всего срока действия государства.
  • StreamBuilder владеет StreamSubscription

В общем, вы можете думать о:

final value = useX(argument);

как строгий эквивалент:

XBuilder(
  argument: argument,
  builder: (context, value) {

  },
);

У них одинаковые правила и одинаковое поведение.

Разработчик больше не несет ответственности за это.

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

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

@Hixie
Я не думаю, что @rrousselGit говорил о том, что это одно и то же, а просто о том, что у них «одинаковые правила и одинаковое поведение» в отношении жизненного цикла? Верный?

Однако они не решают одни и те же проблемы.

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

Такие вещи, как простое совместное использование логики состояния между виджетами, - это то, чего мне не хватало при тестировании флаттера осенью 2019 года.

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

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

Мне любопытно, как люди относятся к коду в https://github.com/flutter/flutter/issues/51752#issuecomment -664787791. Не думаю, что это лучше, чем initState / dispose, но если людям нравятся хуки, им это тоже нравится? Крючки лучше? Худший?

@Hixie Hooks разделяют жизненный цикл на один вызов функции. Если я использую ловушку, скажем useAnimationController , мне больше не нужно думать об initState и dispose. Это снимает с разработчика ответственность. Мне не нужно беспокоиться о том, уничтожил ли я каждый созданный мной контроллер анимации.

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

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

Я добавлю несколько мыслей с точки зрения React.
Простите, если они не относятся к делу, но я хотел вкратце объяснить, что мы думаем о крючках.

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

Эта неявность не присуща модели. Вы можете представить, как аргумент явно проходит через все вызовы - от самого компонента через пользовательские хуки, вплоть до каждого примитивного хука. Но на практике мы обнаружили, что это шумно и бесполезно. Таким образом, мы сделали выполняющийся в данный момент Component неявным глобальным состоянием. Это похоже на то, как throw в виртуальной машине ищет ближайший блок catch вверх вместо того, чтобы передавать errorHandlerFrame в коде.

Итак, это функции с неявным скрытым состоянием внутри них, это кажется плохим? Но в React и Компоненты в целом. В этом весь смысл компонентов. Это функции, с которыми связано время жизни (что соответствует позиции в дереве пользовательского интерфейса). Причина, по которой сами компоненты не являются бесполезным оружием в отношении состояния, заключается в том, что вы не вызываете их просто из случайного кода. Вы вызываете их из других компонентов. Таким образом, их время жизни имеет смысл, потому что вы остаетесь в контексте кода пользовательского интерфейса.

Однако не все проблемы имеют компонентную форму. Компоненты сочетают в себе две способности: состояние + эффекты и время жизни, привязанное к положению в дереве. Но мы обнаружили, что первая способность полезна сама по себе. Точно так же, как функции в целом полезны, потому что они позволяют вам инкапсулировать код, нам не хватало примитива, который позволил бы нам инкапсулировать (и повторно использовать) связки состояние + эффекты без обязательного создания нового узла в дереве. Вот что такое крючки. Компоненты = хуки + возвращаемый интерфейс.

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

По сути, мы не рассматриваем вызовы Hook как обычные вызовы функций. Например, useState() больше use State() если бы у нас был синтаксис. Это была бы языковая особенность. Вы можете смоделировать что-то вроде крючков с алгебраическими эффектами на языках, в которых есть отслеживание эффектов. В этом смысле они будут обычными функциями, но тот факт, что они «используют» State, будет частью их сигнатуры типа. Тогда вы можете думать о React как о «обработчике» этого эффекта. В любом случае, это очень теоретически, но я хотел указать на предшествующий уровень техники с точки зрения модели программирования.

С практической точки зрения здесь есть несколько моментов. Во-первых, стоит отметить, что хуки не являются «дополнительным» API для React. Они РЕАКТ API для написания компонентов в этой точке. Думаю, я согласен с тем, что в качестве дополнительной функции они не будут очень привлекательными. Так что я не знаю, действительно ли они имеют смысл для Flutter, у которого, возможно, другая общая парадигма.

Что касается того, что они позволяют, я думаю, что ключевой особенностью является способность инкапсулировать состояние + эффективную логику, а затем связывать их вместе, как если бы вы делали обычную композицию функций. Поскольку примитивы предназначены для компоновки, вы можете взять некоторый вывод Hook, например useState() , передать его как вход в cusom useGesture(state) , а затем передать его как вход в несколько пользовательских useSpring(gesture) вызовы, которые выдают значения в шахматном порядке и т. д. Каждая из этих частей совершенно не знает других и может быть написана разными людьми, но они хорошо сочетаются друг с другом, потому что состояние и эффекты инкапсулируются и «прикрепляются» к включающему Компоненту. Вот небольшая демонстрация чего-то вроде этого и статья, в которой я кратко описываю, что такое хуки.

Я хочу подчеркнуть, что речь идет не о сокращении шаблона, а о возможности динамически составлять конвейеры инкапсулированной логики с отслеживанием состояния. Обратите внимание, что он полностью реактивен - то есть он не запускается один раз, но реагирует на все изменения свойств с течением времени. Один из способов думать о них - это как плагины в конвейере аудиосигнала. Несмотря на то, что я полностью осознаю, что на практике «функции, у которых есть память», мы не обнаружили в этом проблемы, потому что они полностью изолированы. Фактически, эта изоляция - их главная черта. В противном случае он развалился бы. Таким образом, любая взаимозависимость должна быть выражена явно путем возврата и передачи значений следующему элементу в цепочке. И тот факт, что любой пользовательский хук может добавлять или удалять состояние или эффекты, не нарушая (или даже не затрагивая) своих потребителей, является еще одной важной особенностью с точки зрения сторонней библиотеки.

Я не знаю, было ли это вообще полезно, но надеюсь, что это проливает некоторую перспективу на модель программирования.
С удовольствием отвечу на другие вопросы.

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

Мне любопытно, что люди думают о коде в № 51752 (комментарий) . Не думаю, что это лучше, чем initState / dispose, но если людям нравятся хуки, им это тоже нравится? Крючки лучше? Худший?

Ключевое слово late улучшает ситуацию, но по-прежнему страдает некоторыми проблемами:

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

Например, с StreamBuilder прослушиваемый поток может со временем меняться. Но здесь нет простого решения реализовать такую ​​вещь, поскольку объект инициализируется только один раз.

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

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

С useMemo это может выглядеть так:

String messageId;

Widget build(context) {
  final Future<Message> message = useMemo(() => fetchMessage(messageId), [messageId]);

}

В этой ситуации, даже если метод сборки вызывается снова 10 раз, пока messageId не изменяется, выборка данных не выполняется снова.
Но когда messageId изменяется, создается новый Future .


Стоит отметить, что я не думаю, что flutter_hooks в его текущем состоянии усовершенствован для Dart. Моя реализация больше похожа на POC, чем на полноценную архитектуру.
Но я действительно считаю, что у нас есть проблема с возможностью повторного использования кода StatefulWidgets.

Я не помню, где, но я помню, что предполагал, что хуки в идеальном мире будут настраиваемым генератором функций, рядом с async* & sync* , что может быть похоже на то, что Дэн предлагает с use State вместо useState

@gaearon

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

Здесь обсуждается не эта проблема. Я рекомендую записать отдельную ошибку, чтобы рассказать о невозможности сделать то, что вы описываете. (Это звучит как совсем другая проблема и, честно говоря, более убедительная, чем описанная здесь.) Эта ошибка связана именно с тем, что часть логики слишком многословна.

Нет, он прав, это моя формулировка, которая может сбивать с толку.
Как я упоминал ранее, речь идет не о количестве строк кода, а о самих строках кода.

Речь идет о факторизации состояния.

Эта ошибка очень четко указывает на то, что проблема заключается в том, что «Повторное использование логики состояния слишком многословно / сложно», и сводится к тому, что в состоянии слишком много кода, когда у вас есть свойство, которое должно иметь код для его объявления, в initState, в dispose и в debugFillProperties. Если проблема, которая вас волнует, отличается, я рекомендую зарегистрировать новую ошибку, описывающую эту проблему.

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

Я думаю, вы неправильно поняли то, что я сказал тогда в этом номере.

Проблема отнюдь не в шаблонах, а в возможности повторного использования.

Boilerplate - это следствие проблемы с возможностью повторного использования, а не причина

Эта проблема описывает следующее:

Мы можем захотеть повторно использовать / составить логику состояния. Но доступны варианты: либо миксины, либо конструкторы, либо их повторное использование - у всех из них есть свои проблемы.

Проблемы существующих опций могут быть связаны с шаблоном, но проблема, которую мы пытаемся решить, не связана.
Хотя сокращение шаблона Builders - это один путь (что и делают хуки), может быть и другой.

Например, какое-то время я хотел предложить добавить такие методы, как:

context.onDidChangeDependencies(() {

});
context.onDispose(() {

});

Но у них есть свои проблемы, и они не решают проблему полностью, поэтому я этого не сделал.

@rrousselGit , не стесняйтесь редактировать исходную формулировку проблемы вверху здесь, чтобы лучше отразить проблему. Также не стесняйтесь создавать проектную документацию: https://flutter.dev/docs/resources/design-docs, которую мы можем повторять вместе (опять же, как предлагает @Hixie , сосредоточившись пока на максимально

Я просматривал проблему еще раз несколько раз. Честно говоря, я не понимаю, откуда взялось это недоразумение, поэтому я не уверен, что исправить.
В исходном комментарии неоднократно упоминается желание повторного использования / факторизации. Упоминания о шаблоне - это не «Flutter многословен», а «Некоторые логики нельзя использовать повторно».

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

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

  • У них есть некоторая логика, которую необходимо повторно использовать в нескольких виджетах (обработка состояний загрузки / маркировка «сообщений» как прочитанных, когда некоторые виджеты становятся видимыми / ...)
  • Они пытались использовать миксины, что вызвало серьезные архитектурные ошибки.
  • Они также пытались вручную обработать «создать / обновить / удалить», переписав эту логику в нескольких местах, но это вызвало ошибки.
    Кое-где забыли закрыть подписки. В других случаях они не справлялись со сценарием, когда изменяется их экземпляр stream
  • маркировка "сообщений" как прочитанных, когда некоторые виджеты становятся видимыми

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

https://github.com/jocosocial/rainbowmonkey/blob/master/lib/src/views/forums.dart

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

(Кстати, в общем, я бы настоятельно рекомендовал вообще не использовать Streams. Я думаю, что они в целом ухудшают ситуацию.)

(Кстати, в общем, я бы настоятельно рекомендовал вообще не использовать Streams. Я думаю, что они в целом ухудшают ситуацию.)

(Я полностью согласен. Но в настоящее время у сообщества противоположная реакция. Возможно, поможет извлечение ChangeNotifier / Listenable / ValueNotifier из Flutter в официальный пакет)

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

К сожалению нет. Я могу поделиться только тем опытом, который я получил, помогая другим. У меня нет приложения под рукой.

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

В вашей реализации логика не привязана к какому-либо жизненному циклу и размещена внутри _build_, поэтому она как бы помогает обойти проблему.
В этом конкретном случае это может иметь смысл. Я не уверен, был ли этот пример хорошим.

Лучшим примером может быть обновление по запросу.

При типичном обновлении по запросу нам понадобятся:

  • в первой сборке обрабатывать состояния загрузки / ошибки
  • при обновлении:

    • если экран был в состоянии ошибки, покажите экран загрузки еще раз

    • если обновление было выполнено во время загрузки, отменить ожидающие HTTP-запросы

    • если на экране отображались какие-то данные:

    • продолжать показывать данные, пока загружается новое состояние

    • если обновление не удается, продолжать показывать ранее полученные данные и показывать снэк-бар с ошибкой

    • если пользователь выскакивает и повторно выходит на экран во время ожидания обновления, показать экран загрузки

    • убедитесь, что RefreshIndicator говорит "видимый", пока обновление не завершено.

И мы хотим реализовать такую ​​функцию для всех ресурсов и нескольких экранов. Кроме того, на некоторых экранах может потребоваться одновременное обновление нескольких ресурсов.

ChangeNotifier + provider + StatefulWidget будет иметь довольно много трудностей с факторизацией этой логики.

В то время как мои последние эксперименты (которые основаны на неизменности и полагаются на flutter_hooks ) поддерживают весь спектр из коробки:

final productsProvider = FutureProvider<List<Product>>.autoDispose((ref) async {
  final cancelToken = CancelToken();
  ref.onDispose(cancelToken.cancel);

  return await repository.fetchProducts(cancelToken: cancelToken);
});

// ...

Widget build(context) {
  // Listens to the Future created by productsProvider and handles all the refresh logic
  AsyncValue<List<Product>> products = useRefreshProvider(
    productsProvider,
    // TODO consider making a custom hook to encaplusate the snackbar logic
    onErrorAfterRefresh: (err, stack) => Scaffold.of(context).showSnackBar(...),
  );

  return RefreshIndicator(
    onRefresh: () => context.refresh(productsProvider),
    child: products.when(
      loading: () {
        return const SingleChildScrollView(
          physics: AlwaysScrollableScrollPhysics(),
          child: CircularProgressIndicator(),
        );
      },
      error: (err, stack) {
        return SingleChildScrollView(
          physics: const AlwaysScrollableScrollPhysics(),
          child: Text('Oops, something unexpected happened\n$err'),
        );
      },
      data: (products) {
        return ListView.builder(
          itemCount: products.length,
          itemBuilder: (context, index) {
            return ProductItem(products[index]);
          },
        );
      },
    ),
  );
}

Эта логика полностью автономна. Его можно повторно использовать с любым ресурсом на любых экранах.

А если на одном экране нужно обновить сразу несколько ресурсов, мы можем:

AsyncValue<First> first = useRefreshProvider(
  firstProvider,
  onErrorAfterRefresh: ...
);
AsyncValue<Second> second = useRefreshProvider(
  secondProvider,
  onErrorAfterRefresh: ...
);

return RefreshIndicator(
  onRefresh: () {
     return Future.wait([context.refesh(firstProvider), context.refresh(secondProvider)]);
  }
  ...
)

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

Состояние окружающей среды не несет ответственности за определение того, как отображать ошибку, а не загружать и данные.

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

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

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

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

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

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

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

Однако, с другой стороны, значительное количество людей не понимают, что не так в следующем коде:

FutureBuilder<User>(
  future: fetchUser(),
  builder: ...,
)

Этот факт подтверждается тем, насколько популярны Q / AI, созданные на StackOverflow .

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

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

Проблема в том, что это действительно зависит от вашего приложения, вашего состояния и т. Д. Если вопрос здесь просто «как вы управляете состоянием приложения», то ответ не похож на хуки, это много документации, в которой говорится о разных способах этого и рекомендуются разные методы для разных ситуаций ... в основном, этот набор документы: https://flutter.dev/docs/development/data-and-backend/state-mgmt

Есть эфемерное состояние и состояние приложения, но, похоже, есть и другой вариант использования: состояние, которое касается только одного типа виджета, но которое вы, тем не менее, хотите разделить между этим типом виджета.

Например, ScrollController может вызывать какой-то тип анимации, но не обязательно помещать это в глобальное состояние приложения, потому что это не данные, которые нужно использовать во всем приложении. Однако несколько ScrollController могут иметь одинаковую логику, и вы хотите разделить эту логику жизненного цикла между каждым из них. Состояние по-прежнему только для ScrollController s, поэтому не глобальное состояние приложения, а копирование логики подвержено ошибкам.

Более того, вы можете упаковать эту логику, чтобы сделать ее более компонуемой для ваших будущих проектов, а также для других. Если вы посмотрите на сайт useHooks , вы увидите множество логических разделяют определенные общие действия. Если вы используете useAuth вы пишете его один раз, и вам не нужно беспокоиться о том, пропустили ли вы вызов initState или dispose , или есть ли у асинхронной функции then и catch . Функция пишется только один раз, поэтому возможность ошибки практически исчезает. Следовательно, такое решение не только более легко компонуемо для нескольких частей одного и того же приложения и между несколькими приложениями, но и безопаснее для конечного программиста.

Я не возражаю против использования крючков. Насколько я могу судить, этому ничего не мешает. (Если что-то _is_ препятствует этому, сообщите об этом.)

Эта ошибка не связана с хуками, она связана с тем, что «повторное использование логики состояния слишком многословно / сложно», и я все еще пытаюсь понять, почему это требует каких-либо изменений во Flutter. Было много примеров (включая хуки), показывающих, как можно избежать многословия, структурируя свое приложение тем или иным способом, и уже есть много документации по этому поводу.

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

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

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

Сказать, что эта проблема решена, потому что сообщество сделало пакет, все равно что сказать, что Dart не нужны классы данных + типы объединения, потому что я сделал Freezed .
Freezed может понравиться сообществу как решение обеих этих проблем, но мы все равно можем добиться большего.

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

Этой проблеме нужно это.
Помните: одна из целей команды React - сделать хуки частью языка, как в случае с JSX.

Даже без языковой поддержки нам все равно нужна работа в анализаторе; дротик; flutter / devtools; и множество хуков, чтобы упростить все, что делает Flutter (например, для неявной анимации, форм и т. д.).

Я согласен, что это хороший аргумент, даже несмотря на то, что общая философия Flutter заключается в том, чтобы иметь небольшое ядро. По этой причине мы все чаще добавляем новые функции в виде пакетов, даже если они исходят от Google, cf символов и анимации . Это дает нам большую гибкость, чтобы учиться и меняться с течением времени. Мы сделали бы то же самое для этого места, если только нет убедительной технической причины, по которой пакета было недостаточно (а с методами расширения это даже менее вероятно, чем когда-либо).

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

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

Интересный аргумент, @timneath. Сообщество Rust также делает что-то подобное, потому что, однажды введя его в ядро ​​или стандартную библиотеку языка или фреймворка, очень сложно отказаться от него. В случае с Rust это невозможно, поскольку они хотят вечно поддерживать обратную совместимость. Поэтому они ждут, пока прибудут посылки, и соревнуются друг с другом, пока не появятся только несколько победителей, а затем складывают это в язык.

То же самое и с Flutter. Позже может быть что-то получше, чем хуки, точно так же, как React должен был перейти от классов к хукам, но по-прежнему должен был поддерживать классы, и людям пришлось мигрировать. В таком случае, возможно, было бы лучше иметь конкурирующие решения по управлению состоянием перед добавлением в ядро. И, возможно, нам, сообществу, следует вводить новшества поверх зацепок или пытаться найти еще лучшие решения.

Я понимаю эту озабоченность, но речь идет не о решении для управления состоянием.

Такая функция ближе к Inheritedwidget и StatefulWidget. Это примитив низкого уровня, который может быть таким же низким, как языковая функция.

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

context.onDispose(() {

});

И подобные слушатели событий.
Но это невозможно реализовать вне фреймворка.

Не знаю, что бы команда придумала.
Но нельзя исключать, что такое решение должно быть прямо рядом с Element.

Помогают ли в этом расширения?

(Возможно, нам стоит поговорить об этом в другом вопросе. Здесь это как бы не по теме. Я бы предпочел, чтобы у нас была одна проблема для каждой проблемы, которую видят люди, чтобы мы могли обсуждать решения в нужном месте. Это не ясно, как context.onDispose поможет с многословием.)

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

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

В частности, мы могли бы рассмотреть, как и могут ли они работать как в среде выполнения виртуальной машины, так и в среде выполнения JS.

Непонятно, как context.onDispose поможет с многословием.)

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

Способ context.onDispose связан с этой проблемой, с текущим синтаксисом, который у нас есть:

AnimationController controller;

<strong i="11">@override</strong>
void initState() {
  controller = AnimationController(...);
}

<strong i="12">@override</strong>
void dispose() {
  controller.dispose();
}

Проблема в:

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

С context.onDispose мы могли бы:

<strong i="21">@override</strong>
void initState() {
  controller = AnimationController(...);
  context.onDispose(controller.dispose);
}

Интересная часть:

  • это больше не тесно связано с определением класса, поэтому его можно извлечь в функцию.
    Теоретически мы могли бы иметь полусложную логику:
    дротик
    AnimationController someReusableLogic (контекст BuildContext) {
    финальный контроллер = AnimationController (...);
    controller.onDispose (controller.dispose);
    controller.forward ();
    void listener () {}
    controller.addListener (слушатель);
    context.onDispose (() => controller.removeListener (слушатель));
    }
    ...

@override
void initState () {
контроллер = someReusableLogic (контекст);
}
`` ''

  • вся логика собрана вместе. Даже если размер виджета вырастет до 300, логика controller по-прежнему легко читается.

Проблема с этим подходом:

  • context.myLifecycle(() {...}) не поддерживает горячую перезагрузку
  • неясно, как заставить someReusableLogic читать свойства из StatefulWidget без тесной связи функции с определением виджета.
    Например, AnimationController Duration можно передать как параметр виджета. Итак, нам нужно обработать сценарий, в котором изменяется продолжительность.
  • неясно, как реализовать функцию, которая возвращает объект, который может меняться со временем, без необходимости прибегать к ValueNotifier и иметь дело со слушателями

    • Это особенно важно для вычисляемых состояний.


Я подумаю о предложении по языку. У меня есть кое-какие идеи, но сейчас не о чем стоит говорить.

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

Ok. Не могли бы вы написать новую ошибку, в которой конкретно говорится об этом? Эта ошибка буквально называется «Повторное использование логики состояния слишком многословно / сложно». Если проблема не в многословности, значит, проблема не в этом.

С context.onDispose мы могли бы:

<strong i="11">@override</strong>
void initState() {
  controller = AnimationController(...);
  context.onDispose(controller.dispose);
}

Я не уверен, почему здесь имеет значение contextonDispose нарушает наши соглашения об именах). Однако, если вам просто нужен способ зарегистрировать что-то для запуска во время удаления, вы можете легко это сделать сегодня:

mixin StateHelper<T extends StatefulWidget> on State<T> {
  List<VoidCallback> _disposeQueue;

  void queueDispose(VoidCallback callback) {
    _disposeQueue ??= <VoidCallback>[];
    _disposeQueue.add(callback);
  }

  <strong i="17">@override</strong>
  void dispose() {
    if (_disposeQueue != null) {
      for (VoidCallback callback in _disposeQueue)
        callback();
    }
    super.dispose();
  }
}

Назовите это так:

class _MyHomePageState extends State<MyHomePage> with StateHelper<MyHomePage> {
  TextEditingController controller;

  <strong i="21">@override</strong>
  void initState() {
    super.initState();
    controller = TextEditingController(text: 'button');
    queueDispose(controller.dispose);
  }

  ...
AnimationController someReusableLogic(BuildContext context) {
  final controller = AnimationController(...);
  controller.onDispose(controller.dispose);
  controller.forward();
  void listener() {}
  controller.addListener(listener);
  context.onDispose(() => controller.removeListener(listener));
}
...

<strong i="25">@override</strong>
void initState() {
  controller = someReusableLogic(context);
}

Вы тоже можете это сделать:

AnimationController someReusableLogic<T extends StatefulWidget>(StateHelper<T> state) {
  final controller = AnimationController(...);
  state.queueDispose(controller.dispose);
  controller.forward();
  void listener() {}
  controller.addListener(listener);
  state.queueDispose(() => controller.removeListener(listener));
  return controller;
}
...

<strong i="6">@override</strong>
void initState() {
  controller = someReusableLogic(this);
}

Проблема с этим подходом:

  • context.myLifecycle(() {...}) не поддерживает горячую перезагрузку

В этом контексте это не имеет значения, поскольку это только для вещей, вызываемых в initState? Я что-то упускаю?

  • неясно, как заставить someReusableLogic читать свойства из StatefulWidget без тесной связи функции с определением виджета.
    Например, AnimationController Duration может быть передано как параметр виджета. Итак, нам нужно обработать сценарий, в котором изменяется продолжительность.

Довольно просто добавить очередь didChangeWidget, как и очередь удаления:

mixin StateHelper<T extends StatefulWidget> on State<T> {
  List<VoidCallback> _disposeQueue;
  List<VoidCallback> _didUpdateWidgetQueue;

  void queueDispose(VoidCallback callback) {
    _disposeQueue ??= <VoidCallback>[];
    _disposeQueue.add(callback);
  }

  void queueDidUpdateWidget(VoidCallback callback) {
    _didUpdateWidgetQueue ??= <VoidCallback>[];
    _didUpdateWidgetQueue.add(callback);
  }

  <strong i="24">@override</strong>
  void didUpdateWidget(T oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (_didUpdateWidgetQueue != null) {
      for (VoidCallback callback in _didUpdateWidgetQueue)
        callback();
    }
  }

  <strong i="25">@override</strong>
  void dispose() {
    if (_disposeQueue != null) {
      for (VoidCallback callback in _disposeQueue)
        callback();
    }
    super.dispose();
  }
}

AnimationController conditionalAnimator(StateHelper state, ValueGetter<bool> isAnimating, VoidCallback listener) {
  final controller = AnimationController(vsync: state as TickerProvider, duration: const Duration(seconds: 1));
  state.queueDispose(controller.dispose);
  controller.addListener(listener);
  state.queueDispose(() => controller.removeListener(listener));
  if (isAnimating())
    controller.repeat();
  state.queueDidUpdateWidget(() {
    if (isAnimating()) {
      controller.repeat();
    } else {
      controller.stop();
    }
  });
  return controller;
}

Используется так:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  <strong i="6">@override</strong>
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(animating: false),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.animating}) : super(key: key);

  final bool animating;

  <strong i="7">@override</strong>
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with StateHelper<MyHomePage>, SingleTickerProviderStateMixin {
  AnimationController controller;

  <strong i="8">@override</strong>
  void initState() {
    super.initState();
    controller = conditionalAnimator(this, () => widget.animating, () { print(controller.value); });
  }

  <strong i="9">@override</strong>
  Widget build(BuildContext context) {
    return Center(
      child: FadeTransition(
        opacity: controller,
        child: Text('Hello', style: TextStyle(fontSize: 100.0, color: Colors.white)),
      ),
    );
  }
}
  • неясно, как реализовать функцию, которая возвращает объект, который может меняться со временем, без необходимости прибегать к ValueNotifier и иметь дело со слушателями

    • Это особенно важно для вычисляемых состояний.

Не знаете, что это здесь означает, что не так с ValueNotifier и, скажем, ValueListenableBuilder?

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

Ok. Не могли бы вы написать новую ошибку, в которой конкретно говорится об этом? Эта ошибка буквально называется «Повторное использование логики состояния слишком многословно / сложно». Если проблема не в многословности, значит, проблема не в этом.

Я начинаю чувствовать себя неуютно в связи с этим обсуждением. Я уже отвечал на этот вопрос раньше:
Тема этого выпуска - возможность повторного использования, и многословие обсуждается как следствие проблемы повторного использования; не как основная тема.

В верхнем комментарии есть только один пункт, в котором упоминается многословие, и это касается StreamBuilder, ориентированного в основном на 2 уровня отступов.

Я не уверен, почему контекст уместен в этом [...]. Однако, если вам просто нужен способ зарегистрировать что-то для запуска во время удаления, вы можете легко это сделать сегодня:

Когда я поднял context.onDispose , я прямо упомянул, что не думаю, что это хорошее решение.
Я объяснил это, потому что вы спросили, как это связано с дискуссией.

Что касается того, почему context вместо StateHelper , это потому, что это более гибко (например, работа с StatelessWidget)

context.myLifecycle (() {...}) не поддерживает горячую перезагрузку

В этом контексте это не имеет значения, поскольку это только для вещей, вызываемых в initState? Я что-то упускаю?

Мы можем изменить:

initState() {
  context.myLifecycle(() => print('hello'));
}

в:

initState() {
  context.myLifecycle(() => print('world'));
}

Это не повлияет на обратный вызов myLifecycle .

Но если бы мы использовали:

myLifecycle() {
  super.myLifecycle();
  print('hello');
}

тогда сработает горячая перезагрузка.

Не знаете, что это здесь означает, что не так с ValueNotifier и, скажем, ValueListenableBuilder?

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

Кроме того, если мы действительно хотим сделать нашу функцию компонуемой, вместо вашего предложения ValueGetter + queueDidUpdateWidget функции должны будут принимать параметр ValueNotifier качестве параметра:

AnimationController conditionalAnimator(StateHelper state, ValueListenable<bool> isAnimating, VoidCallback listener) {
...
}

поскольку мы можем захотеть получить isAnimating откуда-нибудь, кроме didUpdateWidget зависимости от того, какой виджет использует эту функцию.
В одном месте это может быть didUpdateWidget; в другом - didChangeDependencies; и еще в одном месте он может быть внутри обратного вызова stream.listen .

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

Что касается того, почему context вместо StateHelper , это потому, что это более гибко (например, работа с StatelessWidget)

StatelessWidget предназначен для виджетов без сохранения состояния. Все дело в том, что они не будут создавать состояние, удалять вещи, реагировать на didUpdateWidget и т. Д.

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

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

  • Объявлять, как что-то удалять, нужно делать в том же месте, где это размещается ...
  • ... и место, которое выделяет его, должно запускаться только один раз, поскольку оно выделяет его ...
  • ... и он должен работать с горячей перезагрузкой (которая по определению не перезапускает код, который запускается только один раз) ...
  • ... и он должен иметь возможность создавать состояние, которое работает с виджетами без состояния (которые по определению не имеют состояния) ...
  • ... и он должен включить подключение к таким вещам, как didUpdateWidget и didChangeDependencies ...

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

Честно говоря, мне кажется, что вы просите совершенно другой дизайн фреймворка. Это прекрасно, но это не Flutter. Если бы мы сделали другой фреймворк, это был бы, ну, другой фреймворк, и нам еще предстоит много работы над _это_ фреймворком. На самом деле, многое из того, что вы описываете, очень похоже на то, как разработан Jetpack Compose. Я не большой поклонник этого дизайна, потому что он требует магии компилятора, поэтому отладка того, что происходит, действительно сложна, но, может быть, это больше для вас?

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

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

Как я пытался сказать раньше, лучший способ получить здесь что-то - это описать проблему, с которой вы сталкиваетесь, понятным нам образом, со всеми потребностями, описанными в одном месте и объясненными с вариантами использования.

Я до сих пор не понимаю, как этот главный комментарий не делает этого.
Мне непонятно то, что непонятно другим.

На самом деле, многое из того, что вы описываете, очень похоже на то, как разработан Jetpack Compose. Я не большой поклонник этого дизайна, потому что он требует магии компилятора, поэтому отладка того, что происходит, действительно сложна, но, может быть, это больше для вас?

Я с ним не знаком, но при быстром поиске сказал бы _да_.

Они не исключают друг друга.

Все ли перечисленные выше пункты являются частью проблемы, которую мы пытаемся решить здесь?

но они ставят все флажки

Вы можете перечислить коробки?

Я до сих пор не понимаю, как этот главный комментарий не делает этого.

Например, OP явно говорит, что проблема связана с StatefulWidgets, но в одном из недавних комментариев по этой проблеме говорилось, что конкретное предложение не годится, потому что оно не работает с StatelessWidgets.

В OP вы говорите:

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

Исходя из этого, я предполагаю, что требования включают:

  • Решение не должно быть глубоко вложенным.
  • Решение не должно требовать большого количества аналогичного кода в местах, которые пытаются добавить состояние.

Первый пункт (о вложенности) кажется нормальным. Определенно не пытаюсь предложить нам делать вещи, которые глубоко вложены. (Тем не менее, мы можем не соглашаться с тем, что является глубоко вложенным; это здесь не определено. Другие комментарии позже подразумевают, что построители вызывают глубоко вложенный код, но, по моему опыту, построители довольно хороши, как показано в коде, который я цитировал ранее.)

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

Следующее заявление OP, описывающее проблему:

Повторное использование логики State в нескольких StatefulWidget очень сложно, поскольку эта логика опирается на несколько жизненных циклов.

Честно говоря, я не совсем понимаю, что это значит. «Сложный» для меня обычно означает, что что-то включает в себя много сложной логики, которую трудно понять, но распределение, удаление и реагирование на события жизненного цикла очень просты. Следующее утверждение, которое создает проблему (здесь я пропускаю пример, который явно описан как «несложный» и, следовательно, предположительно не является описанием проблемы):

Проблема начинается, когда мы хотим масштабировать этот подход.

Это подсказало мне, что под «очень сложным» вы имели в виду «очень многословный» и что сложность возникла из-за того, что много вхождений аналогичного кода, поскольку единственная разница между приведенным вами «несложным» примером и «очень сложным» "Результатом масштабирования примера является буквально то, что один и тот же код повторяется много раз (т.е. многословность, шаблонный код).

Это дополнительно подтверждается следующим утверждением, описывающим проблему:

Копирование этой логики повсюду "работает", но создает слабость в нашем коде:

  • легко забыть переписать один из шагов (например, забыть вызвать dispose )

Так что, по-видимому, это очень сложно, потому что многословие позволяет легко сделать ошибку при копировании и вставке кода? Но опять же, когда я попытался решить эту проблему, которую я бы назвал «многословием», вы сказали, что проблема не в многословии.

  • это добавляет много шума в код

Опять же, это звучит для меня как многословие / шаблон, но опять же вы объяснили, что это не так.

Остальная часть OP просто описывает решения, которые вам не нравятся, поэтому, по-видимому, не описывает проблему.

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

Я думаю, что это недоразумение сводится к значению слова.
Например:

это добавляет много шума в код

Опять же, это звучит для меня как многословие / шаблон, но опять же вы объяснили, что это не так.

Речь идет не о количестве controller.dispose() , а о ценности, которую эти строки кода приносят читателю.
Эта линия всегда должна быть и всегда одна и та же. Таким образом, его ценность для читателя почти равна нулю.

Важно не наличие этой линии, а ее отсутствие.

Проблема в том, что чем больше у нас таких controller.dispose() , тем больше вероятность пропустить реальную проблему в нашем методе удаления.
Если у нас есть 1 контроллер и 0 утилизировать, легко поймать
Если у нас есть 100 контроллеров и 99 утилизируемых, найти недостающий будет сложно.

Тогда у нас есть:

Так что, по-видимому, это очень сложно, потому что многословие позволяет легко сделать ошибку при копировании и вставке кода? Но опять же, когда я попытался решить эту проблему, которую я бы назвал «многословием», вы сказали, что проблема не в многословии.

Как я уже упоминал в предыдущем пункте, не все строки кода равны.

Если сравнить:

+ T state;

<strong i="24">@override</strong>
void initState() {
  super.initState();
+  state = widget.valueNotifier.value;
+  widget.valueNotifier.addListener(_listener);
}

+ void _listener() => seState(() => state = widget.valueNotifier.value);

void dispose() {
+ widget.valueNotifier.removeListener(_listener);
  super.dispose();
}

против:

+ ValueListenableBuilder<T>(
+   valueListenable: widget.valueNotifier,  
+   builder: (context, value, child) {

+    },
+ );

тогда оба этих фрагмента содержат одинаковое количество строк и выполняют одно и то же.
Но предпочтительнее ValueListenableBuilder .

Причина в том, что важно не количество строк, а то, что это за строки.

В первом фрагменте есть:

  • 1 декларация о собственности
  • 1 объявление метода
  • 1 задание
  • 2 вызова методов
  • все они распределены по двум разным жизненным циклам. 3, если мы включим сборку

Во втором фрагменте есть:

  • Создание экземпляра 1 класса
  • 1 анонимная функция
  • нет жизненного цикла. 1, если мы включим build

Это делает ValueListenableBuilder _simpler_.

Есть еще то, о чем не говорится в этих строках:
ValueListenableBuilder обрабатывает изменение valueListenable течением времени.
Даже в сценарии, когда widget.valueNotifier не меняется со временем, пока мы говорим, это не повредит.
Однажды это утверждение может измениться. В этом случае ValueListenableBuilder изящно обрабатывает новое поведение, тогда как с первым фрагментом теперь у нас есть ошибка.

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


С этим, я думаю, мы оба можем согласиться с тем, что ValueListenableBuilder предпочтительнее.
Тогда возникает вопрос: «Почему бы не иметь эквивалента ValueListenableBuilder для каждой повторно используемой логики состояния?»

Например, вместо:

final controller = TextEditingController(text: 'hello world');
...
controller.dispose();

мы бы хотели иметь:

TextEditingControllerBuilder(
  initialText: 'hello world',
  builder: (context, controller) {

  },
);

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

Этот пример может быть несколько тривиальным, но мы могли бы использовать этот принцип для немного более продвинутой логики многократного использования (например, вашего ModeratorBuilder ).

Это «нормально» для небольших фрагментов. Но это вызывает некоторые проблемы, поскольку мы хотим масштабировать подход:

  • Строители возвращаются к проблеме «слишком много шума».

Например, я видел, как некоторые люди управляли своей моделью таким образом:

class User {
  final ValueNotifier<String> name;
  final ValueNotifier<int> age;
  final ValueNotifier<Gender> gender;
}

Но тогда виджет может захотеть прослушать одновременно name , age и gender .
Значит, нам нужно будет сделать:

return ValueListenableBuilder<String>(
  valueListenable: user.name,
  builder: (context, userName, _) {
    return ValueListenableBuilder<int>(
      valueListenable: user.age,
      builder: (context, userAge, _) {
        return ValueListenableBuilder<Gender>(
          valueListenable: user.gender,
          builder: (context, userGender, _) {
            return Text('$userGender. $userName ($userAge)');
          },
        );
      },
    );
  },
);

Это явно не идеально. Мы удалили загрязнение внутри initState / dispose чтобы загрязнить наш метод build .

(давайте проигнорируем Listenable.merge ради примера. Здесь это не имеет значения; это больше о композиции)

Если бы мы широко использовали Builders, легко увидеть себя в этом точном сценарии - и без эквивалента Listenable.merge (не то чтобы мне этот конструктор нравился, начнем с 😛)

  • Написание собственного конструктора утомительно

    Нет простого решения для создания Строителя. Здесь нам не поможет никакой инструмент рефакторинга - мы не можем просто «извлечь как Builder».
    Более того, это не обязательно интуитивно. Создание кастомного Builder - не первое, о чем подумают люди, тем более, что многие будут против шаблона (хотя я нет).

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

  • Манипулировать деревом строителей утомительно

    Допустим, мы хотели удалить ValueListenableBuilder в нашем предыдущем примере или добавить новый, это непросто.
    Мы можем потратить несколько минут на подсчет () и {}, чтобы понять, почему наш код не компилируется.


Хуки предназначены для решения проблем Builder, о которых мы только что говорили.

Если мы проведем рефакторинг предыдущего примера до хуков, у нас будет:

final userName = useValueListenable(user.name);
final useAge = useValueListenable(user.age);
final useGender = useValueListenable(user.gender);

return Text('$userGender. $userName ($userAge)');

Это идентично предыдущему поведению, но теперь код имеет линейный отступ.
Что значит:

  • код значительно более читабельный
  • редактировать легче. Нам не нужно бояться () {}; чтобы добавить новую строку.

Это один из главных provider . Он устранил много вложенности, введя MultiProvider .

Точно так же, в отличие от подхода initState / dispose , мы получаем выгоду от горячей перезагрузки.
Если бы мы добавили новый useValueListenable , изменение вступило бы в силу немедленно.

И, конечно же, у нас все еще есть возможность извлекать повторно используемые примитивы:

String useUserLabel(User user) {
  final userName = useValueListenable(user.name);
  final useAge = useValueListenable(user.age);
  final useGender = useValueListenable(user.gender);

  return '$userGender. $userName ($userAge)';
}

Widget build(context) {
  final label = useUserLabel(user);
  return Text(label);
}

и такое изменение можно автоматизировать с помощью extract as function , что будет работать в большинстве сценариев.


Это ответ на ваш вопрос?

Конечно. Проблема с чем-то подобным в том, что у него просто недостаточно информации, чтобы делать то, что нужно. Например:

Widget build(context) {
  if (random.nextBool())
    final title = useLabel(title);
  final label = useLabel(name);
  return Text(label);
}

... в конечном итоге приводил к сбоям в очень запутанной ситуации.

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

Решение Property, которое я предлагал ранее, или что-то полученное на его основе, похоже, избегает магии компилятора, при этом достигая целей, которые вы описали, по размещению всего кода в одном месте. Я действительно не понимаю, почему это не сработает для этого. (Очевидно, это будет расширено, чтобы также подключиться к didChangeDependencies и т. Д., Чтобы стать полноценным решением.) (Мы бы не стали помещать это в базовую структуру, потому что это нарушило бы наши требования к производительности.)

Как вы говорите, именно из-за ошибок, которые могут возникнуть, это причина, по которой хуки не следует вызывать условно. См. Документ Rules of Hooks от ReactJS для более подробной информации. Основная суть заключается в том, что, поскольку в их реализации они отслеживаются по порядку вызовов, их условное использование нарушит этот порядок вызовов и, таким образом, не позволит их правильно отслеживать. Чтобы правильно использовать ловушку, вы вызываете их на верхнем уровне в build без какой-либо условной логики. В JS-версии вы вернетесь

const [title, setTitle] = useLabel("title");

Эквивалент Dart может быть похожим, только длиннее из-за отсутствия распаковки, как это делает JS:

var titleHook = useLabel("title");
String title = titleHook.property;
Function setTitle = titleHook.setter;

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

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

Отладка / условное поведение не является проблемой. Вот почему плагин анализатора важен. Такой плагин:

  • предупреждать, если функция использует ловушку без имени useMyFunction
  • предупредить, если крючок используется условно
  • предупреждать, если ловушка используется в цикле / обратном вызове.

Это покрывает все возможные ошибки. React доказал, что это возможно.

Тогда у нас остаются преимущества:

  • более читаемый код (как показано ранее)
  • лучше горячая перезагрузка
  • более многоразовый / составной код
  • более гибкие - мы можем легко создавать вычисляемые состояния.

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

Например, мы можем иметь:

class Example extends HookWidget {
  final int userId;

  Widget build(context) {
    // Calls fetchUser whenever userId changes
    // It is the equivalent to both initState and didUpdateWidget
    final future = useMemo1(() => fetchUser(userId), userId);

    final snapshot = useFuture(future);
    if (!snapshot.hasData)
      return Text('loading');
    return Text(snapshot.data.name);
  }  
}

Такой хук useMemo позволяет легко оптимизировать производительность и декларативно обрабатывать как init + update, что также позволяет избежать ошибок.

Это то, чего не хватает в предложении Property / context.onDispose .
Их трудно использовать для декларативных состояний без тесной привязки логики к жизненному циклу или усложнения кода с помощью ValueNotifier .

Подробнее о том, почему предложение ValueGetter нецелесообразно:

Мы можем захотеть провести рефакторинг:

final int userId;

Widget build(context) {
  final future = useMemo1(() => fetchUser(userId), userId);

в:

Widget build(context) {
  final userId = Model.of(context).userId;
  final future = useMemo1(() => fetchUser(userId), userId);

С хуками это изменение работает безупречно, поскольку useMemo не привязан к какому-либо жизненному циклу.

Но с Property + ValueGetter нам пришлось бы изменить реализацию Property чтобы заставить эту работу, что нежелательно, поскольку код Property может быть повторно используется в нескольких местах. Таким образом, мы снова потеряли возможность повторного использования.

FWIW этот фрагмент эквивалентен:

class Example extends StatefulWidget {
  final int userId;
  <strong i="45">@override</strong>
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  Future<User> future;

  <strong i="46">@override</strong>
  void initState() {
    super.initState();
    future = fetchUser(widget.userId);
  }

  <strong i="47">@override</strong>
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.userId != widget.userId) {
      future = fetchUser(widget.userId);
    }
  }

  <strong i="48">@override</strong>
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: future,
      builder: (context, snapshot) {
        if (!snapshot.hasData)
          return Text('loading');
        return Text(snapshot.data.name);
      },
    );
  }
}

Полагаю, нам нужно будет найти решение, которое решает те же проблемы, что упоминает @rrousselGit, но также имеет в виду удобочитаемость и отлаживаемость. У Vue есть собственная реализация, которая может быть более соответствующей тому, что вы ищете, где условия или порядок вызовов не вызывают ошибок, как в React.

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

В любом случае, я не думаю, что мы когда-нибудь достигнем консенсуса. Похоже, мы не согласны даже в том, что можно читать

Напоминаю, что я делюсь этим только потому, что знаю, что у сообщества есть некоторые проблемы с этой проблемой. Я лично не возражаю, если Flutter ничего не сделает с этим (хотя я нахожу это печальным), если у нас есть:

  • правильная система плагинов анализатора
  • возможность использовать пакеты внутри дартпада

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

Вот новая версия предыдущей идеи Property . Он обрабатывает didUpdateWidget и удаление (и его легко можно заставить обрабатывать другие подобные вещи, например didChangeDependencies); поддерживает горячую перезагрузку (вы можете изменить код, регистрирующий свойство и горячую перезагрузку, и все будет правильно); он безопасен по типу, не требуя явных типов (полагается на вывод); в нем есть все в одном месте, кроме объявления и использования свойств, а производительность должна быть достаточно хорошей (хотя и не такой хорошей, как у более подробных способов решения задач).

Свойство / PropertyManager:

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

typedef InitStateCallback<T> = T Function(T oldValue);
typedef DidUpdateWidgetCallback<T, W extends StatefulWidget> = T Function(W oldWidget);

class Property<T, W extends StatefulWidget> {
  Property({
    T value,
    this.initState,
    this.didUpdateWidget,
    this.dispose,
  }) : _value = value;

  T get value {
    assert(_value != null);
    return _value;
  }
  T _value;

  final InitStateCallback<T> initState;
  void _initState(Property<T, W> oldProperty) {
    if (initState != null)
      _value = initState(oldProperty?.value);
    assert(_value != null);
  }

  final DidUpdateWidgetCallback<T, W> didUpdateWidget;
  void _didUpdateWidget(StatefulWidget oldWidget) {
    if (didUpdateWidget != null) {
      final T newValue = didUpdateWidget(oldWidget);
      if (newValue != null)
        _value = newValue;
    }
  }

  final ValueSetter<T> dispose;
  void _dispose() {
    if (dispose != null)
      dispose(value);
  }
}

mixin PropertyManager<W extends StatefulWidget> on State<W> {
  final Set<Property<Object, W>> _properties = <Property<Object, W>>{};
  bool _ready = false;

  Property<T, W> register<T>(Property<T, W> oldProperty, Property<T, W> property) {
    assert(_ready);
    if (oldProperty != null) {
      assert(_properties.contains(oldProperty));
      _properties.remove(oldProperty);
    }
    assert(property._value == null);
    property._initState(oldProperty);
    _properties.add(property);
    return property;
  }

  <strong i="9">@override</strong>
  void initState() {
    super.initState();
    _ready = true;
    initProperties();
  }

  <strong i="10">@override</strong>
  void reassemble() {
    super.reassemble();
    initProperties();
  }

  <strong i="11">@protected</strong>
  <strong i="12">@mustCallSuper</strong>
  void initProperties() { }

  <strong i="13">@override</strong>
  void didUpdateWidget(W oldWidget) {
    super.didUpdateWidget(oldWidget);
    for (Property<Object, W> property in _properties)
      property._didUpdateWidget(oldWidget);
  }

  <strong i="14">@override</strong>
  void dispose() {
    _ready = false;
    for (Property<Object, W> property in _properties)
      property._dispose();
    super.dispose();
  }
}

Вот как бы вы это использовали:

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'properties.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  <strong i="18">@override</strong>
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Example(userId: 1),
    );
  }
}

class User {
  User(this.name);
  final String name;
}

Future<User> fetchUser(int userId) async {
  await Future.delayed(const Duration(seconds: 2));
  return User('user$userId');
}

class Example extends StatefulWidget {
  Example({ Key key, this.userId }): super(key: key);

  final int userId;

  <strong i="19">@override</strong>
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> with PropertyManager {
  Property future;

  <strong i="20">@override</strong>
  void initProperties() {
    super.initProperties();
    future = register(future, Property(
      initState: (_) => fetchUser(widget.userId),
      didUpdateWidget: (oldWidget) {
        if (oldWidget.userId != widget.userId)
          return fetchUser(widget.userId);
      }
    ));
  }

  <strong i="21">@override</strong>
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: future.value,
      builder: (context, snapshot) {
        if (!snapshot.hasData) return Text('loading');
        return Text(snapshot.data.name);
      },
    );
  }
}

Для удобства вы можете создать подготовленные подклассы Property для таких вещей, как AnimationControllers и т. Д.,

Вы, вероятно, можете сделать такую ​​версию, которая будет работать и в методах State.build ...

Я разделяю некоторые сомнения, которые вызывает @Hixie . С другой стороны, я вижу явные преимущества, которые имеют хуки, и, похоже, многим разработчикам это нравится.
Моя проблема с пакетным подходом, предложенным @timneath, заключается в том, что код, использующий
Если в пакетах начнут реализовываться вещи, которые должны соответствовать отклику фреймворка, мы получим множество различных диалектов Flutter, что затруднит изучение новых баз кода. Так что я, вероятно, начал бы использовать хуки, как только они станут частью Flutter.
Это очень похоже на мой нынешний взгляд на замороженный пакет. Мне нравится эта функциональность, но если объединения и классы данных не являются частью Dart, я не хочу включать их в свою базу кода, потому что людям будет сложнее читать мой код.

@escamoteur Насколько я понимаю, вы предлагаете кардинально изменить принцип работы виджетов? Или вы предлагаете какие-то особые новые способности? Учитывая, как такие вещи, как хуки и указанное выше предложение собственности, возможны без каких-либо изменений в основной структуре, мне не ясно, что вы на самом деле хотели бы изменить.

Это ортогонально из разговора о любых предлагаемых изменениях, но я думаю, что то, что я слышал от @escamoteur , @rrousselGit и других как здесь, так и в других местах, заключается в том, что присутствие _в_ фреймворке воспринимается как важный способ установить законность конкретного подход. Поправьте меня, если не согласны.

Я понимаю это мышление - поскольку многое происходит из-за того, что фреймворк (например, DartPad сегодня не поддерживает сторонние пакеты, некоторые клиенты подозревают, от скольких пакетов они зависят после того, как они были записаны с помощью NPM, он кажется более «официальным», он гарантированно продвинется вперед с такими изменениями, как нулевая безопасность).

Но включение в него также сопряжено со значительными расходами: в частности, это закостеняет подход и API. Вот почему мы оба держим очень высокую планку того, что мы добавляем, особенно когда нет единодушного согласия (см. Управление состоянием), где есть вероятность эволюции или где мы можем так же легко добавить что-то, как пакет.

Интересно, нужно ли нам задокументировать нашу философию «сначала пакет», но опять же, _ где_ она идет, это отдельно от обсуждения того, _ что_ мы можем захотеть изменить, чтобы улучшить повторное использование логики состояния.

Наша политика пакетов задокументирована здесь: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#deciding -where-to-put-code

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

Вот почему я не утверждаю, что provider следует объединить во Flutter, но также считаю, что эта проблема описывает проблему, которую Flutter должен решить изначально (не обязательно с помощью хуков, конечно).

Вместе с Provider Flutter предоставляет встроенный примитив для решения такого рода проблем: InheritedWidgets.
Провайдер добавляет только самоуверенный слой наверху, чтобы сделать его «лучше».

Крючки разные. Они примитивны. Они представляют собой однозначное низкоуровневое решение конкретной проблемы: повторное использование логики в нескольких состояниях.
Это не конечный продукт, а то, что люди должны использовать для создания собственных пакетов (как я сделал с hooks_riverpod )

Для меня было бы полезно (с точки зрения понимания желаний здесь и потребностей, которые удовлетворяют крючки и т. Д.), Если бы кто-нибудь мог предоставить подробный обзор того, как подход Property, который я нарисовал выше, сравнивается с крючками. (Моя цель с идеей Property состоит в том, чтобы выровнять мнение поверх фреймворка, чтобы решить проблему повторного использования логики в нескольких состояниях.)

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

Это предложение в некоторой степени увеличивает удобочитаемость за счет перегруппировки всей логики в одном месте; но это не решает проблему повторного использования

В частности, мы не можем извлечь:

Property(
  initState: (_) => fetchUser(widget.userId),
  didUpdateWidget: (oldWidget) {
    if (oldWidget.userId != widget.userId)
      return fetchUser(widget.userId);
  }
)

из _ExampleState и повторно использовать его в другом виджете, поскольку логика привязана к Example и initState + didUpdateWidget

Как бы это выглядело с крючками?

Я согласен с @timneath , увидев нечто подобное в сообществе Rust. Очень сложно извлечь что-то из ядра после того, как оно находится внутри. Шаблон BLoC был указан до того, как появился поставщик, но теперь поставщик является рекомендуемой версией. Возможно, flutter_hooks может быть таким же «благословенным» вариантом. Я говорю это, потому что в будущем могут появиться улучшения по сравнению с крючками, которые люди придумывают. React, у которого теперь есть хуки, не может их изменить или выйти из них. Они должны поддерживать их так же, как и компоненты классов, по сути, вечно, поскольку они находятся в ядре. Поэтому я согласен с философией упаковки.

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

Как бы это выглядело с крючками?

Без использования каких-либо примитивных хуков, поставляемых React / flutter_hooks, мы могли бы получить:

class FetchUser extends Hook<AsyncSnapshot<User>> {
  const FetchUser(this.userId);
  final int userId;

  <strong i="8">@override</strong>
  _FetchUserState createState() => _FetchUserState();
}

class _FetchUserState extends HookState<AsyncSnapshot<User>, FetchUser> {
  Future<User> userFuture;

  <strong i="9">@override</strong>
  void initHook() {
    userFuture = fetchUser(hook.userId);
  }  

  void didUpdateHook(FetchUser oldHook) {
    if (oldHook.userId != hook.userId)
      userFuture = fetchUser(hook.userId);
  }


  <strong i="10">@override</strong>
  User build() {
    return useFuture(userFuture);
  }
}

Затем использовали:

class Example extends HookWidget {
  const Example({Key key, this.userId}) : super(key: key);

  final int userId;

  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    AsyncSnapshot<User> user = use(FetchUser(userId));

    if (!user.hasData)
      return CircularProgressIndicator();
    return Text(user.data.name);
  }
}

В этой ситуации логика полностью независима от Example и жизненных циклов StatefulWidget .
Таким образом, мы могли бы повторно использовать его в другом виджете, который по-другому управляет своим userId . Возможно, этот другой виджет будет StatefulWidget который управляет своим userId изнутри. Возможно, вместо этого он получит userId от InheritedWidget.

Этот синтаксис должен сделать очевидным, что хуки подобны независимым объектам State со своими собственными жизненными циклами.

Кстати, одним из недостатков подхода «сначала пакет» является то, что авторы пакетов с меньшей вероятностью будут публиковать пакеты, полагаясь на ловушки для решения проблем.

Например, одна из распространенных проблем, с которыми сталкиваются пользователи провайдера, заключается в том, что они хотят автоматически избавляться от состояния провайдера, когда он больше не используется.
Проблема в том, что пользователи провайдера также очень любят синтаксис context.watch / context.select в отличие от подробного синтаксиса Consumer(builder: ...) / Selector(builder:...) .
Но у нас не может быть и этого красивого синтаксиса, и решения упомянутой ранее проблемы без хуков (или https://github.com/flutter/flutter/pull/33213, который был отклонен).

Проблема в:
Провайдер не может полагаться на flutter_hooks в решении этой проблемы.
Ввиду того, насколько популярен провайдер, было бы неразумно полагаться на хуки.

В итоге я остановился на:

  • Forking Provider (под кодовым названием Riverpod )
  • добровольно потерять "фаворит Flutter" / рекомендацию Google как следствие
  • решить эту проблему (и еще несколько)
  • добавить зависимость от хуков, чтобы предложить синтаксис, который понравится людям, которым нравится context.watch .

Я вполне доволен тем, что придумал, так как считаю, что он дает значительное улучшение по сравнению с Provider (делает InheritedWidgets безопасным для компиляции).
Но путь туда оставил у меня неприятное послевкусие.

Насколько я могу судить, между версией хуков и версией Property есть три основных отличия:

  • Версия Hooks - это намного больше вспомогательного кода
  • Версия Property - это намного больше шаблонного кода
  • У версии Hooks есть проблема в методах сборки, когда, если вы вызываете хуки в неправильном порядке, все идет плохо, и на самом деле нет никакого способа сразу увидеть это из кода.

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

Я думаю, что хорошее решение не должно зависеть от того, что об этом знают другие пакеты. Не важно, во фреймворке он или нет. Люди, которые его не используют, не должны быть проблемой. Если люди не используют его, это проблема, то, IMHO, это красный флаг для API.

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

Наличие кода в одном месте не означает, что его можно использовать повторно.
Не могли бы вы создать дополнительный виджет, который повторно использует код, который в настоящее время находится внутри _ExampleState в другом виджете?
С одной изюминкой: этот новый виджет должен управлять своим идентификатором пользователя внутри своего состояния, так что у нас есть:

class _WhateverState extends State with PropertyManager {
  // may change over time after some setState calls
  int userId;
}

Если люди не используют его, это проблема, то, IMHO, это красный флаг для API.

Люди, которые что-то не используют, потому что это неофициально, не означает, что API плохой.

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

Даже с самим Provider, который сейчас широко распространен и почти официально, я видел, как люди говорили: «Я предпочитаю использовать встроенные InheritedWidgets, чтобы избежать добавления зависимости».

Не могли бы вы создать дополнительный виджет, который повторно использует код, который в настоящее время находится внутри _ExampleState в другом виджете?

Рассматриваемый код предназначен для получения идентификатора пользователя из виджета и его передачи методу fetchUser. Код для управления изменением userId локально в одном и том же объекте будет другим. Кажется, это нормально? Я не совсем уверен, какую проблему вы здесь пытаетесь решить.

Для записи я бы не стал использовать свойство Property, чтобы делать то, что вы описываете, это будет просто выглядеть так:

class Example extends StatefulWidget {
  Example({ Key key }): super(key: key);

  <strong i="10">@override</strong>
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> with PropertyManager {
  int _userId;
  Future<User> _future;

  void _setUserId(int newId) {
    if (newId == _userId)
      return;
    setState(() {
      _future = fetchUser(_userId);
    });
  }

  // ...code that uses _setUserId...

  <strong i="11">@override</strong>
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: _future.value,
      builder: (context, snapshot) {
        if (!snapshot.hasData) return Text('loading');
        return Text(snapshot.data.name);
      },
    );
  }
}

Люди, которые что-то не используют, потому что это неофициально, не означает, что API плохой.

Согласовано.

Тот факт, что люди не используют что-то плохое, означает, что API плохой. Когда вы говорите «Авторы пакетов с меньшей вероятностью будут публиковать пакеты, полагающиеся на хуки для решения проблем», это означает, что хуки зависят от других людей, использующих их, чтобы быть полезными для вас. Хороший API, ИМХО, не станет плохим, если его никто не примет; он должен сохраняться, даже если об этом никто не знает. Например, приведенный выше пример Property не зависит от других пакетов, использующих его сам по себе.

Даже с самим Provider, который сейчас широко распространен и почти официально, я видел, как люди говорили: «Я предпочитаю использовать встроенные InheritedWidgets, чтобы избежать добавления зависимости».

Что не так с людьми, предпочитающими использовать InheritedWidget? Я не хочу навязывать людям решение. Им следует использовать то, что они хотят использовать. Вы буквально описываете проблему. Решение для людей, предпочитающих использовать InheritedWidget, - уйти с их пути и позволить им использовать InheritedWidget.

. Хороший API, ИМХО, не станет плохим, если его никто не примет; он должен сохраняться, даже если об этом никто не знает. Например, приведенный выше пример свойства не зависит от других пакетов, использующих его, чтобы быть полезным.

Есть недоразумение.

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


Код для управления изменением userId локально в одном и том же объекте будет другим. Кажется, это нормально? Я не совсем уверен, какую проблему вы здесь пытаетесь решить.

Для записи я бы не стал использовать свойство Property, чтобы делать то, что вы описываете, это будет просто выглядеть так:

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

С помощью хуков мы могли бы повторно использовать FetchUser :

class _WhateverState extends State with PropertyManager {
  // may change over time after some setState calls
  int userId;

  Widget build(context) {
    final user = use(FetchUser(userId));
  }
}

С помощью хуков мы могли бы повторно использовать FetchUser :

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

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

ИМХО, хорошее решение проблемы повторного использования кода вообще не обязательно должно быть принято Провайдером. Они были бы полностью ортогональными понятиями. Это то, о чем в руководстве по стилю Flutter говорится под заголовком «Избегайте компилирования».

Не понимаю, почему это желательно. У FetchUser нет интересного кода, это просто переходник от хуков к функции fetchUser. Почему бы просто не вызвать fetchUser напрямую? Код, который вы повторно используете, не интересный код.

Неважно. Мы пытаемся продемонстрировать возможность повторного использования кода. fetchUser может быть любым, например ChangeNotifier.addListener .

Мы могли бы иметь альтернативную реализацию, которая не зависит от fetchUser , и просто предоставить API для неявной выборки данных:

int userId;

Widget build(context) {
  AsyncSnapshot<User> user = use(ImplicitFetcher<User>(userId, fetch: () => fetchUser(userId));
}

ИМХО, хорошее решение проблемы повторного использования кода вообще не обязательно должно быть принято Провайдером. Они были бы полностью ортогональными понятиями. Это то, о чем в руководстве по стилю Flutter говорится под заголовком «Избегайте компилирования».

Вот почему я упомянул, что крючки примитивны.

В качестве метафоры:
package:animations зависит от Animation . Но это не проблема, потому что это примитив ядра.
Было бы иначе, если бы вместо package:animations использовался форк Animation поддерживаемый сообществом.

@escamoteur Насколько я понимаю, вы предлагаете кардинально изменить принцип работы виджетов? Или вы предлагаете какие-то особые новые способности? Учитывая, как такие вещи, как хуки и указанное выше предложение собственности, возможны без каких-либо изменений в основной структуре, мне не ясно, что вы на самом деле хотели бы изменить.

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

Он бы не запретил делать то, что было раньше.

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

Люди всегда следуют «официальному способу» работы, и я считаю, что не должно быть двух официальных способов использования Flutter.

Неважно. Мы пытаемся продемонстрировать возможность повторного использования кода. fetchUser может быть любым, например ChangeNotifier.addListener .

Конечно. Вот для чего подходят функции: абстрагирование кода. Но у нас уже есть функции. Приведенный выше код свойства и приведенный выше код _setUserId показывают, что вы можете перенести весь код, вызывающий эти функции, в одно место без какой-либо конкретной помощи со стороны фреймворка. Зачем нам нужны хуки, чтобы обернуть вызовы этих функций?

ИМХО, хорошее решение проблемы повторного использования кода вообще не обязательно должно быть принято Провайдером. Они были бы полностью ортогональными понятиями. Это то, о чем в руководстве по стилю Flutter говорится под заголовком «Избегайте компилирования».

Вот почему я упомянул, что крючки примитивны.

Они удобны, они не примитивны. Если бы они были примитивными, на вопрос «в чем проблема» было бы гораздо легче ответить. Вы бы сказали: «Вот что я хочу сделать, но не могу».

В качестве метафоры:
package:animations зависит от Animation . Но это не проблема, потому что это примитив ядра.
Было бы иначе, если бы вместо package:animations использовался форк Animation поддерживаемый сообществом.

Иерархия классов Animation делает кое-что фундаментальное: она вводит тикеры и способ их управления и подписки на них. Без иерархии классов анимации вам придется изобрести что-то вроде иерархии классов анимации для создания анимации. (В идеале что-нибудь получше. Это не наша лучшая работа.) Хуки не представляют новой фундаментальной функции. Он просто дает возможность писать один и тот же код по-разному. Может случиться так, что этот код проще или разложен на другие факторы, чем в противном случае, но это не примитив. Для написания кода, который делает то же самое, что и код, использующий хуки, вам не нужна структура, подобная хукам.


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

Крючки _ это_ примитивы
Вот ветка Дэна: https://twitter.com/dan_abramov/status/1093698629708251136, объясняющая это. Некоторые формулировки различаются, но логика в основном применима к Flutter из-за сходства между компонентами класса React и Flutter StatefulWidgets.

В частности, вы можете думать о flutter_hooks как о динамических миксинах состояния.

Если бы они были примитивными, на вопрос «в чем проблема» было бы гораздо легче ответить. Вы бы сказали: «Вот что я хочу сделать, но не могу».

Это в OP:

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

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

Для написания программы вам не нужны классы. Но классы позволяют вам структурировать ваш код и разложить его на множители.
А классы - примитивы.

То же самое и с миксинами, которые тоже являются примитивами

Крючки - это тоже самое.

Зачем нам нужны хуки, чтобы обернуть вызовы этих функций?

Когда нам нужно вызвать эту логику не в одном месте, а в двух.

Повторное использование такой логики через примеси или функции невозможно.

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

До сих пор в этой ветке я не видел другого решения, кроме хуков @rrousselGit, которые решают и упрощают повторное использование и составление логики состояния.

Конечно, в последнее время я не очень много дартс и флаттер, поэтому в приведенных выше примерах кода решения для свойств могу чего-то не хватать, но есть ли какие-нибудь решения? Какие есть варианты сегодня, которые не требуют копирования и вставки вместо повторного использования?
Каков ответ на вопрос @rrousselGit :

Не могли бы вы создать дополнительный виджет, который повторно использует код, находящийся в настоящее время внутри _ExampleState, в другом виджете?
С одной изюминкой: этот новый виджет должен управлять своим идентификатором пользователя внутри своего состояния.

Если невозможно повторно использовать такую ​​простую логику состояния с решением свойств выше, каковы другие варианты?
Ответ прост: это не должно быть легко повторно использовать во флаттере? Что совершенно нормально, но ИМХО немного грустно.

Кстати, SwiftUI делает это новым / другим вдохновляющим способом? Или им не хватает такой же возможности повторного использования логики состояния? Сам вообще не использовал swiftui. Может, все слишком по-другому?

По сути, все строители. Строители - единственный способ повторно использовать состояние на данный момент.
Хуки делают конструкторы более удобочитаемыми и легкими в создании.


Вот коллекция нестандартных крючков, которые я или некоторые клиенты сделали в прошлом месяце для разных проектов:

  • useQuery - эквивалент хука ImplicitFetcher я дал ранее, но вместо этого выполняет запрос GraphQL.
  • useOnResume который дает обратный вызов для выполнения настраиваемого действия над AppLifecycleState.resumed без необходимости
    заняться проблемой создания WidgetsBindingObserver
  • useDebouncedListener который слушает слушателя (обычно TextField или ScrollController), но с отладкой слушателя
  • useAppLinkService который позволяет виджетам выполнять некоторую логику для настраиваемого события, похожего на AppLifecycleState.resumed но с бизнес-правилами.
  • useShrinkUIForKeyboard для плавной обработки внешнего вида клавиатуры. Он возвращает логическое значение, которое указывает, должен ли пользовательский интерфейс адаптироваться к нижнему отступу или нет (который основан на прослушивании focusNode)
  • useFilter , который объединяет useDebouncedListener и useState (примитивный перехватчик, объявляющий одно свойство), чтобы предоставить фильтр для панели поиска.
  • useImplicitlyAnimated<Int/Double/Color/...> - эквивалент TweenAnimationBuilder в качестве крючка

Приложения также используют множество низкоуровневых хуков для разной логики.

Например, вместо:

Whatever whatever;

initState() {
  whatever = doSomething(widget.id);
}

didUpdateWidget(oldWidget) {
  if (oldWidget.id != widget.id)
    whatever = doSomething(widget.id);
}

Они делают:

Widget build(context) {
  final whatever = useUnaryEvent<Whatever, int>(widget.id, (int id) => doSomething(id));
}

Это позволяет избежать дублирования между initState / didUpdateWidget / didChangeDependencies .

Они также используют много useProvider от Riverpod, которые в противном случае должны были бы быть StreamBuilder / ValueListenableBuilder


Важная часть состоит в том, что виджеты редко используют «только один крючок».
Например, виджет может делать

class ChatScreen extends HookWidget {
  const ChatScreen({Key key}) : super(key: key);

  <strong i="13">@override</strong>
  Widget build(BuildContext context) {
    final filter = useFilter(debounceDuration: const Duration(seconds: 2));
    final userId = useProvider(authProvider).userId;
    final chatId = useProvider(selectedChatProvider);
    final chat = useQuery(ChatQuery(userId: userId, chatId: chatId, filter: filter.value));

    return Column(
      children: [
        Searchbar(onChanged: (value) => filter.value = value),
        Expanded(
          child: ChatList(chat: chat),
        ),
      ],
    );
  }
}

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

Если бы мы проделали то же самое без хуков, у нас было бы:

class ChatScreen extends StatefulWidget {
  const ChatScreen({Key key}) : super(key: key);

  <strong i="20">@override</strong>
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  String filter;
  Timer timer;

  <strong i="21">@override</strong>
  void dispose() {
    timer?.cancel();
    super.dispose();
  }

  <strong i="22">@override</strong>
  Widget build(BuildContext context) {
    return Consumer<Auth>(
      provider: authProvider,
      builder: (context, auth, child) {
        return Consumer<int>(
          provider: selectedChatProvider,
          builder: (context, chatId, child) {
            return GraphQLBuilder<Chat>(
              query: ChatQuery(
                userId: auth.userId,
                chatId: chatId,
                filter: filter.value,
              ),
              builder: (context, chat, child) {
                return Column(
                  children: [
                    Searchbar(
                      onChanged: (value) {
                        timer?.cancel();
                        timer = Timer(const Duration(seconds: 2), () {
                          filter = value;
                        });
                      },
                    ),
                    Expanded(
                      child: ChatList(chat: chat),
                    ),
                  ],
                );
              },
            );
          },
        );
      },
    );
  }
}

Это значительно менее читабельно.

  • У нас есть 10 уровней отступа - 12, если мы сделаем FilterBuilder для повторного использования логики фильтра
  • Логика фильтра в нынешнем виде не подлежит повторному использованию.

    • мы можем по ошибке забыть отменить timer

  • половина метода build бесполезна для читателя. Строители отвлекают нас от главного
  • Я потерял добрых 5 минут, пытаясь понять, почему код не компилируется из-за отсутствия скобок

Как пользователь flutter_hooks я выскажу свое мнение. До использования хуков я был доволен Flutter. Я не видел необходимости в чем-то подобном. Прочитав об этом и посмотрев видео на YouTube, я все еще не был уверен, это выглядело круто, но мне нужна была практика или примеры, чтобы действительно мотивировать это. Но потом я кое-что заметил. Я избегал виджетов с отслеживанием состояния любой ценой, было задействовано просто много шаблонов, и я пропускал класс, пытаясь что-то найти. Из-за этого я переместил большую часть своего эфемерного состояния в решение для управления состоянием вместе с остальной частью состояния приложения и просто использовал виджеты без отслеживания состояния. Однако это приводит к тому, что бизнес-логика быстро зависит от Flutter из-за зависимости от получения Navigator или BuildContext для доступа к InheritedWidget s / Providers выше в дерево. Не говоря уже о том, что это был хороший подход к управлению государством, я знаю, что это не так. Но я сделал все, что мог, чтобы не беспокоиться об управлении состоянием в пользовательском интерфейсе.

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

Для меня это как сборщик мусора для эфемерных состояний / контроллеров. Мне не нужно забывать об удалении всех подписок в пользовательском интерфейсе, хотя я все еще очень хорошо осознаю тот факт, что flutter_hooks делает для меня. Это также значительно упрощает обслуживание и рефакторинг моего кода. Говоря о написании ~ 10 приложений за последний год для моих исследований и развлечения.

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

  1. Иногда я делаю ловушку, чтобы упростить использование пакета, в котором есть контроллеры, которые необходимо инициализировать / удалить. (Например, golden_layout или zefyr ). Я считаю, что другие пользователи, использующие flutter_hooks , выиграют от такого пакета. Однако я не могу оправдать публикацию пакета, который буквально содержит 1-3 функции. Альтернативой было бы создание пакета кухонной раковины, который содержит множество крючков для различных пакетов, которые я использую, затем я могу просто использовать зависимость git, но тогда любой, кто использует эти другие пакеты + flutter_hooks, должен будет зависеть от моего git в чтобы получить выгоду (который менее заметен и, вероятно, содержит зависимости от пакетов, которые им не важны), или от пакета, содержащего 3 функции, или я публикую пакет-садовод в pub.dev. Все идеи кажутся нелепыми и не очень открытыми. Другие пользователи flutter_hooks могут легко скопировать и вставить эти функции в свой код или попытаться выяснить логику самостоятельно, но это полностью упускает из виду идею совместного использования кода / пакетов. Гораздо лучше было бы использовать функции в исходных пакетах, а не в каком-нибудь «пакете расширения». Если flutter_hooks был частью фреймворка или даже просто пакетом, который использовался или экспортировался из фреймворка, например characters , то авторы исходного пакета с большей вероятностью приняли бы запрос на перенос для простого перехвата. функций, и у нас не будет путаницы из 1-3 функциональных пакетов.
    Если flutter_hooks не будет принят Flutter, я предвижу, что кучу из 1–3 пакетов функций загромождают результаты поиска pub.dev. Тот факт, что эти пакеты будут действительно маленькими, заставляет меня согласиться с @rrousselGit, что это примитив. Если 1228 звезд в репозитории flutter_hooks не указывают на то, что он решает проблемы, упомянутые @rrousselGit, я не знаю, что это такое.

  2. Я смотрел видео на YouTube о вкладе в репозиторий Flutter, так как мне было интересно узнать, чем я могу помочь. Пока я смотрел, человек, создающий видео, довольно легко добавил в новое свойство, но почти забыл позаботиться об обновлении dispose , didUpdateWidget и debugFillProperties . Увидев снова все сложности виджета с отслеживанием состояния и то, как легко что-то пропустить, я снова перестал доверять им и не так взволнован тем, что внес свой вклад в основной репозиторий Flutter. Не говоря уже о том, что это полностью меня отпугнуло, я все еще заинтересован в участии, но мне кажется, что я буду создавать шаблонный код, который сложно поддерживать и проверять. Дело не в сложности написания кода, а в сложности чтения кода и проверки того, что вы правильно удалили и позаботились об эфемерном состоянии.

Извините за многословный ответ, однако я время от времени смотрю на эту проблему, и меня несколько сбивает с толку ответ команды Flutter. Похоже, вы не нашли времени, чтобы попробовать приложение в обоих направлениях и лично убедиться в отличии. Я понимаю желание не поддерживать дополнительную зависимость или слишком сильно интегрировать ее во фреймворк. Однако основная часть фреймворка flutter_hook - это всего лишь 500 строк хорошо документированного кода. Опять же, извините, если это не имеет отношения к разговору, и я надеюсь, что не оскорбляю никого за то, что отдал свои 2 цента и высказался. Я не говорил раньше, потому что чувствовал, что @rrousselGit высказывает очень хорошие

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

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

Я хотел бы немного больше терпения с этой проблемой - я не понимаю хуки более глубоко после прочтения этой ветки, кроме того, что это еще один способ привязать время жизни Disposables к состоянию. Я не предпочитаю этот подход стилистически, и я чувствую, что есть что-то фундаментально несовершенное, если позиция такова: «просто найдите время, чтобы написать совершенно новое приложение в этой парадигме, тогда вы поймете, почему его нужно впихнуть в рамки!» ' - как отметил инженер React в этой ветке, это действительно не рекомендуется для Flutter, и преимущества, описанные в этой ветке, невелики по сравнению с затратами на вид перепрограммирования, что означает, что вам нужна совершенно новая кодовая база, чтобы увидеть преимущества.

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

Согласовано. Я благодарен Хикси за то, что нашла время принять участие в этой дискуссии.

я больше не понимаю хуки после прочтения этой ветки

Честно говоря, в этом выпуске явно не говорится о крючках.
Это скорее попытка объяснить проблему, чем ее решение.

Вы чувствуете, что этого не происходит?

Я чувствую здесь обе стороны ( @rrousselGit и @Hixie) и хотел оставить некоторые отзывы о (моей) стороне / перспективе использования фреймворка Flutter.

Подход flutter_hooks действительно значительно сокращает шаблон (только из примеров, показанных здесь, поскольку мы можем повторно использовать такие конфигурации состояния) и снижает сложность, поскольку не нужно активно думать об инициализации / удалении ресурсов. Вообще говоря, он хорошо работает, улучшая и поддерживая поток / скорость разработки ... даже несмотря на то, что он не так хорошо вписывается в «ядро» самого Flutter (субъективно).

Как минимум> 95% кода, который я пишу, приводит к тому, что метод сборки является только декларативным, без локальных переменных или вызовов за пределами возвращаемого поддерева виджета, вся логическая часть находится внутри этих функций состояния для инициализации, назначения и удаления ресурсов и добавления слушатели (в моем случае реакции MobX) и тому подобное. Поскольку это также подход по большей части во Flutter, он кажется очень естественным. Это также дает разработчику возможность всегда открыто и открыто говорить о том, что вы делаете - это заставляет меня всегда преобразовывать такие виджеты в StatefulWidget и писать аналогичный код в initState / dispose, но это также всегда приводит к записи того, что вы собираетесь делать, прямо в используемом виджете. Лично для меня, как и сам @Hixie , это никоим образом не мешает мне писать такой шаблонный код и позволяет мне как разработчику решать, как с ним справиться, вместо того, чтобы полагаться на что-то вроде flutter_hooks сделать это за меня, и в результате я не пойму, почему что-то может вести себя так же. Извлечение виджетов небольшими частями также гарантирует, что этот тип шаблона подходит для того варианта использования, для которого он используется. С flutter_hooks мне все равно нужно было бы подумать о том, какие состояния стоит писать как перехватчик и, следовательно, использовать повторно - разные варианты могут привести либо к различным «одноразовым» перехватчикам, либо к отсутствию перехватчиков вообще, так как я мог бы не использовать слишком часто конфигурации повторно, но, как правило, пишут больше нестандартных.

Не поймите меня неправильно, подход в таких хуках кажется очень приятным и полезным, но мне кажется, что это очень фундаментальная концепция, которая меняет основную концепцию того, как с этим справиться. Это очень хорошо, как сам пакет, чтобы дать разработчикам возможность использовать такой подход, если они не довольны тем, как сделать это «изначально», но сделать его частью самого фреймворка Flutter было бы, по крайней мере, для того, чтобы быть чистым / чистым. унифицированы, в результате либо переписываются большие части Flutter, чтобы использовать эту концепцию (много работы), либо использовать ее для будущих / избранных вещей (что может сбивать с толку из-за таких смешанных подходов).

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

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

Однако упоминаются хуки, поскольку они являются одним решением, и я считаю, что

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

значительно менее читаемый

Кажется, что решение свойства не так легко использовать повторно, или это неправильный вывод, который я сделал (?), Поскольку не было ответа о том, как его использовать, чтобы:

создание вторичного виджета, который повторно использует код, находящийся в данный момент внутри _ExampleState, в другом виджете?
С одной изюминкой: этот новый виджет должен управлять своим идентификатором пользователя внутри своего состояния.

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

Я довольно скептически отношусь к идее создания дизайн-документа на данный момент.
Очевидно, что на данный момент @Hixie против

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

Эта ветка выпуска была очень интересной для чтения, и я рад видеть, что обмен мнениями остался вежливым. Однако я немного удивлен нынешним тупиком.

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

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

Хотя аргумент о том, что хуки доступны как пакет для тех, кто хочет воспользоваться его преимуществами, является обоснованным, я считаю, что он не оптимален для таких примитивов, как хуки. Вот почему.

Очень часто при создании пакетов даже для внутреннего повторного использования мы обсуждаем, должен ли пакет быть «чистым» в том смысле, что он может зависеть только от Dart + Flutter SDK, или если мы разрешаем использование в нем некоторых других пакетов, и если да, то какие единицы. Даже Provider не подходит для «чистых» пакетов, но часто допускается для пакетов более высокого уровня. Для приложения всегда ведутся одни и те же споры о том, какие пакеты подходят, а какие нет. Провайдер зеленый, но что-то вроде хуков по-прежнему остается под вопросом как пакет.

Если бы такие хуки, как решение, были бы частью SDK, было бы очевидным выбором использовать возможности, которые он предлагает. Хотя я хочу использовать хуки и разрешить их уже сейчас как пакет, я также обеспокоен тем, что он создает стиль кода Flutter и вводит концепции, которые могут быть не знакомы разработчикам Flutter, которые его не используют. Если мы пойдем по этому пути без поддержки в SDK, это будет немного похоже на развилку дорог. Для небольших личных проектов легко использовать хуки. Рекомендую попробовать вместе с Riverpod.

(Я полагаю, что наш консерватизм в отношении пакетов возник из-за того, что в прошлом его сжигали пакеты и беспорядок зависимостей от других менеджеров пакетов, вероятно, не единственный.)

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

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

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

Недавно мы добавили в фреймворк RestorableProperties. Было бы интересно посмотреть, можно ли их каким-то образом использовать здесь ...

Я согласен с @Hixie, что в API есть скрытые проблемы, для решения которых требуется анализатор или линтер. Я думаю, что мы, как и все, кто хочет участвовать, должны изучить различные решения, возможно, через предложение проектной документации или иным образом, по проблеме многоразового управления жизненным циклом. В идеале он был бы более специфичным для Flutter и использовал бы API-интерфейсы Flutter, а также решал бы проблемы, которые делает API-интерфейс хуков. Я думаю, что версия Vue - хорошая модель для начала, как я уже упоминал ранее, поскольку она не зависит от порядка вызова ловушки. Кто-нибудь еще заинтересован в расследовании со мной?

@Hixie, но вы согласны с тем, что нет хорошего способа повторно использовать логику состояния компонуемым способом между виджетами? Вот почему вы начали как-то задумываться об использовании ResuableProperties?

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

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

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

Крючки для письма не обременительны.
Это все еще проще, чем решения, доступные сейчас ИМХО (повторюсь снова 😉).
Может, я неверно истолковываю то, что вы пишете. Но я не думаю, что кто-то это сказал?
Я читал это так, как будто люди действительно ценят все преимущества, которые дает решение с крючками, и хотят, чтобы они могли использовать его повсюду. Чтобы воспользоваться всеми преимуществами. Поскольку хуки можно использовать повторно, было бы замечательно, если бы сторонние разработчики могли уверенно писать код и отправлять свои собственные хуки, не требуя, чтобы каждый писал свои собственные оболочки. Воспользуйтесь преимуществами многократного использования логики состояний.
Я думаю, что @rrousselGit и @gaearon уже объяснили эту примитивную вещь. Так что я не буду вдаваться в подробности.
Может быть, я не понимаю этого утверждения, потому что не вижу, что это хорошее резюме того, что люди написали в этой ветке. Мне жаль.

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

Кто решает путь вперед?
Что дальше?

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

В React мы решаем эту проблему с помощью линтера - статического анализа. По нашему опыту, этот недостаток не важен даже для большой кодовой базы. Есть и другие проблемы, которые мы можем считать недостатками, но я просто хотел указать, что, хотя люди интуитивно полагают, что использование постоянного порядка вызовов будет проблемой, на практике баланс оказывается совершенно другим.

Настоящая причина, по которой я пишу этот комментарий, заключается в том, что Flutter использует компилируемый язык. «Линтинг» не является обязательным. Итак, если существует согласование между основным языком и структурой пользовательского интерфейса, определенно можно обеспечить выполнение этой «условной» проблемы, которая никогда не возникает статически. Но это работает только тогда, когда UI-фреймворк может мотивировать изменения языка (например, Compose + Kotlin).

@Hixie, но вы согласны с тем, что нет хорошего способа повторно использовать логику состояния компонуемым способом между виджетами? Вот почему вы начали как-то задумываться об использовании ResuableProperties?

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

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

Я хочу сказать, что хорошее решение здесь не потребует от кого-либо писать обертки.

Что дальше?

Есть много следующих шагов, например:

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

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

Мне бы очень не хотелось видеть тогда, как выглядит нецивилизованная нить. В этой ветке так мало сочувствия, что ее трудно читать и следить со стороны

Я хочу сказать, что хорошее решение здесь не потребует от кого-либо писать обертки.

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

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

Решение повторно используемой составной логики состояния в ядре означало бы, что людям не пришлось бы писать оболочки, поскольку такая повторно используемая логика могла бы безопасно поставляться во всех пакетах без добавления зависимостей.

Я пытаюсь сказать, что, ИМХО, хорошее решение не потребует от кого-то написания этой логики. Просто не было бы лишней логики для повторного использования. Например, глядя на предыдущий пример «fetchUser», никому не нужно было бы писать ловушку или ее эквивалент, чтобы вызвать функцию «fetchUser», вы просто вызываете функцию «fetchUser» напрямую. Точно так же fetchUser не должен ничего знать о хуках (или о том, что мы используем), а хуки (или о том, что мы используем) не нужно ничего знать о fetchUser. И все это при сохранении тривиальной логики написания, как в случае с хуками.

Текущие ограничения вызваны тем фактом, что хуки - это исправление, превышающее языковые ограничения.

В некоторых языках хуки представляют собой языковые конструкции, например:

state count = 0;

return RaisedButton(
  onPressed: () => count++,
  child: Text('clicked $count times'),
)

Это был бы вариант функций async / sync , которые могут сохранять некоторое состояние между вызовами.

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

Я бы добавил, что ограничения хуков аналогичны ограничениям --track-widget-creation.

Этот флаг нарушает канонализацию конструктора const для виджетов. Но это не проблема, поскольку виджеты декларативны.

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

Возможно, пример с fetchuser не идеален.
Но useStream, useAnimstion или useStreamCintroller делают дерево виджетов намного чище и не дают вам забыть об утилизации или позаботиться о dudChangeDependencues.
Следовательно, текущий способ имеет свои ловушки, в которые можно попасться. Так что я предполагаю, что потенциальная проблема с последовательностью вызовов не является более серьезной, чем эти.
Я не уверен, что начал бы писать свои собственные хуки, но было бы неплохо иметь коллекцию часто необходимых, готовых к использованию, внутри фреймворка.
Это был бы альтернативный способ справиться с ними.

@ Хикси , мне очень жаль, что я не могу понять, что вы пытаетесь описать, я виню, что здесь уже поздно вечером, но, вероятно, это только я 😳 .. Но в хорошем решении, которое вы описываете, где бы значения состояния, бизнес-логика состояния и логика событий времени жизни, которые решение проблемы обертывает / инкапсулирует, чтобы их можно было легко компоновать и совместно использовать между виджетами? Не могли бы вы подробнее рассказать о том, что делает хорошее решение и как, по вашему мнению, оно будет работать в идеале?

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

Тем не менее, я думаю, стоит отметить, что эта тема глубоко волнует людей со всех сторон.

  • @rrousselGit уже много лет отвечает на вопросы начинающих об управлении состоянием в StackOverflow и в системе отслеживания проблем package:provider . Я слежу только за последним из этих огненных рукавов и не испытываю ничего, кроме уважения к усердию и терпению Реми.
  • @Hixie и другие в команде Flutter глубоко заботятся об API Flutter, его стабильности, поверхности, ремонтопригодности и удобочитаемости. Благодаря этому опыт разработчиков Flutter стал таким, как сейчас.
  • Разработчики Flutter глубоко заботятся об управлении состоянием, потому что этим они занимаются значительную часть своего времени разработки.

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

Так что я был бы рад, если бы обсуждение продолжилось здесь или в какой-либо другой форме. Если я могу чем-то помочь, например, с официальным документом, просто дайте мне знать.

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

(Отдельно я хочу поблагодарить @gaearon за участие в этом обсуждении. Опыт React в этом отношении бесценен.)

@ emanuel-lundman

Но в хорошем решении, которое вы описываете, где будут располагаться значения состояния, бизнес-логика состояния и логика событий времени жизни, которые решение проблемы обертывает / инкапсулирует, чтобы их можно было легко компоновать и совместно использовать между виджетами? Не могли бы вы подробнее рассказать о том, что делает хорошее решение и как, по вашему мнению, оно будет работать в идеале?

К сожалению, я не могу уточнить, потому что не знаю. :-)

@escamoteur

Возможно, пример с fetchuser не идеален.
Но useStream, useAnimstion или useStreamCintroller делают дерево виджетов намного чище и не дают вам забыть об утилизации или позаботиться о dudChangeDependencues.

Одной из трудностей в этом вопросе было «перемещение стоек», когда проблема описывается, затем, когда она анализируется, проблема отклоняется как не настоящая проблема и описывается новая проблема, и так далее. Что действительно может быть полезно, так это придумать некоторые канонические примеры, например, демонстрационное приложение Flutter, в котором есть реальный пример проблемы, не слишком упрощенный для демонстрации. Затем мы могли бы повторно реализовать это, используя хуки и другие предложения, и действительно оценить их друг относительно друга в конкретных терминах. (Я бы сделал это сам, но я не совсем понимаю, в чем проблема, поэтому, вероятно, лучше, если это сделает кто-то, кто защищает хуки.)

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

Что вы думаете о приведенном здесь примере? https://github.com/flutter/flutter/issues/51752#issuecomment -669626522

Это реальный фрагмент кода.

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

Извините, я имел в виду фрагмент кода, а не приложение.

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

У меня есть несколько примеров на Riverpod, например https://marvel.riverpod.dev/#/, где исходный код находится здесь: https://github.com/rrousselGit/river_pod/tree/master/examples/marvel/lib
Но это не будет сильно отличаться от того, что было упомянуто до сих пор.

@Hixie

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

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

У меня есть много виджетов форм с отслеживанием состояния, некоторые из которых имеют десятки полей формы, и я должен сам управлять текстовыми контроллерами и фокусными узлами. Я создаю и размещаю их в методах жизненного цикла виджета без сохранения состояния. Хотя это довольно утомительно, у меня нет виджета, который использует такое же количество контроллеров / focusNodes или для того же варианта использования. Единственное, что у них общего - это общая концепция состояния и формы. То, что это шаблон, не означает, что код повторяется.
Я имею в виду, что во многих частях моего кода мне приходится перебирать массивы в цикле, я бы не стал называть повторение кода «for (var thing in things)» во всем моем приложении.

Мне нравится мощь StatefulWidget, которая проистекает из простоты API жизненного цикла. Это позволяет мне писать StatefulWidgets, которые делают одно и делают это изолированно от остальной части приложения. «Состояние» моих виджетов всегда принадлежит им самим, поэтому повторное использование моих виджетов не является проблемой, равно как и повторное использование кода.

У меня есть пара проблем с приведенными здесь примерами, которые в некоторой степени соответствуют вашим соображениям:

  • создание нескольких виджетов с сохранением состояния с одной и той же «логикой состояния» кажется просто неправильным и противоречит идее о том, чтобы виджеты были самодостаточными. Но опять же, я не понимаю, что люди подразумевают под общей «логикой состояния».
  • крючки, похоже, не делают того, что я уже не могу сделать, используя простой дротик и базовые концепции программирования (например, функции)
  • проблемы кажутся связанными или вызванными определенным стилем программирования, стилем, который, кажется, отдает предпочтение «многократно используемому глобальному состоянию».
  • абстрагирование от пары строк кода пахнет «преждевременной оптимизацией кода» и добавляет сложности для решения проблемы, которая практически не имеет ничего общего с фреймворком и полностью связана с тем, как люди его используют.

Это значительно менее читабельно.

  • У нас есть 10 уровней отступа - 12, если мы сделаем FilterBuilder для повторного использования логики фильтра
  • Логика фильтра в нынешнем виде не подлежит повторному использованию.

    • мы можем по ошибке забыть отменить timer
  • половина метода build бесполезна для читателя. Строители отвлекают нас от главного
  • Я потерял добрых 5 минут, пытаясь понять, почему код не компилируется из-за отсутствия скобок

Ваш пример - это скорее демонстрация того, насколько подробным является Provider и почему злоупотребление InheritedWidgets для всего - это плохо, а не какая-либо реальная проблема с API Flutter StatelessWidget и State жизненного цикла.

@rrousselGit Извините, если я не

Я даже не понимаю, что такое «логика состояния», которую люди должны использовать повторно.

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

Мы можем технически удалить слой виджетов. В этой ситуации останутся RenderObjects.

Например, у нас может быть минималистичный счетчик:

var counter = 0;

final counterLabel = RenderParagraph(
  TextSpan(text: '$counter'),
  textDirection: TextDirection.ltr,
);

final button = RenderPointerListener(
  onPointerUp: (_) {
    counter++;
    counterLabel.text = TextSpan(text: '$counter');
  },
  child: counterLabel,
);

Это не обязательно сложно. Но это подвержено ошибкам. У нас есть дубликат на рендеринге counterLabel

С виджетами у нас есть:

class _CounterState exends State {
  int counter = 0;

  Widget build(context ) {
    return GestureDetector(
      onTap: () => setState(() => counter++);
      child: Text('$counter'),
    );
  }
}

Единственное, что он сделал, - это факторизовал логику Text , сделав ее декларативной.
Это минималистское изменение. Но для большой базы кода это значительное упрощение.

Крючки делают то же самое.
Но вместо Text вы получаете настраиваемые хуки для логики состояний. Что включает в себя прослушиватели, устранение неполадок, выполнение HTTP-запросов, ...


Ваш пример - это скорее демонстрация того, насколько подробным является Provider и почему злоупотребление InheritedWidgets для всего - это плохо, а не какая-либо реальная проблема с API Flutter StatelessWidget и State жизненного цикла.

Это не связано с провайдером (этот код в конце концов не использует провайдера).
Во всяком случае, у провайдера это лучше, потому что у него context.watch вместо Consumer .

Стандартным решением было бы заменить Consumer на ValueListenableBuilder что приводит к той же проблеме.

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

@satvikpendem
Я был бы готов помочь.
Я думаю, что пример приложения в репозитории flutter_hooks вероятно, демонстрирует несколько разных хуков и то, что они упрощают / решают проблемы, и было бы хорошей отправной точкой.

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

Обновление: код здесь, https://github.com/TimWhiting/local_widget_state_approaches
Я не уверен в правильном имени репозитория, поэтому не думайте, что это проблема, которую мы пытаемся решить. Я сделал базовое приложение счетчика с отслеживанием состояния и хуками. Сегодня вечером у меня не так много времени, но я постараюсь добавить больше вариантов использования, которые более наглядно показывают, в чем может быть проблема. Всем, кто хочет внести свой вклад, запросите доступ.

Конкретные примеры, которые мы здесь обсуждали, такие как пример "fetchUser", всегда заканчивались обсуждением типа "ну, вы могли бы справиться с этим случаем вот так, и это было бы просто и не потребовались бы хуки". на "ну, это упрощенно, в реальном мире вам нужны хуки".

Я не согласен. Не думаю, что я видел такое «вы могли бы справиться с этим случаем вот так» и согласился, что полученный код лучше, чем вариант с перехватом.

Я всегда хотел сказать, что, хотя мы можем делать что-то по-другому, полученный код подвержен ошибкам и / или менее читабелен.
Это относится и к fetchUser тоже

Крючки делают то же самое.
Но вместо Text вы получаете пользовательские хуки для логики состояний. Что включает в себя прослушиватели, устранение неполадок, выполнение HTTP-запросов, ...

Нет, я все еще не понимаю, какой должна быть эта общая логика состояния. Я имею в виду, что у меня есть много виджетов, которые считывают данные из базы данных в своих методах "initState / didUpdateDependency", но я не могу найти два виджета, которые выполняют один и тот же запрос, поэтому их "логика" отличается.

На примере выполнения HTTP-запроса. Предполагая, что у меня есть «makeHTTPRequest (url, paramters)» где-то в моем классе обслуживания, который должны использовать некоторые из моих виджетов, зачем мне использовать ловушку вместо того, чтобы просто вызывать метод, когда он мне нужен? Чем в этом случае использование хуков отличается от обычных вызовов методов?

Слушатели. У меня нет виджетов, которые слушают одно и то же. Каждый из моих виджетов отвечает за то, чтобы подписаться на все, что им нужно, и убедиться, что они отписываются. Хуки могут быть синтаксическим сахаром для большинства вещей, но поскольку мои виджеты не будут прослушивать одну и ту же комбинацию объектов, ловушки должны быть каким-то образом «параметризованы». Опять же, чем хуки отличаются от простой старой функции?


Это не связано с провайдером (этот код в конце концов не использует провайдера).
Во всяком случае, у провайдера это лучше, потому что у него context.watch вместо Consumer .

Хм? Ваш контрпример к тому, что должен решать ваш "ChatScreen" HookWidget, заключался в следующем:

<strong i="19">@override</strong>
  Widget build(BuildContext context) {
    return Consumer<Auth>(
      provider: authProvider,
      builder: (context, auth, child) {
        return Consumer<int>(
          provider: selectedChatProvider,
          builder: (context, chatId, child) {

Как это не связано с провайдером? Я запутался. Я не эксперт в Provider, но это определенно похоже на код, использующий Provider.

Хочу настаивать на том, что речь идет не о сложных состояниях.
Речь идет о крошечных приращениях, которые можно применить ко всей кодовой базе.

Если мы не согласны с ценностью приведенных здесь примеров, приложение не будет ничего вносить в беседу - поскольку мы ничего не можем сделать с хуками, чего мы не можем сделать с StatefulWidget.

Я бы порекомендовал вместо этого сравнить параллельные микро-фрагменты, такие как ImplicitFetcher , и _объективно_ определить, какой код лучше, используя измеримые метрики, и сделать это для большого количества небольших фрагментов.


Как это не связано с провайдером? Я запутался. Я не эксперт в Provider, но это определенно похоже на код, использующий Provider.

Этот код не от Provider, а от другого проекта, который не использует InheritedWidgets.
Consumer провайдера не имеет параметра provider .

И, как я уже упоминал, вы можете заменить Consumer -> ValueListenableBuilder / StreamBuilder / BlocBuilder / Observer / ...

в их методах "initState / didUpdateDependency", но я не могу найти два виджета, которые выполняют один и тот же запрос, поэтому их "логика" отличается.

Логика состояния, которую мы хотим повторно использовать, - это не «сделать запрос», а «сделать что-нибудь при изменении x». «Сделай что-нибудь» может измениться, но «когда х изменится» - обычное дело.

Конкретный пример:
Мы можем захотеть, чтобы виджет выполнял HTTP-запрос всякий раз, когда полученный им идентификатор изменяется.
Мы также хотим отменить ожидающие запросы с помощью CancelableOperation package:async .

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

CancelableOperation<User> pendingUserRequest;

initState() {
  pendingUserRequest = fetchUser(widget.userId);
}

didUpdateWidget(oldWidget) {
  if (widget.userId != oldWidget.userId) {
      pendingUserRequest.cancel();
      pendingUserRequest = fetchUser(widget.userId);
  }
}

dispose() {
  pendingUserRequest.cancel();
}

ПРОТИВ:

CancelableOperation<Message> pendingMessageRequest;

initState() {
  pendingMessageRequest = fetchMessage(widget.messageId);
}

didUpdateWidget(oldWidget) {
  if (widget.userId != oldWidget.messageId) {
      pendingMessageRequest.cancel();
      pendingMessageRequest = fetchMessage(widget.messageId);
      message = pendingMessageRequest.value;
  }
}

dispose() {
  pendingMessageRequest.cancel();
}

Единственная разница в том, что мы заменили fetchUser на fetchMessage . В остальном логика на 100% такая же. Но мы не можем использовать его повторно, что чревато ошибками.

С помощью хуков мы могли бы разложить это на хук useUnaryCancelableOperation .

Это означает, что с теми же двумя виджетами вместо этого будет:

Widget build(context) {
  Future<User> user = useUnaryCancelableOperation(userId, fetchUser);
}

ПРОТИВ

Widget build(context) {
  Future<Message> message = useUnaryCancelableOperation(messageId, fetchMessage);
}

В этом сценарии используется общая логика, связанная с выполнением запроса и его отменой. Остается только значимая разница: fetchUser vs fetchMessage .
Мы могли бы даже сделать пакет из этого useUnaryCancelableOperation , и теперь каждый может повторно использовать его в своем приложении.

Если мы не согласны с ценностью приведенных здесь примеров, приложение не будет ничего вносить в беседу - поскольку мы ничего не можем сделать с хуками, чего мы не можем сделать с StatefulWidget.

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

Единственная разница в том, что мы заменили fetchUser на fetchMessage . В остальном логика на 100% такая же. Но мы не можем использовать его повторно, что чревато ошибками.

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

Опять же, то, что что-то является обычным шаблоном, не означает, что вам нужно создавать для него новую функцию. Кроме того, если вы хотите уменьшить повторяющийся код в этом случае, вы можете просто расширить класс StatefulWidget * и переопределить методы initstate / didUpdateWidget общими битами.

С помощью хуков мы могли бы разложить это на хук useUnaryCancelableOperation .

Это означает, что с теми же двумя виджетами вместо этого будет:

Widget build(context) {
  Future<User> user = useUnaryCancelableOperation(userId, fetchUser);
}

ПРОТИВ

Widget build(context) {
  Future<Message> message = useUnaryCancelableOperation(messageId, fetchMessage);
}

В этом сценарии используется общая логика, связанная с выполнением запроса и его отменой. Остается только значимая разница: fetchUser vs fetchMessage .
Мы могли бы даже сделать пакет из этого useUnaryCancelableOperation , и теперь каждый может повторно использовать его в своем приложении.

Мне очень жаль, но это однозначно отрицательное с моей стороны. Помимо того факта, что он сохраняет лишь незначительную часть избыточности кода, «факторизация» кода, который концептуально принадлежит методам жизненного цикла «initState» и «update», в метод сборки - большой недостаток.

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

Если это действительно так, я думаю, мы должны закрыть эту ошибку

@ Хикси, пожалуйста, не надо. Людей волнует этот вопрос. Я говорил с вами на Reddit о том же , но в контексте SwiftUI: https://github.com/szotp/SwiftUI_vs_Flutter

Дело не в хуках, а в том, чтобы как-то улучшить виджеты с отслеживанием состояния. Люди просто ненавидят писать шаблоны. Для разработчиков, пишущих код SwiftUI, которые привыкли к RAII и копированию семантики представлений, ручное управление одноразовыми предметами кажется просто невозможным.

Поэтому я призываю команду flutter хотя бы рассматривать это как проблему и думать об альтернативных решениях / улучшениях.

Я ожидаю, что мои методы сборки будут строить только макет и ничего больше. Настройка и удаление зависимостей определенно не относится к методу сборки,
Это важный момент. Методы сборки должны быть чистыми. Тем не менее я хотел, чтобы у нас были преимущества без трудностей

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

Проблема, решаемая хуками, проста и очевидна, код остается СУХИМ. Преимущества этого очевидны: меньше кода == меньше ошибок, обслуживание проще, меньше мест для скрытия ошибок, меньшее количество строк в целом повышает читаемость, младшие программисты лучше изолированы и т. Д.

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

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

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

Лично я считаю, что использование виджетов - лучший подход, я редко использую AnimatorController, например, потому что установка / разборка настолько утомительна, многословна и подвержена ошибкам, вместо этого я использую TweenAnimationBuilder везде, где могу. Но этот подход имеет свои ограничения, поскольку вы получаете большее количество объектов с сохранением состояния в данном представлении, вызывая вложенность и многословность там, где на самом деле ничего не требуется.

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

Базовые приложения @escamoteur помогут нам разрабатывать решения, в которых нет проблем.

@esDotDev До сих пор мы обсуждали подобные случаи в этой ошибке, но каждый раз, когда у нас есть решения, отличные от хуков, отклоняются, потому что они не решают какую-то проблему, которая не была включена в пример, который решено решением. Следовательно, простого описания проблем недостаточно, чтобы охватить их в полной мере. Например, случай «12 контроллеров аниматора» может быть решен с помощью набора контроллеров анимации и функциональных возможностей Dart. TweenAnimationBuilder может быть другим решением. Ни то, ни другое не связано с крючками. Но я уверен, что если я предложу это, кто-то укажет на что-то, что я пропустил, и скажет «это не работает, потому что ...» и поднимет эту (новую в контексте примера) проблему. Следовательно, необходимость в базовом приложении, с которым мы все согласны, охватывает весь спектр проблемы.

Если кто-то захочет продвинуть это вперед, я действительно думаю, что лучший способ сделать это - это то, что я описал выше (https://github.com/flutter/flutter/issues/51752#issuecomment-670249755 и https://github.com / flutter / flutter / issues / 51752 # issuecomment-670232842). Это даст нам отправную точку, которая, как мы все можем согласиться, представляет масштаб проблемы, которую мы пытаемся решить; затем мы можем разработать решения, которые решают эти проблемы таким образом, чтобы удовлетворить все пожелания (например, потребность @rrousselGit в повторном использовании, потребность @Rudiksz в чистых методах сборки и т. д.), и, что наиболее важно, мы можем оценить эти решения в контекст базовых приложений.

Я думаю, мы все могли бы довольно легко согласиться с проблемой:
_Нет элегантного способа поделиться задачами установки / демонтажа, связанными с такими вещами, как Streams, AnimatorControllers и т. Д. Это приводит к ненужной многословности, возможностям для ошибок и снижению читабельности.

Кто-нибудь с этим не согласен? Разве мы не можем начать с этого и двигаться дальше в поисках решения? Сначала мы должны согласиться, что это основная проблема, которой, похоже, мы до сих пор не занимаемся.

Однако, когда я пишу это, я понял, что точно соответствует названию проблемы, которая является открытой и оставляет место для обсуждения:
« Повторное использование логики состояний либо слишком многословно, либо слишком сложно »

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

@ Хикси, пожалуйста, не надо. Людей волнует этот вопрос. Я говорил с вами на Reddit о том же , но в контексте SwiftUI: https://github.com/szotp/SwiftUI_vs_Flutter

Ваш пример SwiftUI можно воспроизвести в dart в нескольких строках кода, просто расширив класс StatefulWidget.

У меня есть StatefulWidgets, которые не подписываются ни на какие уведомители и / или не выполняют никаких внешних вызовов, и на самом деле большинство из них такие. У меня около 100 настраиваемых виджетов (хотя и не все с сохранением состояния), и, возможно, 15 из них имеют какую-либо «общую логику состояния», как описано в примерах здесь.

В конечном итоге написание нескольких строк кода (также известного как шаблон) - это небольшой компромисс, позволяющий избежать ненужных накладных расходов. И снова проблема реализации методов initState / didUpdate кажется преувеличенной. Когда я создаю виджет, который использует любой из описанных здесь шаблонов, я, возможно, потрачу первые 5-10 минут на «реализацию» методов lifeCycle, а затем несколько дней на самом деле написание и полировка самого виджета, никогда не касаясь указанных методов жизненного цикла. Количество времени, которое я трачу на написание так называемого стандартного кода установки / удаления, ничтожно по сравнению с кодом моего приложения.

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

Добавление нового типа виджета во Flutter, который является подклассом StatefulWidget (или нет) для этого конкретного варианта использования, было бы хорошо, но давайте не будем встраивать его в сам StatefulWidget. У меня есть много виджетов, которым не нужны накладные расходы, связанные с системой «крючков» или микросостояниями.

@esDotDev Я согласен, что это проблема, с которой сталкиваются некоторые люди; Я даже предлагал некоторые решения ранее в этой проблеме (поиск моих различных версий класса Property , возможно, сейчас похоронен, поскольку GitHub не любит отображать все комментарии). Сложность состоит в том, что эти предложения были отклонены, потому что они не решали конкретных проблем (например, в одном случае не обрабатывалась горячая перезагрузка, в другом - didUpdateWidget). Затем я сделал больше предложений, но затем они были снова отклонены, потому что они не касались чего-то другого (я забыл, что именно). Вот почему важно иметь какую-то базовую линию, которая, как мы согласны, представляет проблему в целом, чтобы мы могли найти решения этой проблемы.

Цель никогда не менялась. Критика состоит в том, что предлагаемые решения не обладают гибкостью. Ни один из них не продолжает работать вне того фрагмента, для которого они были реализованы.

Вот почему в заголовке этого выпуска упоминается «Сложный»: потому что в настоящее время нет гибкости в способах решения проблем.


Другой способ взглянуть на это:

Эта проблема в основном утверждает, что нам нужно реализовать уровень виджетов для логики состояния.
Предлагаемые решения: «Но вы можете сделать это с помощью RenderObjects».

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

Пара гнид с этим утверждением:

  1. На самом деле это не несколько строк, если вы возьмете скобки, межстрочный интервал @overides и т. Д. В acct, вы можете увидеть 10-15 + строк для простого контроллера аниматора. На мой взгляд, это нетривиально ... как нечто большее, чем тривиальное. У меня есть 3 строки, чтобы сделать это (в Unity это Thing.DOTween() ). 15 - это смешно.
  1. Дело не в наборе текста, хотя это и больно. Это о глупости наличия класса 50 строк, где 30 строк - это шаблонный шаблон. Его запутывание. Дело в том, что если вы _не_ пишете шаблон, нет предупреждений или чего-то еще, вы просто добавили ошибку.
  2. Я не вижу никаких накладных расходов, которые стоило бы обсуждать с чем-то вроде Hooks. Мы говорим о массиве объектов с горсткой fxns для каждого. В Dart, что очень быстро. Имо, это отвлекающий маневр.

@esDotDev

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

Расширение виджетов. Подобно тому, как ValueNotifier расширяет ChangeNotifier для упрощения общего шаблона использования, каждый может написать свои собственные разновидности StatelessWidgets для своих конкретных случаев использования.

Да, я согласен, это эффективный подход, но он оставляет желать лучшего. Если у меня есть 1 аниматор, я могу просто использовать TweenAnimationBuilder. Круто, это все еще как 5 строк кода, но неважно. это работает ... не СЛИШКОМ плохо. Но если у меня будет 2 или 3? Теперь я нахожусь в аду вложенности, если по какой-то причине у меня есть cpl другие объекты с состоянием, ну, все это вроде как беспорядок с отступами, или я создаю какой-то очень специфический виджет, который инкапсулирует случайный набор настроек, обновления и разрушения логика.

Расширение виджетов. Подобно тому, как ValueNotifier расширяет ChangeNotifier для упрощения общего шаблона использования, каждый может написать свои собственные разновидности StatelessWidgets для своих конкретных случаев использования.

Вы можете расширять только один базовый класс за раз. Это не масштабируется

Следующая логическая попытка - миксины. Но, как упоминается в OP, они тоже не масштабируются.

@esDotDev

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

Вид виджета, который должен настраивать 3-4 вида AnimationControllers, звучит как очень специфический вариант использования, и поддержка случайного набора логики установки / удаления определенно не должна быть частью фреймворка. Фактически, именно поэтому методы initState / didUpdateWidget представлены в первую очередь, так что вы можете выполнить свой случайный набор настроек по своему желанию.

Мой самый длинный метод initState - это 5 строк кода, мои виджеты не страдают от чрезмерной вложенности, поэтому у нас определенно разные потребности и варианты использования. Или другой стиль разработки.

@esDotDev

3. Я не вижу никаких накладных расходов, которые стоит обсуждать с чем-то вроде Hooks. Мы говорим о массиве объектов с горсткой fxns для каждого. В Dart, что очень быстро. Имо, это отвлекающий маневр.

Если предлагаемое решение похоже на пакет flutter_hooks, это совершенно неверно. Да, концептуально это массив с функциями в нем, но реализация далеко не тривиальна или эффективна.

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

Имеет ли смысл взять один из @brianegan в качестве базового приложения для сравнения?

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

В большинстве случаев, когда вы создаете компоненты / виджеты с состоянием или действиями, которые необходимо выполнить на основе свойств, вы хотите, чтобы что-то произошло при изменении этого состояния / свойств (например, как я видел, упомянутый в этом потоке, вы захотите повторно -получить данные о пользователе при изменении пропа userId). Обычно гораздо естественнее думать об этом как об «эффекте» изменения userId, чем о том, что происходит при изменении всех свойств виджета.

То же самое и с очисткой, обычно гораздо естественнее думать: «Мне нужно очистить это состояние / слушателя / контроллер при изменении этого свойства / состояния», а не «Мне нужно не забыть очистить X, когда все свойства / состояние меняются или когда весь компонент будет уничтожен ".

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

@escamoteur @Rudiksz @Hixie был проект GitHub, созданный @TimWhiting , и меня пригласили туда, где мы начинаем создавать эти примеры. Каждый человек / группа может придумать, как они решат заранее определенную проблему. Это не полноценные приложения, больше похожие на страницы, но мы также можем добавлять приложения, если они служат для демонстрации более сложных примеров. Затем мы сможем обсудить проблемы и создать лучший API. Я полагаю, @TimWhiting может пригласить всех, кого это интересует.

https://github.com/TimWhiting/local_widget_state_approaches

Jetpack Compose также имеет похож на крючки, который по сравнению с реагирующими здесь .

@satvikpendem @TimWhiting Замечательно ! Спасибо.

@esDotDev
очень конкретный вариант использования и поддержка случайного набора логики установки / удаления определенно не должны быть частью фреймворка.

Это гвоздь, которым крючки ударяют по голове. Каждый тип объекта отвечает за свою настройку и демонтаж. Аниматоры умеют создавать, обновлять и уничтожать себя, как потоки и так далее. Хуки специально решают проблему «случайных наборов» строительных лесов состояний, разбросанных по всему вашему просмотру. Это позволяет основной части кода представления сосредоточиться на бизнес-логике и форматировании макета, что является преимуществом. Он не заставляет вас создавать собственные виджеты, просто чтобы скрыть какой-то общий шаблон, который одинаков в каждом проекте.

Мой самый длинный метод initState - это 5 строк кода, мои виджеты не страдают от чрезмерной вложенности, поэтому у нас определенно разные потребности и варианты использования. Или другой стиль разработки.

Мой тоже. Но это initState () + dispose () + didUpdateDependancies (), и отсутствие одного из последних двух может вызвать ошибки.

Я думаю, что канонический пример будет примерно таким: напишите представление, которое использует 1 контроллер потока и 2 контроллера аниматора.

Насколько я понимаю, у вас есть 3 варианта:

  1. Добавьте в класс около 30 строк шаблона и несколько примесей. Это не только многословно, но и довольно сложно усвоить изначально.
  2. Используйте 2 TweenAnimationBuilder и StreamBuilder примерно для 15 уровней отступа, прежде чем вы даже перейдете к своему коду представления, и у вас все еще есть много шаблонов для Stream.
  3. Добавьте около 6 строк кода без отступов в верхней части build (), чтобы получить 3 подобъекта с отслеживанием состояния и определить любой настраиваемый код инициализации / уничтожения.

Может быть, есть четвертый вариант, который представляет собой SingleStreamBuilderDoubleAnimationWidget, но это всего лишь надстройка для разработчиков и в целом довольно раздражает.

Также стоит отметить, что когнитивная нагрузка 3 значительно ниже, чем 2 других для нового разработчика. Большинство новых разработчиков даже не знают о существовании TweenAnimationBuilder, и простое изучение концепции SingleTickerProvider - это самостоятельная задача. Просто сказать: «Дайте мне аниматора, пожалуйста» - это более простой и надежный подход.

Я попробую написать что-нибудь сегодня.

2. Используйте 2 TweenAnimationBuilder и StreamBuilder примерно для 15 уровней отступа, прежде чем вы даже дойдете до своего кода представления, и у вас все еще есть много шаблонов для Stream.

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

Как замена 30 строк кода на 6 строк + сотни строк в библиотеке снижает когнитивную нагрузку? Да, я могу просто игнорировать «магию» библиотеки, но не ее правила. Например, пакет хуков недвусмысленно сообщает вам, что хуки должны использоваться только в методах сборки. Теперь у вас есть дополнительное ограничение, о котором нужно беспокоиться.

У меня, вероятно, менее 200 строк кода, который включает в себя фокусные узлы, текстовые контроллеры, синглетикпровайдеры или различные методы жизненного цикла моих виджетов с отслеживанием состояния, в проекте с 15 тыс. Строк кода. О какой когнитивной перегрузке вы говорите?

@Rudiksz, пожалуйста, перестань быть пассивно-агрессивным.
Мы можем не соглашаться без борьбы.


Ограничения крючков - меньше всего меня беспокоит.

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

Важна проблема, которую мы хотим решить.

Более того, виджеты могут использоваться только внутри сборки и без каких-либо условий (или мы иным образом меняем глубину дерева, что недопустимо)

Это идентично ограничению хуков, но я не думаю, что люди жаловались на это.

Более того, виджеты могут использоваться только внутри сборки и без каких-либо условий (или мы иным образом меняем глубину дерева, что недопустимо)

Это идентично ограничению хуков, но я не думаю, что люди жаловались на это.

Нет, не идентично. Представленная здесь проблема, по-видимому, связана с кодом, который _подготовляет_ виджеты _перед_ построением (пере) построением. Подготовка состояния, зависимостей, потоков, контроллеров и прочего, а также обработка изменений в древовидной структуре. Ничего из этого не должно быть в методе сборки, даже если оно скрыто за одним вызовом функции.
Точка входа для этой логики никогда не должна быть в методе сборки.

Заставлять меня помещать любую логику инициализации в метод сборки - это совсем не то же самое, что «заставлять» меня составлять дерево виджетов в методе сборки. Вся причина метода сборки заключается в том, чтобы взять существующее состояние (набор переменных) и создать дерево виджетов, которое затем раскрашивается.

И наоборот, я также был бы против принуждения меня добавлять код, строящий виджеты внутри методов initState / didUpdateWidget.

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

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

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

Например, он решает:

Consumer(
  provider: provider,
  builder: (context, value) {
    return Consumer(
      provider: provider2,
      builder: (context, value2) {
        return Text('$value $value2');
      },
    );
  },
)

имея:

Consumer(
  builder (context, watch) {
    final value = watch(provider);
    final value2 = watch(provider2);
  },
)

Где watch можно вызвать условно:

Consumer(
  builder: (context, watch) {
    final value = watch(provider);
    if (something) {
      final value2 = watch(provider2);
    }
  },
)

Мы могли бы даже полностью избавиться от Consumer , имея собственный базовый класс StatelessWidget / StatefulWidget :

class Example extends ConsumerStatelessWidget {
  <strong i="21">@override</strong>
  Widget build(ConsumerBuildContext context) {
    final value = context.watch(provider);
    final value2 = context.watch(provider2);
  }
}

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

Так что мы еще далеки от гибкости крючков.

@rrousselGit Я думаю, что без расширения StatelessWidget / StatefulWidget и создания чего-то вроде ConsumerStatelessWidget можно получить что-то вроде context.watch , используя методы расширения в BuildContext class, и поставщик предоставит функцию наблюдения с InheritedWidgets.

Это другая тема. Но tl; dr, мы не можем полагаться на InheritedWidgets как на решение этой проблемы: https://github.com/flutter/flutter/issues/30062

Чтобы решить эту проблему, использование InheritedWidgets заблокировало бы нас из-за https://github.com/flutter/flutter/issues/12992 и https://github.com/flutter/flutter/pull/33213.

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

Сравнивая Flutter и SwiftUI, для меня очевидно, что существует реальная проблема, или, скорее, все не так хорошо, как могло бы быть.

Это может быть трудно увидеть, потому что Flutter и другие много работали над этим: у нас есть оболочки для каждого конкретного случая: AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity и т. Д. StatefulWidget отлично подходит для реализации этих небольших многоразовых утилит, но это слишком низкий уровень для одноразовых, специфичных для домена компонентов, где у вас может быть множество текстовых контроллеров, анимаций или любой другой бизнес-логики. Обычное решение - либо укусить пулю и написать весь этот шаблон, либо создать тщательно построенное дерево провайдеров и слушателей. Ни один из подходов не приносит удовлетворения.

Как говорит @rrousselGit , в былые времена (UIKit) мы были вынуждены вручную управлять нашими UIViews (эквивалентами RenderObjects) и не забывать копировать значения из модели в представление и обратно, удалять неиспользуемые представления, перерабатывать и т. Д. Это не было ракетной наукой, и многие люди не видели эту старую проблему, но я думаю, что все здесь согласятся, что Flutter явно улучшил ситуацию.
С отслеживанием состояния проблема очень похожа: это скучная работа, подверженная ошибкам, которую можно автоматизировать.

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

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

Я сбиваюсь с толку, когда вы говорите, что для создания ваших одноразовых доменных компонентов вам нужен виджет высокого уровня. Обычно бывает с точностью до наоборот.

AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity - все это виджеты, которые реализуют определенный вариант использования. Когда мне нужен виджет с такой специфической логикой, что я не могу использовать ни один из этих виджетов более высокого уровня, я перехожу к api более низкого уровня, чтобы я мог написать свой собственный конкретный вариант использования. Так называемый шаблонный шаблон реализует то, как мой уникальный виджет управляет своей уникальной комбинацией потоков, сетевых вызовов, контроллеров и многого другого.

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

С отслеживанием состояния проблема очень похожа: это скучная работа, подверженная ошибкам, которую можно автоматизировать.

Опять таки. Вы хотите автоматизировать __ "одноразовые, зависящие от домена компоненты, где у вас может быть множество текстовых контроллеров, анимаций или любой другой бизнес-логики, требующей" __ ?! "

Как говорит @rrousselGit , в былые времена (UIKit) мы были вынуждены вручную управлять нашими UIViews (эквивалентами RenderObjects) и не забывать копировать значения из модели в представление и обратно, удалять неиспользуемые представления, перерабатывать и т. Д. Это не было ракетной наукой, и многие люди не видели эту старую проблему, но я думаю, что все здесь согласятся, что Flutter явно улучшил ситуацию.

Я занимался разработкой iOS и Android 6-7 лет назад (примерно в то время, когда Android перешел на их материальный дизайн), и я не помню, чтобы какие-либо из этих проблем с управлением и переработкой представлений были проблемой, а Flutter не кажется лучше или хуже. О текущих делах говорить не могу, ушел, когда запустили Swift и Kotlin.

Шаблон, который я вынужден написать в своих StatefulWidgets, составляет около 1% моей базы кода. Это подвержено ошибкам? Каждая строчка кода - потенциальный источник ошибок, так что будьте уверены. Это громоздко? 200 строк кода или 15000? Я действительно так не думаю, но это только мое мнение. Контроллеры текста / анимации Flutter и focusnodes имеют проблемы, которые можно улучшить, но многословность - не одна из них.

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

Прислушиваться к некоторым комментариям здесь похоже на то, что управлять 5 строками кода вместо 1 примерно в 5 раз сложнее. Это не.

Разве вы не согласны с тем, что вместо настройки initState и dispose для каждого AnimationController, например, может быть больше ошибок, чем просто сделать это один раз и повторно использовать эту логику? Тот же принцип, что и использование функций, возможность повторного использования. Я согласен с тем, что вставлять хуки в функцию сборки проблематично, но определенно есть способ получше.

Похоже, что разница между теми, кто видит и не видит здесь проблемы, заключается в том, что первые ранее использовали конструкции типа хуков, например, в React, Swift и Kotlin, а вторые - нет, например, работая на чистой Java. или Android. Я думаю, что единственный способ убедиться, по моему опыту, - это попробовать хуки и посмотреть, сможете ли вы вернуться к стандартному пути. Часто, опять же, по моему опыту, многие люди не могут. Вы знаете это, когда используете это.

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

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

Я потратил дни на попытки провайдера, еще больше дней на попытки блока, я не нашел ни одного из них хорошим решением. Если это сработает для вас, отлично.

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

Какой бы шаблонный сокращающий код ни был добавлен в фреймворк, я надеюсь, что Stateful / StatelessWidgets останутся неизменными. Я не могу ничего добавить к этому разговору.

Давайте начнем снова, в гипотетическом мире, где мы можем изменить Дарт, не говоря о крючках.

Обсуждаемая проблема:

Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}

Этот код не читается.

Мы могли бы исправить проблему с удобочитаемостью, введя новое ключевое слово, которое изменяет синтаксис на:

Widget build(context) {
  final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);
  final value2 = keyword StreamBuilder(stream: someStream);
  final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

  return Text('$value $value2 $value3');
}

Этот код значительно более читабелен, не связан с хуками и не страдает от его ограничений.
Улучшение читабельности связано не столько с количеством строк, сколько с форматированием и отступами.


Но как быть, если Builder не является корневым виджетом?

Пример:

Widget build(context) {
  return Scaffold(
    body: StreamBuilder(
      stream: stream,
      builder: (context, value) {
        return Consumer<Value2>(
          builder: (context, value2, child) {
            return Text('${value.data} $value2');
          },
        );
      },
    ),
  );
}

У нас могло быть:

Widget build(context) {
  return Scaffold(
    body: {
      final value = keyword StreamBuilder(stream: stream);
      final value2 = keyword Consumer<Value2>();
      return Text('${value.data} $value2');
    }
  );
}

Но как это связано с проблемой повторного использования?

Причина, по которой это связано, заключается в том, что построители технически являются способом повторного использования логики состояния. Но их проблема в том, что они делают код не очень читаемым, если мы планируем использовать _many_ builders, как в этом комментарии https://github.com/flutter/flutter/issues/51752#issuecomment -669626522

С помощью этого нового синтаксиса мы устранили проблему с удобочитаемостью. Таким образом, мы можем извлечь больше вещей в Builders.

Так, например, useFilter упомянутое в этом комментарии, может быть:

FilterBuilder(
  debounceDuration: const Duration(seconds: 2),
  builder: (context, filter) {
    return TextField(onChange: (value) => filter.value = value);
  }
)

Что мы затем можем использовать с новым ключевым словом:

class ChatScreen extends HookWidget {
  const ChatScreen({Key key}) : super(key: key);

  <strong i="15">@override</strong>
  Widget build(BuildContext context) {
    final filter = keyword FilterBuilder(debounceDuration: const Duration(seconds: 2));
    final userId = keyword Consumer(authProvider).userId;
    final chatId = keyword Consumer(selectedChatProvider);
    final chat = keyword QueryBuilder(ChatQuery(userId: userId, chatId: chatId, filter: filter.value));

    return Column(
      children: [
        Searchbar(onChanged: (value) => filter.value = value),
        Expanded(
          child: ChatList(chat: chat),
        ),
      ],
    );
  }
}

А как насчет «извлечения как функции», о котором мы говорили с помощью хуков, для создания пользовательских хуков / построителей?

Мы могли бы сделать то же самое с таким ключевым словом, извлекая комбинацию Builders в функции:

Builder<Chat> ChatBuilder()  {
  final filter = keyword FilterBuilder(debounceDuration: const Duration(seconds: 2));
  final userId = keyword Consumer(authProvider).userId;
  final chatId = keyword Consumer(selectedChatProvider);
  final chat = keyword QueryBuilder(ChatQuery(userId: userId, chatId: chatId, filter: filter.value));

  return Builder(chat);
}

class ChatScreen extends HookWidget {
  const ChatScreen({Key key}) : super(key: key);

  <strong i="21">@override</strong>
  Widget build(BuildContext context) {
    final chat = keyword ChatBuilder();

    return Column(
      children: [
        Searchbar(onChanged: (value) => filter.value = value),
        Expanded(
          child: ChatList(chat: chat),
        ),
      ],
    );
  }
}

Очевидно, не было уделено много внимания всем последствиям такого синтаксиса. Но это основная идея.


Крючки - это особенность.
Ограничения хуков существуют, потому что они реализованы как пакет, а не как языковая функция.

И ключевое слово - use , так что keyword StreamBuilder становится use StreamBuilder , что в конечном итоге реализуется как useStream

Этот код значительно читабельнее

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

Тем не менее, следующим шагом будет работа над приложением @TimWhiting (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/stateful/counter.dart), чтобы превратить его в то, что имеет все проблемы. что мы хотим решить.

Как бы то ни было, https://github.com/flutter/flutter/issues/51752#issuecomment -670959424 в значительной степени соответствует вдохновению для Hooks in React. Шаблон Builder кажется идентичным шаблону Render Props, который раньше был распространен в React (но привел к аналогичным глубоким деревьям). Позже @trueadm предложил синтаксический сахар для Render Props, а позже это привело к Hooks (чтобы удалить ненужные накладные расходы времени выполнения).

`Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}`

Если читабельность и отступы являются проблемой, это можно переписать как

  <strong i="8">@override</strong>
  Widget build(context) {
    return ValueListenableBuilder<String>(
      valueListenable: someValueListenable,
      builder: (context, value, _) => buildStreamBuilder(value),
    );
  }

  StreamBuilder<int> buildStreamBuilder(String value) => StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) => buildTweenAnimationBuilder(value, value2),
      );

  Widget buildTweenAnimationBuilder(String value, AsyncSnapshot<int> value2) =>
      TweenAnimationBuilder<double>(
        duration: Duration(milliseconds: 500),
        tween: Tween(),
        builder: (context, value3, _) => Text('$value $value2 $value3'),
      );

Если функции не для вас или вам нужна возможность повторного использования, извлеките их как виджеты.

class NewWidget extends StatelessWidget {
  var someValueListenable;

  var someStream;

  <strong i="12">@override</strong>
  Widget build(context) {
    return ValueListenableBuilder<String>(
      valueListenable: someValueListenable,
      builder: (context, value, _) {
        return MyStreamedWidget(value, someStream);
      },
    );
  }
}

class MyStreamedWidget extends StatelessWidget {
  const MyStreamedWidget(
    this.value,
    this.someStream, {
    Key key,
  }) : super(key: key);

  final String value;
  final Stream someStream;

  <strong i="13">@override</strong>
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: someStream,
      builder: (context, value2) => MyAnimatedWidget(value, value2),
    );
  }
}

class MyAnimatedWidget extends StatelessWidget {
  final String value;
  final AsyncSnapshot<int> value2;

  const MyAnimatedWidget(
    this.value,
    this.value2, {
    Key key,
  }) : super(key: key);

  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      tween: Tween(),
      builder: (context, value3, _) {
        return Text('$value $value2 $value3');
      },
    );
  }
}

В вашем примере нет ничего, что требовало бы нового ключевого слова или функции.

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

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

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

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

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

Если это вопрос мнения, я бы предположил, что он довольно однобокий в одном направлении.

  1. У обоих есть магия. Я не обязательно знаю, что делают эти строители внутри компании. Немагическая версия записывает фактический шаблон внутри этих построителей. Использование миксина SingleAnimationTIckerProvider - тоже волшебство для 95% разработчиков Flutter.
  2. Один скрывает очень важные имена переменных, которые будут использоваться позже в дереве, а именно value1 и value2 , у другого они расположены спереди и по центру в верхней части сборки. Это явная победа при синтаксическом анализе / обслуживании.
  3. У одного есть 6 уровней отступа еще до начала дерева виджетов, у другого - 0.
  4. Одна - 5 вертикальных линий, другая - 15.
  5. Один показывает фактический фрагмент содержимого на видном месте (Text ()), а другой скрывает его, вложенный глубоко в дерево. Еще одна явная победа в парсинге.

В общем, я мог понять, что это, возможно, дело вкуса. Но у Flutter есть проблема с количеством строк, отступом и отношением сигнал / шум в целом. Хотя я абсолютно _люблю_ возможность декларативно формировать дерево в коде Dart, это может привести к очень нечитабельному / подробному коду, особенно когда вы полагаетесь на несколько слоев обернутых построителей. Таким образом, в контексте самого Flutter, где мы постоянно ведем эту битву, такая оптимизация является отличной особенностью, потому что она предоставляет нам действительно отличный инструмент для борьбы с этой в некоторой степени распространенной проблемой общей многословности.

TL; DR - Все, что значительно сокращает отступы и количество строк во Flutter, является вдвойне ценным из-за, как правило, большого количества строк и отступов, присущих Flutter.

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

За исключением того, что если это изменение в основной структуре, тогда это повлияет на меня, не так ли?

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

Да, эту лошадь уже забили до смерти бесчисленное количество раз, так что я не буду попадаться в ловушку, отвечая на какие-либо комментарии.

Построители - это технически способ повторно использовать логику состояния. Но их проблема в том, что они делают код не очень читабельным.

Это прекрасно об этом говорит. Чтобы думать об этом в терминах Flutter, нам нужны однострочные конструкторы.

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

Мантра «все есть виджет» здесь не очень хорошая вещь. Соответствующий код в построителе обычно - это просто свойства настройки и возвращаемый им объект с отслеживанием состояния, необходимый для сборки fxn. Каждый отдельный разрыв строки и табуляция в основном бессмысленны.

За исключением того, что если это изменение в основной структуре, тогда это повлияет на меня, не так ли?

@Rudiksz Я не думаю, что кто-то предлагает изменить виджеты с Вы всегда можете использовать их в текущем виде, если хотите. Какое бы решение мы ни придумали, мы будем использовать либо виджеты с отслеживанием состояния без изменений, либо виджет другого типа полностью. Мы не говорим, что виджеты с отслеживанием состояния плохие, просто мы хотели бы другой тип виджетов, который позволяет создавать более компонуемое состояние виджета. Думайте об этом как о виджете с отслеживанием состояния, который вместо одного связанного с ним объекта состояния содержит несколько объектов состояния и одну функцию построения, которая является отдельной, но имеет доступ к этим объектам состояния. Таким образом, вы можете повторно использовать биты общего состояния (вместе с логикой состояния, связанной с этим состоянием) с их initState и dispose уже реализованными для вас. По сути, более модульное состояние, которое можно составлять по-разному в разных ситуациях. Опять же, это не конкретное предложение, но, возможно, еще один способ его осмыслить. Возможно, это могло бы превратиться в решение, которое больше похоже на flutter , но я не знаю.

Тем не менее, следующим шагом будет работа над приложением @TimWhiting (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/stateful/counter.dart), чтобы превратить его в то, что имеет все проблемы. что мы хотим решить.

Это сложно, потому что эта проблема по сути одна из смертельных от тысячи порезов. Это просто добавляет раздувания и снижает читабельность всей кодовой базы. Это влияние сильнее всего ощущается в небольших виджетах, где весь класс занимает <100 строк, а половина его тратится на управление контроллером аниматора. Так что я не знаю, что покажет 30 из этих примеров, а что 1 - нет.

На самом деле разница между этим:

<strong i="10">@override</strong>
  Widget build(BuildContext context) {
    final controller = get AnimationController(vsync: this, duration: widget.duration);
    //do stuff
  }

И это:

  AnimationController _controller;

  <strong i="14">@override</strong>
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: widget.duration);
  }

  <strong i="15">@override</strong>
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.duration != oldWidget.duration) {
      _controller.duration = widget.duration;
    }
  }

  <strong i="16">@override</strong>
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  <strong i="17">@override</strong>
  Widget build(BuildContext context) {
    //Do Stuff
  }

Просто нет лучшего способа проиллюстрировать это. Вы можете распространить этот вариант использования на любой объект типа контроллера. AnimatorController, FocusController и TextEditingController, вероятно, являются наиболее распространенными и раздражающими при повседневном использовании. Теперь просто изобразите 50 или 100 из них, разбросанных по всей моей кодовой базе.

  • У вас есть около 1000-2000 строк, которые могут просто исчезнуть.
  • У вас, вероятно, есть десятки ошибок и RTE (на разных этапах разработки), которые никогда не должны были существовать из-за отсутствия какого-либо переопределения.
  • У вас есть виджеты, на которые при взгляде холодным взглядом гораздо труднее разглядеть. Мне нужно прочитать каждое из этих переопределений, я не могу просто предположить, что они являются шаблонными.

И вы, конечно, можете распространить это на настраиваемые контроллеры. Сама концепция использования контроллеров во Flutter менее привлекательна, потому что вы знаете, что вам придется загружать, управлять и уничтожать их вот так, что раздражает и подвержено ошибкам. Это заставляет вас избегать создания собственных и вместо этого создавать собственные StatefulWidgets / Builders. Было бы неплохо, если бы объекты типа контроллера были просто более простыми в использовании и более надежными, поскольку у конструкторов есть проблемы с читабельностью (или, по крайней мере, они значительно более подробны и перегружены пробелами).

Это сложно

Ага, дизайн API - это непросто. Добро пожаловать в мою жизнь.

Так что я не знаю, что покажет 30 из этих примеров, а что 1 - нет.

Это не 30 примеров, которые помогут, это 1 пример, который достаточно сложен, чтобы его нельзя было упростить таким образом, чтобы вы потом сказали «ну, конечно, для этого примера он работает, но не для _реального_ примера».

Это не 30 примеров, которые помогут, это 1 пример, который достаточно сложен, чтобы его нельзя было упростить таким образом, чтобы вы сказали «ну, конечно, для этого примера он работает, но не для реального».

Я уже говорил это несколько раз, но такая оценка крючков несправедлива.
Зацепки не для того, чтобы сделать возможным то, что раньше было невозможным. Речь идет о предоставлении согласованного API для решения такого рода проблем.

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

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

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

Правильный способ судить о решении этой проблемы - это не приложение (поскольку каждое отдельное использование API будет отклонено, как и приведенные здесь примеры).

О предлагаемом решении следует судить:

  • Является ли полученный код объективно лучше синтаксиса по умолчанию?

    • Избегает ли ошибок?

    • Это более читабельно?

    • Легче написать?

  • Насколько можно использовать повторно код?
  • Сколько проблем можно решить с помощью этого API?

    • Теряем ли мы какие-то преимущества из-за каких-то конкретных проблем?

При оценке в этой сетке предложение Property / addDispose может иметь хороший ответ "лучше ли полученный код?" оценка, но плохая оценка как с точки зрения возможности повторного использования, так и с точки зрения гибкости.

Я не знаю, как ответить на эти вопросы, не увидев в действительности каждое предложение.

Почему?

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

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

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

К сожалению, я не разделяю здесь ваши инстинкты (о чем свидетельствует тот факт, что я думал, что Property (https://github.com/flutter/flutter/issues/51752#issuecomment-667737471) отлично работает, пока вы не скажете, что он должен обрабатывать значения как из виджета, так и из локального состояния с одним и тем же API, о котором я не знал, что это ограничение, пока вы не подняли его). Если я предоставлю версию Property, которая также решает эту проблему, все ли будет в порядке, или возникнет какая-то новая проблема, которую она не покрывает? Мы все согласны с тем, что цель - это без цели, я не знаю, для чего мы разрабатываем решения.

Мы можем взять любой существующий * Builder и попробовать заново реализовать его, используя предложенное решение.

Ясно, что не _any_. Например, вы указали один в OP, и когда я сделал свое первое предложение по недвижимости (https://github.com/flutter/flutter/issues/51752#issuecomment-664787791), вы указали на проблемы с ним, которые не были проиллюстрированы оригинальный Builder.

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

Я не против, с чего мы начнем. Если у вас есть особенно хороший пример, который, по вашему мнению, иллюстрирует проблему, и в нем используются хуки, тогда отлично, давайте добавим его в репозиторий @TimWhiting . Все дело в том, чтобы реализовать одно и то же разными способами, я не возражаю, откуда берутся идеи.

Это не 30 примеров, которые помогут, это 1 пример, который достаточно сложен, чтобы его нельзя было упростить таким образом, чтобы вы потом сказали «ну, конечно, для этого примера он работает, но не для _реального_ примера».

Никогда не будет ничего более продуманного, чем простое желание использовать AnimatorController (или любой другой повторно используемый компонент с отслеживанием состояния, о котором вы только можете подумать) без нечитаемого конструктора или набора шаблонов жизненного цикла, подверженных ошибкам.

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

Я настаиваю на том, что подойдет _any_ Builder, поскольку эту проблему можно переименовать в «Нам нужен синтаксический сахар для Builders» и привести к тому же обсуждению.

Все остальные аргументы (например, создание и удаление AnimationController ) основаны на том, что они также могут быть извлечены в построитель:

Widget build(context) {
  return AnimationControllerBuilder(
    duration: Duration(seconds: 2),
    builder: (context, animationController) {

    }
  );
}

В конце концов, я думаю, что идеальный пример - это попытка полностью переопределить StreamBuilder и протестировать его в разных сценариях:

  • поток исходит из Widget
  • // из InheritedWidget
  • от местного государства

и протестируйте каждый отдельный случай на предмет того, что "поток может меняться со временем", поэтому:

  • didUpdateWidget с новым потоком
  • обновлен InheritedWidget
  • мы вызвали setState

В настоящее время эта проблема не решается с помощью Property или onDispose

@esDotDev Можете ли вы перечислить «требуемые преимущества удобочитаемости и надежности»? Если кто-то делает предложение, которое обрабатывает AnimationController с такими преимуществами удобочитаемости и надежности, то на этом мы закончили?

@rrousselGit Я не

Но если бы кто-то создал решение, которое делало бы все, что делает StreamBuilder, но без отступов, разве это было бы? Вы были бы счастливы?

Скорее всего да

Конечно, нам нужно сравнить это решение с другими решениями. Но это достигнет приемлемого уровня.

@esDotDev Можете ли вы перечислить «требуемые преимущества удобочитаемости и надежности»?

Надежность в том, что он может полностью инкапсулировать шаблон вокруг зависимостей и жизненного цикла. т.е. Мне не нужно каждый раз сообщать fetchUser, что он, вероятно, должен перестраиваться при изменении идентификатора, он знает, как это делать внутри. Не нужно указывать Animation, что нужно уничтожать себя каждый раз, когда удаляется родительский виджет и т. Д. (Я не совсем понимаю, может ли Property делать это tbh). Это мешает разработчикам делать ошибки из-за механических задач по всей базе кода (одно из основных преимуществ использования Builders в настоящее время, imo)

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

@esDotDev Если кто-то делает предложение, которое обрабатывает AnimationController с такими преимуществами удобочитаемости и надежности, то на этом мы закончили?

Если вы имеете в виду именно AnimationController нет. Если вы имеете в виду любой объект типа AnimationController / FocusController / TextEdiitingController, тогда да.

Наличие функционально-подобного API, возвращающего значение с неясным определенным временем жизни, является

Я думаю, что это ключевое недоразумение. Время жизни крючка ясно, потому что они по определению являются подсостояниями. Они _всегда_ существуют на протяжении всей жизни Государства, которое их «использует». На самом деле это не могло быть более ясным. Синтаксис может показаться странным и незнакомым, но, безусловно, ему не хватает ясности.

Подобно тому, как ясно время жизни TweenAnimationBuilder (). Он уйдет, когда уйдет его родитель. Как и дочерний виджет, это дочерние состояния. Они являются полностью независимыми государственными «компонентами», мы можем легко собирать и повторно использовать их, и мы явно не управляем их сроком службы, потому что мы хотим, чтобы он был естественным образом привязан к состоянию, в котором он объявлен, а это функция, а не ошибка.

@esDotDev

так далее

Можете быть более конкретными? (Вот почему я предложил создать демонстрационное приложение, охватывающее все основы. Я по-прежнему считаю, что это лучший способ сделать это.) Существуют ли функции, которые имеют значение, кроме вызова инициализатора, если конфигурация не изменилась, и автоматического удаления выделенные ресурсы при удалении хост-элемента?

TextEdiitingController-подобный объект

Можете быть более конкретными? TextEditingController в чем-то более сложен, чем AnimationController?


@rrousselGit

Но если бы кто-то создал решение, которое делало бы все, что делает StreamBuilder, но без отступов, разве это было бы? Вы были бы счастливы?

Скорее всего да

Вот решение, которое делает все, что делает StreamBuilder, без каких-либо отступов:

Widget build(context) {
  var result = Text('result:');
  var builder1 = (BuildContext context, AsyncSnapshot<int> snapshot) {
    return Row(children: [result, Text(snapshot.data)]);
  };
  result = StreamBuilder(stream: _stream1, builder: builder1);
  var builder2 = (BuildContext context, AsyncSnapshot<int> snapshot) {
    return Column(children: [result, Text(snapshot.data)]);
  };
  result = StreamBuilder(stream: _stream2, builder: builder2);
}

Я предполагаю, что это нарушает какое-то другое ограничение. Вот почему я бы предпочел, чтобы мы все согласились с полным описанием проблемы, прежде чем мы попытаемся ее решить.

Это просто те же самые ограничения, которые есть у конструкторов @Hixie, и никто не

Можете быть более конкретными? TextEditingController в чем-то более сложен, чем AnimationController?

Нет, но он может делать разные вещи в init / dispose или привязываться к другим свойствам, и я хотел бы инкапсулировать этот конкретный шаблон.

@esDotDev Значит, вам нужно то же самое, что и конструктор, но без отступов и в одной строке (за вычетом самого обратного вызова построителя, предположительно)? Пример, который я только что опубликовал (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483), делает то же самое со строителями сегодня, поэтому, по-видимому, есть дополнительные ограничения помимо этого?

(FWIW, я не думаю, что построители или что-то вроде построителей, но в одной строке, являются хорошим решением, потому что они требуют, чтобы для каждого типа данных был создан собственный построитель; нет хорошего способа просто создать его на лету .)

(FWIW, я не думаю, что построители или что-то вроде построителей, но в одной строке, являются хорошим решением, потому что они требуют, чтобы для каждого типа данных был создан собственный построитель; нет хорошего способа просто создать его на лету .)

Я не понимаю, что это значит. Не могли бы вы это перефразировать? 🙏

Вам необходимо создать AnimationBuilder для анимаций и StreamBuilder для потоков и так далее. Вместо того, чтобы просто иметь один Builder и говорить «вот как вы получаете новый, вот как вы его удаляете, вот как вы получаете данные» и т. Д. При создании своего StatefulWidget.

Я предполагаю, что это нарушает какое-то другое ограничение. Вот почему я бы предпочел, чтобы мы все согласились с полным описанием проблемы, прежде чем мы попытаемся ее решить.

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

Я думаю, что запрашивается что-то вроде (потерпите меня, я не использую Streams):

Widget build(context) {
   var snapshot1 = get AsyncSnapshot<int>(stream1);
   var snapshot2 = get AsyncSnapshot<int>(stream2);
   return Column(children: [Text(snapshot1.data), Text(snapshot2.data)]);
}

Это весь код. Больше ничего не будет, так как Stream создан для нас, поток предназначен для нас, мы не можем прострелить себе ногу, и код стал намного более читаемым.

Вам необходимо создать AnimationBuilder для анимаций и StreamBuilder для потоков и так далее.

Я не считаю это проблемой. У нас уже есть RestorableInt против RestorableString против RestorableDouble

И дженерики могут решить эту проблему:

GenericBuilder<Stream<int>>(
  create: (ref) {
    var controller = StreamController<int>();
    ref.onDispose(controller.close);
    return controller.stream;
  }
  builder: (context, Stream<int> stream) {

  }
)

Точно так же Flutter или Dart могут включать интерфейс Disposable если это действительно проблема.

@esDotDev

Я думаю, что запрашивается что-то вроде:

Это нарушило бы некоторые из довольно разумных ограничений, перечисленных другими (например, @Rudiksz), а именно гарантию того, что код инициализации никогда не произойдет во время вызова метода сборки.

@rrousselGit

Я не считаю это проблемой. У нас уже есть RestorableInt против RestorableString против RestorableDouble

И у нас есть AnimationBuilder, StreamBuilder и так далее, да. В обоих случаях это прискорбно.

GenericBuilder

Это похоже на то, что я предложил для Property, но если я понял ваши опасения по этому поводу, вы посчитали, что это слишком многословно.

Ранее вы сказали, что если бы кто-то создал решение, которое делает все, что делает StreamBuilder, но без отступов, вы, вероятно, будете счастливы. Вы не прокомментировали мою попытку сделать это (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483). Довольны ли вы этим решением?

@esDotDev

Я думаю, что запрашивается что-то вроде:

Это нарушило бы некоторые из довольно разумных ограничений, перечисленных другими (например, @Rudiksz), а именно гарантию того, что код инициализации никогда не произойдет во время вызова метода сборки.

Не важно, чтобы этот код был в сборке. Важная часть состоит в том, что

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

Это было бы потрясающе:

AsyncSnapshot<int> snapshot1 = createLifecycleState(widget.stream1);
AsyncSnapshot<int> snapshot2 = createLifecycleState(widget.stream2);
AniamtorController animator = createLifecycleState(duration: Duration(seconds: 1), (a)=>a.forward());

Widget build(context) {
   return Opacity(opacity: animator.value, child: Column(children: [Text(snapshot1.data), Text(snapshot2.data)]));
}

Или не так кратко, но все же более читабельно, чем использование построителей, и менее многословно и подвержено ошибкам, чем при прямом выполнении:

AsyncSnapshot<int> stream1;
AsyncSnapshot<int> stream2;
<strong i="18">@override</strong> 
void initState(){
    snapshot1 = createLifecycleState(widget.stream1);
    snapshot2 = createLifecycleState(widget.stream2);
   super.initState();
}

Widget build(context) {
   return Column(children: [Text(snapshot1.data), Text(snapshot2.data)]);
}

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

Я даже сделал сетку для оценки решения https://github.com/flutter/flutter/issues/51752#issuecomment -671000137 и тестовый пример https://github.com/flutter/flutter/issues/51752#issuecomment - 671002248


Ранее вы сказали, что если бы кто-то создал решение, которое делает все, что делает StreamBuilder, но без отступов, вы, вероятно, будете счастливы. Вы не прокомментировали мою попытку сделать это (# 51752 (комментарий)). Довольны ли вы этим решением?

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

Оценка его согласно https://github.com/flutter/flutter/issues/51752#issuecomment -671000137 дает:

  • Является ли полученный код объективно лучше синтаксиса по умолчанию?

    • Избегает ли ошибок?

      _Синтаксис по умолчанию (StreamBuilder без взлома) менее подвержен ошибкам. Это решение не позволяет избежать ошибок, оно создает некоторые _ ❌

    • Это более читабельно?

      _Это явно не читабельно _ ❌

    • Легче написать?

      _Нелегко писать _ ❌

  • Насколько можно использовать повторно код?
    _StreamBuilder не привязан к виджетам / состоянию / жизненным циклам, поэтому этот проход _ ✅
  • Сколько проблем можно решить с помощью этого API?
    _Мы можем создавать собственные конструкторы и использовать этот шаблон. Итак, это pass_. ✅
  • Теряем ли мы какие-то преимущества из-за каких-то конкретных проблем?
    _Нет, синтаксис относительно согласован_. ✅
  1. Эта функция IMO может распространяться на все виджеты конструктора, включая LayoutBuilder, например.
  2. Должен быть способ отключить прослушивание, чтобы вы могли создавать контроллеры 10x и передавать их листам для восстановления, или flutter должен каким-то образом знать, какая часть дерева использует значение, полученное построителем.
  3. Использование этого не должно быть более подробным, чем хуки.
  4. Компилятор должен быть расширен, чтобы справиться с этим должным образом.
  5. Нужны помощники по отладке. Допустим, вы установили точки останова в один из своих виджетов, который использует это. При достижении точки останова внутри метода сборки, поскольку сработал один из построителей, IDE может отображать дополнительную информацию для каждого использованного построителя:
Widget build(context) {
   // this builder is not highlighted, but you can hover over it to see how often it rebuilds, how heavy were those rebuilds, and when was the last rebuild
   var snapshot1 = keyword StreamBuilder(stream1);
   // this builder will be highlighted because it triggered the rebuild
   var constraints = keyword LayoutBuilder(); 

   // <-- I had a breakpoint here and parent constraint changed, breakpoints got reached.
   return Column(children: [Text(snapshot1.data), Text(snapshot2.data)]);
}

Также @Hixie

Это нарушило бы некоторые из довольно разумных ограничений, перечисленных другими (например, @Rudiksz), а именно гарантию того, что код инициализации никогда не произойдет во время вызова метода сборки.

Мы уже косвенно делаем это, используя * Builders. Нам просто нужен синтаксический сахар, чтобы избавиться от них. Я думаю, это очень похоже на async / await и Futures.

@esDotDev То, что вы описываете, очень похоже на то, что я предлагал ранее для Property (см., например, https://github.com/flutter/flutter/issues/51752#issuecomment-664787791 или https://github.com/flutter/flutter/ issues / 51752 # issuecomment-667737471). Есть ли что-то, что препятствует созданию такого рода решений в виде пакета? То есть, какое изменение нужно было бы внести в базовую структуру, чтобы вы могли использовать эту функцию?

@rrousselGit Тогда, как и в случае с Шоном, я бы спросил вас о том же. Если единственное различие между текущей функцией StreamBuilder и той, которая могла бы удовлетворить ваши потребности, заключается в другом синтаксисе, что вам требуется от основного синтаксиса, чтобы вы могли использовать такую ​​функцию? Разве недостаточно просто создать синтаксис, который вы предпочитаете, и использовать его?

Проблема, с которой я столкнулся с вашей сеткой, заключается в том, что если бы я применил ее к решениям до сих пор, я бы получил это, что, как я полагаю, сильно отличается от того, что вы получили бы:

StatefulWidget

  • Является ли полученный код объективно лучше синтаксиса по умолчанию?

    • Избегает ли ошибок?

      _Это то же самое, что и синтаксис по умолчанию, который не особенно подвержен ошибкам. _ 🔷

    • Это более читабельно?

      _Это то же самое, поэтому он одинаково читаем, что разумно читается. _ 🔷

    • Легче написать?

      _Это то же самое, поэтому писать так же легко, что довольно легко. _ 🔷

  • Насколько можно использовать повторно код?
    _Кода для повторного использования очень мало. _ ✅
  • Сколько проблем можно решить с помощью этого API?
    _Это базовый уровень._ 🔷
  • Теряем ли мы какие-то преимущества из-за каких-то конкретных проблем?
    _Не похоже. _ ✅

Варианты собственности

  • Является ли полученный код объективно лучше синтаксиса по умолчанию?

    • Избегает ли ошибок?

      _Это перемещает код в другое место, но не особо снижает количество ошибок. _ 🔷

    • Это более читабельно?

      _Он помещает код инициализации, код очистки и другой код жизненного цикла в одно и то же место, поэтому он менее понятен. _ ❌

    • Легче написать?

      _ Он смешивает код инициализации, код очистки и другой код жизненного цикла, поэтому его труднее писать. _ ❌

  • Насколько можно использовать повторно код?
    _Точно так же многоразово, как StatefulWidget, только в разных местах. _ ✅
  • Сколько проблем можно решить с помощью этого API?
    _Это синтаксический сахар для StatefulWidget, так что никакой разницы. _ 🔷
  • Теряем ли мы какие-то преимущества из-за каких-то конкретных проблем?
    _Производительность и использование памяти немного пострадают. _ ❌

Вариации на тему строителей

  • Является ли полученный код объективно лучше синтаксиса по умолчанию?

    • Избегает ли ошибок?

      _Это, в основном, решение StatefulWidget, но исключенное; ошибки должны быть примерно равнозначными. _ 🔷

    • Это более читабельно?

      _Методы сборки более сложные, остальная логика перемещается в другой виджет, так что примерно то же самое. _ 🔷

    • Легче написать?

      _ Сложнее писать в первый раз (создание виджета-конструктора), чуть проще после этого, так что примерно то же самое. _ 🔷

  • Насколько можно использовать повторно код?
    _Точно так же многоразово, как StatefulWidget, только в разных местах. _ ✅
  • Сколько проблем можно решить с помощью этого API?
    _Это синтаксический сахар для StatefulWidget, поэтому в основном никакой разницы. В некоторых аспектах это действительно лучше, например, это уменьшает объем кода, который должен выполняться при обработке изменения зависимости. _ ✅
  • Теряем ли мы какие-то преимущества из-за каких-то конкретных проблем?
    _Не похоже. _ ✅

Крючковые решения

  • Является ли полученный код объективно лучше синтаксиса по умолчанию?

    • Избегает ли ошибок?

      _Поощряет неправильные шаблоны (например, построение в методе сборки), рискует ошибками при случайном использовании с условными выражениями. _ ❌

    • Это более читабельно?

      _ Увеличивает количество концепций, которые необходимо знать, чтобы понять метод сборки. _ ❌

    • Легче написать?

      _Разработчик должен научиться писать хуки, а это новая концепция, поэтому сложнее. _ ❌

  • Насколько можно использовать повторно код?
    _Точно так же многоразово, как StatefulWidget, только в разных местах. _ ✅
  • Сколько проблем можно решить с помощью этого API?
    _Это синтаксический сахар для StatefulWidget, так что никакой разницы. _ 🔷
  • Теряем ли мы какие-то преимущества из-за каких-то конкретных проблем?
    _Снижается производительность и использование памяти. _ ❌

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

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

@szotp

  1. Эта функция IMO может распространяться на все виджеты конструктора, включая LayoutBuilder, например.

LayoutBuilder сильно отличается от виджета FWIW большинства конструкторов. Ни одно из обсуждаемых до сих пор предложений не будет работать для проблем, подобных LayoutBuilder, и ни одно из требований, описанных перед вашим комментарием, не включает LayoutBuilder. Если мы также должны использовать эту новую функцию для обработки LayoutBuilder, это важно знать; Я рекомендую работать с @TimWhiting, чтобы убедиться, что пример приложения, на котором мы собираемся основывать предложения, включает в себя построители макетов в качестве примера.

  1. Должен быть способ отключить прослушивание, чтобы вы могли создавать контроллеры 10x и передавать их листам для восстановления, или flutter должен каким-то образом знать, какая часть дерева использует значение, полученное построителем.

Я не совсем понимаю, что это значит. Насколько я могу судить, вы можете сделать это сегодня с помощью слушателей и построителей (например, я использую ValueListenableBuilder в приложении, которое я цитировал ранее, чтобы сделать именно это).

Это нарушило бы некоторые из довольно разумных ограничений, перечисленных другими (например, @Rudiksz), а именно гарантию того, что код инициализации никогда не произойдет во время вызова метода сборки.

Мы уже косвенно делаем это, используя * Builders.

Я не думаю, что это правильно. Это зависит от построителя, но некоторые очень усердно работают над разделением initState, didChangeDependencies, didUpdateWidget и логики сборки, так что для запуска каждой сборки в зависимости от того, что было изменено, требуется только минимальный объем кода. Например, ValueListenableBuilder регистрирует слушателей только при первом создании, его построитель может повторно запускаться без повторного запуска родительского элемента или initState. Хуки этого не делают.

@esDotDev То, что вы описываете, очень похоже на то, что я предлагал ранее для Property (см., например, # 51752 (комментарий) или # 51752 (комментарий) ).

Если я правильно понимаю, мы могли бы создать UserProperty который автоматически обрабатывает DidDependancyChange для UserId, или AnimationProperty , или любое другое свойство, которое нам нужно для обработки инициализации / обновления / удаления для этого типа? Тогда мне это кажется приятным. Можно быстро создать наиболее распространенные варианты использования.

Единственное, что меня сбивает с толку, - это будущий строитель. Но я думаю, это просто из-за выбранного вами примера?

Например, могу ли я это создать?

class _ExampleState extends State<Example> with PropertyManager {
  AnimationProperty animProp1;
  AnimationProperty animProp2;

  <strong i="15">@override</strong>
  void initProperties() {
    super.initProperties();
    anim1= register(anim1, AnimationProperty (
      AnimationController(duration: Duration(seconds: 1)),
      initState: (animator) => animator.forward()
      // Not dealing with updates or dispose here, cause AnimationProperty handles it
    ));
   anim2 = register(anim2, AnimationProperty(
       AnimationController(duration: Duration(seconds: 2))..forward(),
   ));
  }

  <strong i="16">@override</strong>
  Widget build(BuildContext context) {
    return Column(children: [
       FadeTransition(opacity: anim1, child: ...),
       FadeTransition(opacity: anim2, child: ...),
   ])
  }
}

Если так, то это полностью LGTM! Что касается добавления к фреймворку, это вопрос о том, следует ли продвигать его до первоклассного синтаксического подхода (что означает, что он становится общей практикой через год или около того), или существует ли он как плагин, который некий однозначный процент разработчиков использовать.

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

Imo, было бы неплохо, если бы разработчик мог использовать аниматоры с надлежащими didUpdate и dispose и debugFillProperties и всем остальным, никогда не задумываясь об этом дважды (точно так же, как мы делаем, когда сейчас используем TweenAnimationBuilder, что является основной причиной, по которой мы рекомендуем все наши разработчики по умолчанию используют его вместо ручного управления аниматорами).

Если так, то это полностью LGTM! Что касается добавления к фреймворку, это вопрос о том, следует ли продвигать его до первоклассного синтаксического подхода (что означает, что он становится общей практикой через год или около того), или существует ли он как плагин, который некий однозначный процент разработчиков использовать.

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

Единственное, что меня сбивает с толку, - это будущий строитель. Но я думаю, это просто из-за выбранного вами примера?

Я пытался обратиться к примеру, который дал

Например, могу ли я это создать?

Вы бы хотели переместить конструктор AnimationController в вызываемое замыкание, а не вызывать его каждый раз, поскольку initProperties вызывается во время горячей перезагрузки для получения новых замыканий, но обычно вы не хотите создавать новый контроллер во время горячей перезагрузки (например, он сбрасывает анимацию). Но да, в остальном вроде нормально. Вы даже можете создать AnimationControllerProperty который принимает аргументы конструктора AnimationController и делает с ними правильные вещи (например, обновляет продолжительность при горячей перезагрузке, если она изменилась).

Imo, было бы неплохо, если бы разработчик мог использовать аниматоры с надлежащими didUpdate и dispose и debugFillProperties и всем остальным, никогда не задумываясь об этом дважды (точно так же, как мы делаем, когда сейчас используем TweenAnimationBuilder, что является основной причиной, по которой мы рекомендуем все наши разработчики по умолчанию используют его вместо ручного управления аниматорами).

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

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

Спасибо, @Hixie , это действительно здорово, и я думаю, что проблема решена достаточно хорошо.

Я не предлагаю разработчикам никогда не думать об этих вещах, но я думаю, что в 99% случаев эти вещи почти всегда привязаны к StatefulWidget, в котором они используются, и выполнение чего-либо другого, кроме этого, уже приводит вас на землю промежуточного разработчика.

Опять же, я не понимаю, чем это принципиально отличается от рекомендации TweenAnimationBuilder вместо необработанного AnimatorController. Это в основном идея, что ЕСЛИ вы хотите, чтобы состояние полностью содержалось / управлялось в этом другом состоянии (а это обычно то, что вы хотите), тогда сделайте это таким образом, чтобы он был проще и надежнее.

На этом этапе мы должны организовать звонок и обсудить его вместе с различными заинтересованными сторонами.
Потому что это обсуждение не продвигается, поскольку мы снова и снова отвечаем на один и тот же вопрос.

Я не понимаю, как после такого долгого обсуждения с таким количеством аргументов мы все еще можем утверждать, что Builders не избегают ошибок по сравнению с StatefulWidget или что хуки не более пригодны для повторного использования, чем необработанные StatefulWidgets.

Это особенно неприятно спорить, учитывая, что все основные декларативные фреймворки (React, Vue, Swift UI, Jetpack Compose) так или иначе позволяют решить эту проблему.
Кажется, только Flutter отказывается рассматривать эту проблему.

@esDotDev Основная причина, по которой ИМХО использовать AnimationBuilder, TweenAnimationBuilder или ValueListenableBuilder, заключается в том, что они перестраивают только при изменении значения без перестройки остальной части их хост-виджета . Это дело производительности. На самом деле речь не идет о многословности или повторном использовании кода. Я имею в виду, что их можно использовать и по этим причинам, если вы считаете их полезными по этим причинам, но это не основной вариант использования, по крайней мере, для меня. Это также то, что Property (или Hooks) не дает вам. С ними вы в конечном итоге перестраиваете виджет _entire_, когда что-то меняется, что не очень хорошо сказывается на производительности.

@rrousselGit

Кажется, только Flutter отказывается рассматривать эту проблему.

В эти выходные я потратил буквально часы своего времени, не говоря уже о том, что до этого много часов провел Google, рассматривая эту проблему, описывая возможные решения и пытаясь понять, что именно мы пытаемся решить. Пожалуйста, не путайте непонимание с тем, что является проблемой, с отказом рассматривать это. Особенно, когда я уже описал лучшее, что можно сделать для продвижения вперед (создание демонстрационного приложения со всей логикой состояния, которая «слишком подробна или слишком сложна», цитируя название проблемы, для повторного использования), что другие, занимающиеся этой ошибкой, взяли на себя задачу, в которой вы отказались участвовать.

The main reason IMHO to use an AnimationBuilder or TweenAnimationBuilder or ValueListenableBuilder is that they rebuild only when the value changes, without rebuilding the rest of their host widget. It's a performance thing.

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

По нашему опыту, стоимость перерисовки пикселей, которая, кажется, происходит со всем деревом, если вы не намерены определять свои RepaintBoundaries, является гораздо более важным фактором реальной производительности, чем затраты на частичную компоновку виджета. Особенно, когда вы попадаете в диапазон мониторов 4k.

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

Часто мы этого не делаем, и в этом случае Builder просто добавляет беспорядок, но мы его принимаем, потому что альтернатива - это просто другой тип беспорядка, и, по крайней мере, с Builder все более или менее гарантировано без ошибок. В случаях, когда перестраивается все представление или перестраивается представление вовсе не обязательно (TextEditingController, FocusController) с использованием построителя, не имеет особого смысла, и во всех случаях прокрутка шаблона вручную принципиально НЕ СУХОЙ.

Это, безусловно, очень зависит от ситуации, поскольку часто возникают проблемы с производительностью. Я думаю, что людям имеет смысл использовать что-то вроде Hooks или Property, если им нравится этот стиль. Это возможно сегодня и, похоже, не требует ничего лишнего от фреймворка (и, как показывает Property, для этого действительно не требуется много кода).

Нет, но это немного похоже на просьбу сообщества создать TweenAnimationBuilder и ValueListenableBuilder, а не предоставлять им StatefulWidget для развития.

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

StatefulWidget - это масса кода по сравнению с Property, и она нетривиальна (в отличие от Property, которая в основном является связующим кодом). Тем не менее, если свойство - это то, что имеет смысл широко использовать повторно (в отличие от создания индивидуальных версий для каждого приложения или команды на основе их конкретных потребностей), то я бы посоветовал тому, кто выступает за его использование, создать пакет и загрузить его в pub . То же самое и с крючками. Если это то, что нравится сообществу, то оно найдет широкое применение, как и Provider. Непонятно, зачем нам нужно было помещать что-то подобное в сам фреймворк.

Я предполагаю, потому что это по своей сути расширяемое. Провайдера нет, это простой инструмент. Это то, что сделано для расширения, как и StatefulWidget, но для StatefulComponents. Тот факт, что это относительно тривиально, не следует возражать против этого?

Примечание о «тех, кто предпочитает этот стиль». Если вы можете сохранить 3 переопределения и 15-30 строк, в большинстве случаев это будет просто победой для удобочитаемости. Объективно говоря имо. Это также объективно устраняет 2 целых класса ошибок (забвение утилизировать вещи, забывая обновить deps).

Большое спасибо за отличное обсуждение, рад видеть, к чему это приведет, я обязательно оставлю это здесь :)

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

Это особенно неприятно спорить, учитывая, что все основные декларативные фреймворки (React, Vue, Swift UI, Jetpack Compose) так или иначе позволяют решить эту проблему.

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

Чувствую (по крайней мере, мне), что это не воспринимают всерьез, извините, @Hixie , я имею в виду, что это не воспринимается всерьез в смысле: We understand the problem and want to solve it . Как и другие декларативные фреймворки, похоже, сделали.
С другой стороны, такая же заметка:

Легче написать?
Разработчик должен научиться писать хуки, это новая концепция, поэтому сложнее

Огорчает меня. Зачем что-то улучшать или менять? Вы всегда можете привести этот аргумент, несмотря ни на что. Даже если новые решения станут намного проще и приятнее, когда вы их освоите. Вы можете заменить хуки в этом операторе множеством вещей. Моя мать могла бы использовать такое высказывание о микроволновых печах 30 лет назад. Это, например, работает так же, если вы замените «крючки» в предложении на «флаттер» или «дротик».

Легче написать?
Это то же самое, поэтому так же легко писать, что довольно легко

Я не думаю, что @rrousselGit означает с is it easier to write? (вопрос с логическим ответом), что если это то же самое, то ответ не false / undefined .

Я не понимаю, как мы когда-нибудь сможем куда-то добраться, поскольку мы даже не согласны с тем, что есть проблема, только то, что многие люди считают это проблемой. Например:

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

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

Я действительно надеюсь, что вы примете @rrousselGit его предложение и организуете звонок, может быть, будет проще обсудить это в реальном времени, вместо того, чтобы все время писать туда-сюда. Если какие-либо люди из других фреймворков, которые решили проблему, описанную в OP, если один из них действительно добр, возможно, они могли бы некоторое время присоединиться к призыву и поделиться своими 5-ю копейками о возникающих вещах. Всегда можно было спросить.

В любом случае, я отписываюсь сейчас, потому что мне немного грустно следить за этой веткой, так как я не вижу, чтобы она никуда не делась. Но я действительно надеюсь, что эта ветка дойдет до состояния согласия, что есть проблема, и сможет сосредоточиться на возможных решениях OP. Поскольку кажется немного бесполезным предлагать решения, если вы не понимаете проблемы, с которыми сталкиваются люди, с @Hixie, вероятно, соглашается, я имею в виду, поскольку люди с проблемами скажут вам, почему решение не работает впоследствии.

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

LayoutBuilder сильно отличается от виджета FWIW большинства конструкторов. Ни одно из обсуждаемых до сих пор предложений не будет работать для проблем, подобных LayoutBuilder, и ни одно из требований, описанных перед вашим комментарием, не включает LayoutBuilder. Если мы также должны использовать эту новую функцию для обработки LayoutBuilder, это важно знать; Я рекомендую работать с @TimWhiting, чтобы убедиться, что пример приложения, на котором мы собираемся основывать предложения, включает в себя построители макетов в качестве примера.

@Hixie Да, нам определенно нужны образцы. Я что-нибудь подготовлю (но все же думаю, что в компиляторе нужны изменения, поэтому пример может быть неполным). Общая идея такова - синтаксический сахар, который сглаживает компоновщики и не заботится о том, как он реализован.

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

@ emanuel-lundman

Чувствую (по крайней мере, мне), что это не воспринимают всерьез, извините, @Hixie , я имею в виду, что это не воспринимается всерьез в смысле: We understand the problem and want to solve it . Как и другие декларативные фреймворки, похоже, сделали.

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

@szotp

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

Я изучал Swift UI. Конечно, писать код Swift UI менее многословно, чем код Flutter, но стоимость удобочитаемости очень высока, ИМХО. В нем много «магии» (в смысле логики, которая работает способами, не очевидными в коде потребителя). Я могу полностью поверить, что это стиль, который предпочитают некоторые люди, но я также считаю, что одна из сильных сторон Flutter заключается в том, что в нем очень мало магии. Это действительно означает, что иногда вы пишете больше кода, но это также означает, что отладка этого кода становится намного проще.

Я думаю, что есть место для множества стилей фреймворков. Стиль MVC, стиль React, супер краткий волшебный, свободный от магии, но многословный ... Одним из преимуществ архитектуры Flutter является то, что аспект переносимости полностью отделен от самого фреймворка, поэтому можно использовать все наши инструменты - кроссплатформенная поддержка, горячая перезагрузка и т. д. - но создать совершенно новый фреймворк. (Уже существуют другие фреймворки Flutter, например flutter_sprites.) Точно так же сам фреймворк спроектирован многоуровневым, так что, например, вы можете повторно использовать всю нашу логику RenderObject, но с заменой слоя виджетов, поэтому, если многословие Виджетов слишком много, кто-то может создать альтернативный фреймворк, который его заменяет. И, конечно же, есть система упаковки, поэтому функции могут быть добавлены без потери какого-либо существующего кода фреймворка.

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


Я призываю людей, заинтересованных в этой проблеме, работать с @TimWhiting, чтобы создать приложение, которое демонстрирует, почему вы хотите повторно использовать код и как он выглядит сегодня, когда вы не можете (https://github.com/TimWhiting/local_widget_state_approaches). Это напрямую поможет нам создать предложения о том, как решить эту проблему таким образом, чтобы удовлетворить потребности _все_ людей, которые здесь комментируют (включая тех, кто любит хуки, и тех, кто не любит хуки).

На самом деле не может быть так сложно понять, почему "_ синтаксический сахар, который сглаживает строители и не заботится о том, как реализован конструктор ._" желателен разработчиками как первоклассная функция. Мы снова и снова очерчивали проблемы с альтернативными подходами.

Короче говоря, конструкторы решают проблему повторного использования, но их трудно читать и составлять. «Проблема» просто в том, что мы хотели бы, чтобы функциональность, подобная построителю, была значительно легче читаемой.

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

В эти выходные я потратил буквально часы своего времени, не говоря уже о многих часах времени, которое Google провел до этого, рассматривая эту проблему, описывая возможные решения и пытаясь точно определить, что мы пытаемся решить.

Я благодарен за это

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

Меня устраивает непонимание, но текущая ситуация кажется безнадежной.
Мы все еще обсуждаем моменты, которые были высказаны в самом начале обсуждения.

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

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

У этих анализов есть значительное количество 👍, и другие люди, кажется, согласны с этим.

И все же, согласно вашему недавнему ответу, проблемы с читабельностью нет: https://github.com/flutter/flutter/issues/51752#issuecomment -671009593

Вы также предложили:

Widget build(context) {
  var result = Text('result:');
  var builder1 = (BuildContext context, AsyncSnapshot<int> snapshot) {
    return Row(children: [result, Text(snapshot.data)]);
  };
  result = StreamBuilder(stream: _stream1, builder: builder1);
  var builder2 = (BuildContext context, AsyncSnapshot<int> snapshot) {
    return Column(children: [result, Text(snapshot.data)]);
  };
  result = StreamBuilder(stream: _stream2, builder: builder2);
}

зная, что это не читается

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

  • мы не согласны с тем, что существует проблема с удобочитаемостью
  • до сих пор неясно, является ли удобочитаемость частью этой проблемы или нет

Это неприятно слышать, учитывая, что единственная цель хуков - улучшить синтаксис Builders, которые находятся на пике возможности повторного использования, но имеют плохую читаемость / возможность записи.
Если мы не согласны с таким основным фактом, я не уверен, что мы можем сделать.

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

И мне очень нравится многоуровневая архитектура Flutter. Я также хотел бы продолжать использовать виджеты. Так что, возможно, ответ состоит в том, чтобы просто улучшить расширяемость Dart & Flutter, что для меня было бы:

Сделайте генерацию кода более плавной - возможно, удастся реализовать магию SwiftUI в Dart, но обычная необходимая настройка слишком велика и слишком медленна.

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

РЕДАКТИРОВАТЬ: Я думаю, что flutter generate было шагом в правильном направлении, жаль, что его удалили.

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

Было бы хорошее начало. Разделите эту проблему на разные части / вопросы и посмотрите, действительно ли это проблема, которую разработчики Flutter хотят решить.

Как только это станет ясно, этот разговор станет более плавным и обогащающим.

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

Если я задаю одни и те же вопросы, это потому, что я не понимаю ответов.

Например, возвращаясь к вашему предыдущему комментарию (https://github.com/flutter/flutter/issues/51752#issuecomment-670959424):

Обсуждаемая проблема:

Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}

Этот код не читается.

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

Почему это не читается?

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

Мы могли бы исправить проблему с удобочитаемостью, введя новое ключевое слово, которое изменяет синтаксис на:

Widget build(context) {
  final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);
  final value2 = keyword StreamBuilder(stream: someStream);
  final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

  return Text('$value $value2 $value3');
}

Этот код значительно более читабелен, не связан с хуками и не страдает от его ограничений.

Это, конечно, менее многословно. Я не уверен, что это станет более читаемым, по крайней мере, для меня. Есть больше концепций (теперь у нас есть и виджеты, и функция "ключевых слов"); больше понятий означает большую когнитивную нагрузку. (Это также потенциально менее эффективно, в зависимости от того, насколько эти объекты независимы; например, если анимация всегда изменяется чаще, чем значение listenable и stream, теперь мы перестраиваем ValueListenableBuilder и StreamBuilder, даже если обычно они не запускались ; также теперь необходимо вводить логику инициализатора и пропускать каждую сборку.)

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

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

У вас есть решение, которое вам нравится (хуки), которое, насколько я могу судить, отлично работает, но вы хотите что-то изменить во фреймворке, чтобы люди использовали хуки? Чего я тоже не понимаю, потому что, если людям не нравятся хуки в достаточной степени, чтобы использовать их как пакет, это, вероятно, тоже не лучшее решение для фреймворка (в целом мы больше движемся к использованию пакетов, даже функций команда Flutter создает, чего это стоит).

Я понимаю, что есть желание упростить повторное использование кода. Я просто не знаю, что это значит.

Как следующая читаемость соотносится с двумя вышеупомянутыми версиями?

Widget build(context) {
  return
    ValueListenableBuilder(valueListenable: someValueListenable, builder: (context, value, _) =>
    StreamBuilder(stream: someStream, builder: (context, value2) =>
    TweenAnimationBuilder(tween: Tween(...), builder: (context, value3) =>
    Text('$value $value2 $value3'),
  )));
}

@szotp Если вокруг нашего текущего решения по сообщать об ошибке с просьбой внести улучшения.

@jamesblasco Я не думаю, что есть сомнения в том, что здесь есть настоящая проблема, которую люди хотят решить. Для меня вопрос в том, что это за проблема, чтобы мы могли разработать решение.

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

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

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

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

Например, Flutter и приведенный вами пример приложения:

  • не используйте dartfmt
  • используйте always_specify_types

С этими двумя пунктами я был бы удивлен, если бы они представляли более 1% сообщества.

Таким образом, то, что вы оцениваете как читаемое, скорее всего, сильно отличается от того, что большинство людей считает читаемым.

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

Почему это не читается?

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

Проведем эксперимент:
Я дам вам два дерева виджетов. Один использует линейный синтаксис, другой - вложенный синтаксис.
Я также дам вам конкретные вещи, которые вам нужно искать в этом фрагменте.

Легче найти ответ: использовать линейный синтаксис или вложенный синтаксис?

Вопросы:

  • Какой виджет, не связанный с построением, возвращается этим методом сборки?
  • Кто создает переменную bar ?
  • Сколько у нас строителей?

Использование строителей:

 Сборка виджета (контекст) {
 вернуть ValueListenableBuilder(
 valueListenable: someValueListenable,
 строитель: (context, foo, _) {
 вернуть StreamBuilder(
 поток: someStream,
 builder: (context, baz) {
 return TweenAnimationBuilder(
 подросток: Подросток (...),
 builder: (context, bar) {
 return Container ();
 },
 );
 },
 );
 },
 );
 }

Используя линейный синтаксис:

 Сборка виджета (контекст) {
 final foo = ключевое слово ValueListenableBuilder (valueListenable: someValueListenable);
 заключительная панель = ключевое слово StreamBuilder (поток: someStream);
 final baz = ключевое слово TweenAnimationBuilder (tween: Tween (...));

return Image (); }


В моем случае мне сложно просматривать вложенный код, чтобы найти ответ.
С другой стороны, поиск ответа с помощью линейного дерева происходит мгновенно.

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

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

Игнорируя тот факт, что пример не работает, я не против разбить его, чтобы объяснить, почему он не читается. Было бы это ценно?

дротик
Сборка виджета (контекст) {
возвращение
ValueListenableBuilder (valueListenable: someValueListenable, построитель: (контекст, значение, _) =>
StreamBuilder (поток: someStream, построитель: (контекст, значение2) =>
TweenAnimationBuilder (анимация движения: Tween (...), построитель: (контекст, значение3) =>
Текст ('$ значение $ значение2 $ значение3'),
)));
}

Так выглядит лучше.
Но это при условии, что люди не используют dartfmt

С dartfmt у нас есть:

Widget build(context) {
  return ValueListenableBuilder(
      valueListenable: someValueListenable,
      builder: (context, value, _) => StreamBuilder(
          stream: someStream,
          builder: (context, value2) => TweenAnimationBuilder(
                tween: Tween(),
                builder: (context, value3) => Text('$value $value2 $value3'),
              )));
}

который почти не отличается от исходного кода.

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

Это деталь реализации.
Нет особой причины иметь элемент или нет.
Фактически, может быть интересно иметь элемент, чтобы мы могли включить LayoutBuilder и, возможно, GestureDetector .

Думаю, это низкий приоритет. Но в сообществе React среди различных библиотек хуков я видел:

  • useIsHovered - возвращает логическое значение, которое сообщает, завис ли виджет
  • useSize - (Вероятно, следует использовать useContraints во Flutter), который дает размер связанного пользовательского интерфейса.

(Это также потенциально менее эффективно, в зависимости от того, насколько эти объекты независимы; например, если анимация всегда изменяется чаще, чем значение listenable и stream, теперь мы перестраиваем ValueListenableBuilder и StreamBuilder, даже если обычно они не запускались ; также теперь необходимо вводить логику инициализатора и пропускать каждую сборку.)

Это зависит от того, как будет решено решение.

Если мы исправим язык, эта проблема не будет проблемой.

Мы могли бы сделать это:

Widget build(context) {
  final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);
  final value2 = keyword StreamBuilder(stream: someStream);
  final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

  return Text('$value $value2 $value3');
}

"компилируется" в:

Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}

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

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

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

Почему это не читается?

Из-за вложенности вложение затрудняет быстрое сканирование и понимание того, какие части вы можете игнорировать, а какие важны для понимания того, что происходит. Код является своего рода «последовательным» по своей природе, но вложенность скрывает это. Вложение также усложняет работу с ним - представьте, что вы хотите переупорядочить две вещи - или внедрить новую вещь между двумя - тривиально для действительно последовательного кода, но сложно, когда вам нужно работать с вложением.

Это очень похоже на асинхронный / ожидающий сахар по сравнению с работой с необработанным Future API, концепция, основанная на продолжении dame внизу (и даже аргументы за и против очень похожи) - да Future API можно использовать напрямую и ничего не скрывает, кроме удобочитаемости и ремонтопригодности это, конечно, нехорошо - async / await здесь победитель.

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

Я программировал уже 25 лет на более чем 10 разных языках, и это худший способ оценить, что делает код читабельным. Читаемость исходного кода - непростая задача, но это больше о том, насколько хорошо он выражает концепции и логику программирования, а не о том, «куда смотрят мои глаза» или о том, сколько строк кода он использует.

Или, скорее, мне кажется, что вы, ребята, слишком много внимания уделяете удобочитаемости и меньше - ремонтопригодности .

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


final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);

Какая будет стоимость? Виджет? Строковая переменная? Я имею в виду, что он используется внутри
return Text('$value $value2 $value3');

По сути, вы хотите, чтобы ссылка на переменную A в методе сборки виджета B заставляла B перестраивать всякий раз, когда значение A изменяется? Это буквально то, что делает mobx, и делает это именно с правильным количеством магии / шаблонов.


final value2 = keyword StreamBuilder(stream: someStream);

Что это должно вернуть? Виджет? Поток? Строковое значение?

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

Да, с mobx вы можете сделать так, чтобы ваши методы сборки выглядели точно так же, как ваш «более читаемый» пример (за исключением того, что вы ссылаетесь на наблюдаемые). Вам все равно нужно написать код, который выполняет всю работу, как вы это делаете с хуками. Фактический код составляет около 10 строк, и его можно многократно использовать в любом виджете.


final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

Класс под названием "TweenAnimationBuilder" возвращает строку ?! Я даже не подхожу, почему это ужасная идея.

Нет никакой разницы в отступе / удобочитаемости между:

Future<double> future;

AsyncSnapshot<double> value = keyword FutureBuilder<double>(future: future);

а также:

Future<double> future;

double value = await future;

Оба делают одно и то же: слушают объект и раскрывают его значение.

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

Тот же аргумент можно применить к цепочкам Promise / Future.

foo().then(x =>
  bar(x).then(y =>
    baz(y).then(z =>
      qux(z)
    )
  )
)

против

let x = await foo();
let y = await bar(x);
let z = await baz(y);
await qux(z);

Можно сказать, что первый способ написания проясняет, что промисы создаются под капотом и как именно формируется цепочка. Интересно, подписались ли вы на это или считаете, что Promises принципиально отличаются от Builders тем, что они заслуживают синтаксиса.

Класс под названием "TweenAnimationBuilder" возвращает строку ?! Я даже не подхожу, почему это ужасная идея.

Вы можете привести тот же аргумент в отношении Promises / Futures и сказать, что await скрывает тот факт, что он возвращает Promise.

Должен заметить, что идея «разворачивать» вещи с помощью синтаксиса вряд ли нова. Да, в основных языках это происходит через async / await, но, например, F # имеет вычислительные выражения , похожие на нотацию do в некоторых хардкорных языках программирования FP. Там он имеет гораздо большую мощность и предназначен для работы с любыми оболочками, которые удовлетворяют определенным законам. Я не предлагаю добавлять монады в Dart, но я думаю, что стоит упомянуть, что определенно существует прецедент типобезопасного синтаксиса для «разворачивания» вещей, которые не обязательно соответствуют асинхронным вызовам.

Сделав шаг назад, я думаю, что одна вещь, с которой здесь борются многие люди (включая меня), - это вопрос о удобочитаемости. Как упоминал @rrousselGit , в этом потоке было много примеров проблем с удобочитаемостью с текущим подходом на основе Builder . Многим из нас кажется очевидным, что это:

Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}

значительно менее читабельно, чем это:

Widget build(context) {
  final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);
  final value2 = keyword StreamBuilder(stream: someStream);
  final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

  return Text('$value $value2 $value3');
}

Но это явно не самоочевидно, поскольку @Hixie и @Rudiksz не убеждены (или активно возражают) против идеи, что второе более читабельно, чем первое.

Итак, вот моя разбивка (за какую бы небольшую сумму) о том, почему второй блок кода более читабелен, чем первый:

1. Первый блок кода имеет значительно больший отступ, чем второй.

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


Во втором блоке кода нет отступов, которые устраняют проблему.

2. Первый блок кода требует большего синтаксиса для выражения своего намерения.

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


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

3. Первый блок кода скрывает наиболее важную часть метода build в самой глубокой части вложенности.

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


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

Что касается вложенности, есть разница между вложением, выражающим _дерево_, и вложением, выражающим _последовательность_ .

В случае нормального вложения View -> Text и т. Д. Вложение важно, потому что оно представляет отношения родитель-потомок на экране. Для таких функций, как Context (не уверен, есть ли он у Flutter), он представляет собой область контекстов. Таким образом, вложение имеет важное семантическое значение в этих случаях и не может быть проигнорировано. Вы не можете просто поменять местами родительский и дочерний и ожидать, что результат будет таким же.

Однако с вложением Builders (также известного как «Render Props» в React) или вложением обещаний предполагается, что вложение передает последовательность преобразований / дополнений . Само дерево не так важно - например, при вложении независимых ABuilder -> BBuilder -> CBuilder их родительско-дочерние отношения не передают дополнительного значения.

Пока все три значения доступны в приведенной ниже области, их древовидная структура на самом деле не актуальна. Он концептуально «плоский», и вложение - только артефакт синтаксиса. Конечно, они могут использовать значения друг друга (в этом случае их порядок имеет значение), но это также относится и к последовательным вызовам функций, и это можно делать без вложенности.

Вот почему async/await привлекает внимание. Он удаляет дополнительную информацию (отношения родитель-потомок промисов), которая описывает низкоуровневый механизм, и вместо этого позволяет вам сосредоточиться на высокоуровневом намерении (описании последовательности).

Дерево - более гибкая структура, чем список. Но когда у каждого родителя есть только один ребенок, это становится патологическим деревом - по сути, списком. Async / Await и Hooks распознают, что мы тратим синтаксис впустую на то, что не передает информацию, и удаляем его.

Это на самом деле интересно, потому что раньше я сказал: «Дело не в шаблоне», а теперь мне кажется, что я противоречу самому себе. Я думаю, здесь есть две вещи.

Сами по себе Builders (или, по крайней мере, Render Props в React) являются решением (AFAIK) проблемы «многоразовой логики». Просто они не очень эргономичны, если их использовать много. Естественно, отступы отговаривают вас использовать более 4 или 5 из них в одном компоненте. Каждый следующий уровень - это удар по удобочитаемости.

Так что часть, которая мне кажется нерешенной, - это снижение затрат читателя на вложение. И этот аргумент точно такой же, как и для async / await .

Он не так удобочитаем по следующим причинам:

  • Чрезмерное использование пробелов плохо масштабируется. У нас на мониторе ограниченное количество строк, а принудительная прокрутка снижает удобочитаемость и увеличивает постоянную нагрузку. Представьте, что у нас уже есть дерево виджетов из 60 строк, и вы только что заставили меня добавить еще 15 для строителей, что не идеально.
  • Он тратит впустую пространство Hz, которое мы ограничиваем, что приводит к дополнительному обтеканию, что еще больше расходует пространство строки.
  • Он толкает листовой узел, также известный как контент, дальше от левой стороны в дерево, где его труднее заметить с первого взгляда.
  • Намного труднее сразу определить «ключевых игроков» или «нестандартных». Я должен «найти самое важное» еще до того, как начну рассуждать.

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

С выделением этого очень легко рассуждать. Без него мне нужно прочитать всю многословность, прежде чем я смогу выяснить, кто что использует и где:
image

Теперь сравните с этим, выделение в основном избыточно, потому что мне больше некуда деться:
image

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

Для справки, эквивалентом Context в React является InheritedWidgets / Provider.

Единственная разница между ними в том, что в React перед хуками мы _had_ использовали шаблон Builder для использования Context / Inheritedwidget.

В то время как у Flutter есть способ связать перестройку с помощью простого вызова функции.
Таким образом, нет необходимости в крючках для выравнивания деревьев с помощью InheritedWidgets, что позволяет решить проблему Builders.

Вероятно, это одна из причин, по которой обсуждение затруднено, поскольку строители нам нужны реже.

Но стоит упомянуть, что внедрение крючкового решения решит как https://github.com/flutter/flutter/issues/30062
и https://github.com/flutter/flutter/issues/12992

Также похоже, что @Hixie больше привык читать глубоко вложенные деревья, потому что Flutter - это в основном все деревья, на мой взгляд, гораздо больше, чем другие языки. Как один из основных разработчиков самого Flutter, он, конечно, имел бы гораздо больший опыт в этом. По сути, Flutter можно рассматривать как фреймворк слева направо с глубоким вложением, очень похожий на HTML, с которым, я полагаю, у @Hixie был опыт, создав спецификацию HTML5. Таким образом, самая правая точка блока кода - это место, где находятся основная логика и возвращаемое значение.

Однако большинство разработчиков не являются языками сверху вниз или переходят на них

Другой способ взглянуть на это - сколько кода моему мозгу нужно визуально отредактировать. Для меня это точно представляет «чертову работу», которую должен выполнить мой мозг, прежде чем я смогу проанализировать то, что возвращается из дерева:
image

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

Считаете мой глаз голодным процессором, который больше оптимизирован для обработки? :)

В случае обычного View -> Text вложенности и т.п. вложенность важна, потому что она представляет на экране _детско-родительские отношения_. Для таких функций, как Context (не уверен, есть ли он у Flutter), он представляет собой область контекстов. Таким образом, вложение имеет важное семантическое значение в этих случаях и не может быть проигнорировано. Вы не можете просто поменять местами родительский и дочерний и ожидать, что результат будет таким же.

Полностью согласен, и я уже упоминал об этом ранее. Семантически нет смысла создавать дополнительные слои контекста в дереве визуального отображения, потому что я использую дополнительные невизуальные контроллеры, у которых есть состояние. Используя 5 аниматоров, теперь ваш виджет имеет 5 слоев? Только на этом высоком уровне от нынешнего подхода попахивает.

Здесь меня бросают в глаза две проблемы.

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

  2. Сборка - это синхронизация, но самое интересное, с чем вы имеете дело как разработчик приложения, - это асинхронность. Конечно, мы не можем сделать сборку асинхронной. Мы создали такие удобства, как Stream / Animation / FutureBuilder, но они не всегда работают достаточно хорошо для того, что нужно разработчику. Вероятно, это говорит о том, что мы не часто используем Stream или FutureBuilder во фреймворке.

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

fwiw, команда React решила повторно использовать проблему удобочитаемости в качестве мотивации 1:
Мотивация хуков: сложно повторно использовать логику с отслеживанием состояния между компонентами
React не предлагает способ «привязать» повторно используемое поведение к компоненту ... возможно, вы знакомы с шаблонами ... которые пытаются решить эту проблему. Но эти шаблоны требуют от вас реструктуризации компонентов при их использовании, что может быть громоздким и усложнять выполнение кода .

Это очень похоже на то, как Flutter в настоящее время не предлагает нам возможности изначально «составить состояние». Это также перекликается с тем, что происходит, когда мы, строители, модифицируем наше дерево макетов и делаем его более громоздким для работы и «сложным для отслеживания», - сказано в дереве.

@dnfield, если build должен вызываться каждый раз, возможно, мы сможем сделать перехватчики не в методе build чтобы сборка всегда синхронизировалась, т.е. поместите их в класс, где initState и dispose равны. Есть ли проблемы с этим у тех, кто пишет хуки?

Вы можете привести тот же аргумент в отношении Promises / Futures и сказать, что await скрывает тот факт, что он возвращает Promise.

Нет, не знаешь. Await - это буквально синтаксический сахар для одной-единственной функции. Если вы используете подробные Futures или декларативный синтаксис, __intent__ кода будет тем же.

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

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

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

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

@Rudiksz Опять же, никто не заставляет переходить на хуки. Текущий стиль останется. Каковы ваши аргументы против этого знания?

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

@Rudiksz Опять же, никто не заставляет переходить на хуки. Текущий стиль останется. Каковы ваши аргументы против этого знания?

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

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

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

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

fwiw, команда React решила повторно использовать проблему удобочитаемости в качестве мотивации 1:
Мотивация хуков: сложно повторно использовать логику с отслеживанием состояния между компонентами
React не предлагает способ «привязать» повторно используемое поведение к компоненту ... возможно, вы знакомы с шаблонами ... которые пытаются решить эту проблему. Но эти шаблоны требуют от вас реструктуризации компонентов при их использовании, что может быть громоздким и усложнять выполнение кода .

Я слышал, как все восхищаются тем, что Flutter намного круче, чем React. Может быть, это потому, что он не делает все так, как React? У вас не может быть обоих вариантов, нельзя сказать, что Flutter намного опережает React, а также попросить, чтобы он делал все точно так же, как React.

Какое бы решение Flutter ни решил использовать для данной проблемы, оно должно стоять само по себе. Я не знаком с React, но, видимо, упускаю какую-то действительно удивительную технологию. : /

Я не думаю, что кто-то спорит, что Flutter должен делать все как React.

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

Это также означает, что у React больше опыта, чем у Flutter, в решении этих проблем.
Он дольше с ними сталкивался и лучше их понимает.

Поэтому неудивительно, что решения проблем Flutter аналогичны решениям проблем React.

Пользовательский API @Rudiksz Flutter очень похож на модель, основанную на классах React, даже несмотря на то, что внутренний API может быть другим (я не знаю, отличаются ли они, я действительно не очень часто сталкиваюсь с внутренним API). Я действительно рекомендую вам попробовать React с хуками, чтобы увидеть, как это происходит, как я уже говорил ранее, что, похоже, существует дихотомия мнений, основанная почти исключительно на тех, кто использовал и не использовал крючковые конструкции в других фреймворках.

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

Пожалуйста, давайте постараемся не драться друг с другом.

Единственное, к чему нас приведет борьба, - это уничтожить эту дискуссию и не найти решения.

Я слышал, как все восхищаются тем, что Flutter намного круче, чем React. Может быть, это потому, что он не делает все так, как React? У вас не может быть обоих вариантов, нельзя сказать, что Flutter намного опережает React, а также попросить, чтобы он делал все точно так же, как React.

Указание на то, что у команды React были похожие мотивы, когда они придумывали хуки, подтверждает озабоченность, которую мы здесь выражаем. Это, безусловно, подтверждает, что существует проблема повторного использования и объединения общей логики с отслеживанием состояния в рамках этого типа компонентной структуры, а также в некоторой степени подтверждает правильность обсуждения читабельности, вложенности и общей проблемы с «беспорядком» в ваших представлениях.

Ничего не в восторге, я даже никогда не работал в React, и мне нравится Flutter. Я легко вижу здесь проблему.

@Rudiksz, мы не можем быть уверены, что он эффективен на практике, пока не

@Hixie это пример путешествия, которое может иметь обычный пользователь flutter для реализации виджета для отображения никнейма пользователя из userId как с HookWidget и с StatefulWidget .

__hook widget__

String useUserNickname(Id userid) {
  final name = useState("");
  useEffect(async () {
    name.value = "Loading...";
    name.value = await fetchNicknames()[userId;
  }, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__stateful widget__

class UserNickname extends Widget {
  final userId;
  // ... createState() ...
}

class UserNicknameState extends State {

  String nickname= "";

   initState() {
     super.initState();
     fetchAndUpdate();
   }

   fetchAndUpdate() async {
      setState(() { this.nickname = "Loading..." });
      final result = await fetchNicknames()[widget.userId];
      setState(() { this.nickname = result });
    }


 void didUpdateWidget(oldWidget) { 
     if (oldWidget.userId != widget.userId) {
        fetchAndUpdate();
     }
   }

  Widget build(BuildContext context) {
    return Text(this.nickname);
  }
}

пока ничего интересного. оба решения довольно приемлемы, просты и производительны.
теперь мы хотим использовать UserNickname внутри ListView . как вы можете видеть, fetchNicknames возвращает карту псевдонимов, а не только один ник. поэтому называть его каждый раз излишне. несколько решений, которые мы можем применить здесь:

  • переместите вызывающую логику fetchNicknames() в родительский виджет и сохраните результат.
  • с помощью кеш-менеджера.

Первое решение приемлемо, но имеет 2 проблемы.
1 - он делает UserNickname бесполезным, потому что теперь это только текстовый виджет, и если вы хотите использовать его в другом месте, вам нужно повторить то, что вы сделали в родительском виджете (который имеет ListView ) . логика отображения псевдонима принадлежит UserNickname но мы должны перемещать его отдельно.
2 - мы можем использовать fetchNicknames() во многих других поддеревьях, и лучше кэшировать его для всего приложения, а не только для одной части приложения.

Итак, представьте, что мы выбираем диспетчер кеша и предоставляем класс CacheManager с InheritedWidgets или Provider .

после добавления поддержки кеширования:

__hook widget__

String useUserNickname(Id userid) {
  final context = useContext();
  final cache = Provider.of<CacheManager>(context);
  final name = useState("");
  useEffect(async () {
    name.value = "Loading...";
    var cachedValue = cache.get("nicknames");
    if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        cache.set("nicknames", result );
        cachedValue = result ;
    }
    final result = cachedValue[widget.userId];
    name.value = result ;
  }, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__stateful widget__

class UserNickname extends Widget {
  final userId;
  // ... createState() ...
}

class UserNicknameState extends State {

  String nickname= "";
  CacheManager cache;

   initState() {
     super.initState();
     fetchAndUpdate();
     this.cache = Provider.of<CacheManager>(context);
   }

   fetchAndUpdate() async {
      setState(() { this.nickname = "Loading..." });
      var cachedValue = this.cache.get("nicknames");
      if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        this.cache.set("nicknames", result );
        cachedValue = result ;
      }
      final result = cachedValue [widget.userId];
      setState(() { this.nickname = result });
    }


 void didUpdateWidget(oldWidget) { 
     if (oldWidget.userId != widget.userId) {
        fetchAndUpdate();
     }
   }

  Widget build(BuildContext context) {
    return Text(this.nickname);
  }
}

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

__hook widget__

String useUserNickname(Id userid) {
  final context = useContext();
  final cache = Provider.of<CacheManager>(context);
  final notifications = Provider.of<ServiceNotifications>(context);
  final name = useState("");

  fetchData() async {
    name.value = "Loading...";
    var cachedValue = cache.get("nicknames");
    if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        cache.set("nicknames", result );
        cachedValue = result ;
    }
    final result = cachedValue[widget.userId];
    name.value = result ;
   }

  useEffect(() {
     final sub = notifications.on("nicknameChanges", fetchData);
     return () => sub.unsubscribe();
   }, [])

  useEffect(fetchData, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__stateful widget__

class UserNickname extends Widget {
  final userId;
  // ... createState() ...
}

class UserNicknameState extends State {

  String nickname= "";
  CacheManager cache;
  ServerNotification notifications;
  CancelableOperation sub;

   initState() {
     super.initState();
     fetchAndUpdate();
     this.cache = Provider.of<CacheManager>(context);
     this.notifications = Provider.of<ServerNotification>(context);
     this.sub = notifications.on("nicknameChanges", fetchAndUpdate);
   }

   dispose() {
      super.dispose();
      this.sub.unsubscribe();
   }

   fetchAndUpdate() async {
      setState(() { this.nickname = "Loading..." });
      var cachedValue = this.cache.get("nicknames");
      if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        this.cache.set("nicknames", result );
        cachedValue = result ;
      }
      final result = cachedValue [widget.userId];
      setState(() { this.nickname = result });
    }


 void didUpdateWidget(oldWidget) { 
     if (oldWidget.userId != widget.userId) {
        fetchAndUpdate();
     }
   }

  Widget build(BuildContext context) {
    return Text(this.nickname);
  }
}

пока обе реализации приемлемы и хороши. ИМО, шаблон в статической версии вообще не проблема. проблема возникает, когда нам нужен виджет типа UserInfo , у которого есть и ник пользователя, и аватар. также мы не можем использовать виджет UserNickname потому что нам нужно отобразить его в предложении типа «Добро пожаловать, [имя пользователя]».

__hook widget__

useFetchUserNickname(userId) // old code
useUserAvatar(userId) // implementation like `useFetchUserNickname`

class UserNickname extends HookWidget {
  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    final avatar = useUserAvatar(userId);
    return Row(
      children: [Image.network(avatar), Text(nickname)],
    );
  }
}

но для виджета __stateful__ мы не можем просто использовать написанную нами логику. мы должны переместить логику в класс (например, Property который вы предложили), и все же нам нужно снова написать клей для виджета с классом свойств в новом виджете.

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

ИМХО, единственная проблема - это вызов useUserNickname страшно, потому что одна функция может сделать это много.
но мой многолетний опыт реагирования и использования flutter_hooks в 2 приложениях, которые находятся в производстве (которые активно используют хуки), доказывает, что отсутствие хорошего управления состоянием (я также пробовал MobX и другие решения для управления состоянием, но клей в виджете всегда есть) намного страшнее. Мне не нужно писать 5-страничные документы для каждого экрана во внешнем приложении, и мне может потребоваться добавить небольшую функцию через несколько месяцев после первого выпуска, чтобы она понимала, как работает страница моего приложения. Приложение слишком много звонит на сервер? простая задача Я использую связанный крючок и меняю его, и все приложение исправляет, потому что все приложение использует этот крючок. мы можем иметь похожие вещи в приложениях без использования хуков с хорошей абстракцией, но я хочу сказать, что хуки - это хорошая абстракция.

Я почти уверен, что @gaearon может

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

также возможно обновить только поддерево, которое нам нужно обновить, например StreamBuilder когда есть слишком много обновлений (например, анимаций), с помощью:

1 - просто создание нового виджета, который вполне жизнеспособен как для HookWidget и для StatefulWidget/StatelessWidget
2 - использование чего-то похожего на HookWidgetBuilder в пакете flutter_hooks , потому что данные родительского и дочернего виджетов очень тесно связаны.

Боковое примечание: я очень признателен @Hixie и @rrousselGit за обсуждение этой темы и за то, что они

Я думаю, что придумываю что-то довольно крутое / элегантное, основываясь на отправной точке @Hixie . Еще не совсем готов к публикации, но он позволяет мне создавать довольно приличные образцы кода, которые, я думаю, будет легче сравнивать яблоки: яблоки, а не крючки, которые выглядят так чуждо.

Итак, представьте, что у нас есть StatefulWidget с такой подписью:

class ExampleSimple extends StatefulWidget {
  final Duration duration1;
  final Duration duration2;
  final Duration duration3;

  const ExampleSimple({Key key, this.duration1, this.duration2, this.duration3}) : super(key: key);

  <strong i="9">@override</strong>
  _ExampleSimpleState createState() => _ExampleSimpleState();
}

Если бы мы реализовали состояние с помощью контроллеров ванильного аниматора, мы получили бы что-то вроде:

class _ExampleSimpleVanillaState extends State<ExampleSimpleVanilla> with SingleTickerProviderStateMixin {
  AnimationController _anim1;
  AnimationController _anim2;
  AnimationController _anim3;

  <strong i="13">@override</strong>
  void initState() {
    _anim1 = AnimationController(duration: widget.duration1, vsync: this);
    _anim1.forward();
    _anim2 = AnimationController(duration: widget.duration2, vsync: this);
    _anim2.forward();
    _anim3 = AnimationController(duration: widget.duration3, vsync: this);
    _anim3.forward();
    super.initState();
  }

  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: _anim2.value * 20, horizontal: _anim3.value * 30,),
      color: Colors.red.withOpacity(_anim1.value),
    );
  }

  <strong i="15">@override</strong>
  void didUpdateWidget(ExampleSimpleVanilla oldWidget) {
    if (oldWidget.duration1 != widget.duration1) {
      _anim1.duration = widget.duration1;
    }
    if (oldWidget.duration2 != widget.duration2) {
      _anim1.duration = widget.duration1;
    }
    if (oldWidget.duration3 != widget.duration3) {
      _anim1.duration = widget.duration1;
    }
    super.didUpdateWidget(oldWidget);
  }

  <strong i="16">@override</strong>
  void dispose() {
    _anim1.dispose();
    _anim2.dispose();
    _anim3.dispose();
    super.dispose();
  }
}

Если мы создадим его с помощью StatefulProperty, мы получим что-то вроде этого:

class _ExampleSimpleState extends State<ExampleSimple> with StatefulPropertyManager {
  StatefulAnimationProperty _anim1;
  StatefulAnimationProperty _anim2;
  StatefulAnimationProperty _anim3;

  <strong i="6">@override</strong>
  void initStatefulProperties({bool firstRun = false}) {
    _anim1 = initProperty(_anim1, StatefulAnimationProperty(duration: widget.duration1, playOnInit: true));
    _anim2 = initProperty(_anim2, StatefulAnimationProperty(duration: widget.duration2, playOnInit: true));
    _anim3 = initProperty(_anim3, StatefulAnimationProperty(duration: widget.duration3, playOnInit: true));
    super.initStatefulProperties(firstRun: firstRun);
  }

  <strong i="7">@override</strong>
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: _anim2.controller.value * 20, horizontal: _anim3.controller.value * 30,),
      color: Colors.red.withOpacity(_anim1.controller.value),
    );
  }
}

Некоторые заметки о различиях здесь:

  1. Сверху одна - 20 строк, другая - 45. Один - 1315 символов, другой - 825. В этом классе имеют значение только 3 строки и 200 символов (то, что происходит в сборке), так что это уже значительное улучшение сигнала. : коэффициент
  2. В стандартных вариантах есть несколько точек, где могут быть созданы ошибки. Забудьте утилизировать, или забудьте обработать didChange, или сделайте ошибку в didChange, и у вас есть ошибка в базе кода. Ситуация усугубляется, когда используются несколько типов контроллеров. Затем у вас есть отдельные функции, разрушающие объекты всех разных типов, которые не будут называться такими красивыми и последовательными, как это. Это становится беспорядочным, и довольно легко сделать ошибки или пропустить записи.
  3. Ванильный вариант не предоставляет метода для повторного использования общих шаблонов или логики, таких как playOnInit, поэтому мне приходится дублировать эту логику или создавать какую-то настраиваемую функцию в одном классе, который хочет использовать Animator.
  4. Здесь нет необходимости понимать SingleTickerProviderMixin, который является «волшебным» и на несколько месяцев запутывает то, что для меня представляет собой тикер (задним числом, я должен был просто прочитать класс, но в каждом руководстве просто говорится: добавьте этот волшебный миксин). Здесь вы можете посмотреть прямо на исходный код StatefulAnimationProperty и увидеть, как контроллеры анимации напрямую и в контексте используют провайдера тикера.

Вместо этого вы должны понимать, что делает StatefulPropertyManager, но, что важно, это изучено один раз и применяется к объектам любого типа, SingleTickerProviderMixin в основном предназначен для использования контроллеров Animator, и каждый контроллер может иметь собственное микширование, чтобы упростить использование, что становится беспорядочным. Наличие отдельных «StatefulObjects», которые знают все эти вещи (точно так же, как это делает конструктор!), Намного чище и лучше масштабируется.

Код StatefulAnimationProperty будет выглядеть примерно так:

class StatefulAnimationProperty extends BaseStatefulProperty<StatefulAnimationProperty> implements TickerProvider {
  final Duration duration;
  final TickerProvider vsync;
  final bool playOnInit;

  StatefulAnimationProperty({<strong i="7">@required</strong> this.duration, <strong i="8">@required</strong> this.vsync, this.playOnInit = false});

  AnimationController get controller => _controller;
  AnimationController _controller;

  Ticker _ticker;

  <strong i="9">@override</strong>
  Ticker createTicker(onTick) {
    _ticker ??= Ticker((elapsed)=>handleTick(elapsed, onTick));
    return _ticker;
  }

  handleTick(Duration elapsed, TickerCallback onTick) {
    managerWidget.buildWidget(); //TODO: This just calls setState() on the host widget. Is there some better way to do this?
    onTick(elapsed);
  }

  <strong i="10">@override</strong>
  void init(StatefulAnimationProperty old) {
    _controller = old?.controller ??
        AnimationController(
          duration: duration ?? Duration(seconds: 1),
          vsync: vsync ?? this,
        );
    if (playOnInit && old?.controller == null) {
      _controller.forward();
    }
    super.init(old);
  }

  <strong i="11">@override</strong>
  void update(StatefulAnimationProperty old) {
    if (duration != old.duration) {
      _controller.duration = duration;
    }
    if (vsync != old.vsync) {
      _controller.resync(vsync);
    }
    super.update(old);
  }

  <strong i="12">@override</strong>
  void dispose() {
    _controller?.dispose();
    _ticker?.dispose();
    super.dispose();
  }
}

Наконец, стоит отметить, что читаемость можно улучшить с помощью расширений, поэтому у нас может получиться что-то вроде:

  void initStatefulProperties({bool firstRun = false}) {
    _anim1.init(duration: widget.duration1, playOnInit: true);
    _anim2.init(duration: widget.duration2, playOnInit: true);
    _anim3.init(duration: widget.duration3, playOnInit: true);
    super.initStatefulProperties(firstRun: firstRun);
  }

[Edit] Как бы я хотел сказать, мой ванильный пример содержит ошибку. Я забыл передать правильную продолжительность каждому аниматору в didUpdateWidget. Сколько времени понадобилось бы нам, чтобы найти эту ошибку в природе, если бы никто не заметил этого при проверке кода? Неужели никто не заметил при чтении? Оставим это, потому что это прекрасный пример того, что происходит в реальном мире.

Вот вид с высоты птичьего полета, шаблон отмечен красным:
image

Это было бы не так уж плохо, если бы это был чистый шаблон, а компилятор кричал на вас, если бы он отсутствовал. Но это все необязательно! А если опущено, создает ошибки. Так что это на самом деле очень плохая практика и совсем не СУХАЯ. Здесь на помощь приходят конструкторы, но они годятся только для упрощенных вариантов использования.

Что мне кажется очень интересным в этом, так это то, как сотня строк и простой миксин в State делают множество существующих классов избыточными. Например, сейчас практически нет необходимости когда-либо использовать TickerProviderMixins. TweenAnimationBuilder почти никогда не нужно использовать, если только вы на самом деле не _ хотите_ создать подконтекст. Многие традиционные болевые точки, такие как управление контроллерами фокуса и контроллерами ввода текста, значительно упрощены. Использование Streams становится более привлекательным и менее запутанным. По всей кодовой базе использование Builders может быть сокращено в целом, что приведет к более легкому грокированию деревьев.

Также упрощает _extremely_ создание ваших собственных объектов пользовательского состояния, таких как пример FetchUser, перечисленный ранее, который в настоящее время в основном требует построителя.

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

Было бы хорошее начало. Разделите эту проблему на разные части / вопросы и посмотрите, действительно ли это проблема, которую разработчики Flutter хотят решить.

Как только это станет ясно, этот разговор станет более плавным и обогащающим.

Реакция на эмодзи под каждым комментарием дает четкое представление о том, видит ли сообщество в этом проблему или нет. Мнение разработчиков, прочитавших 250+ длинных комментариев по этой проблеме, много значит imho.

@esDotDev Это похоже на некоторые из идей, с которыми я играл, хотя мне нравится ваша идея о том, чтобы само свойство было просто поставщиком тикера, я не думал об этом. Одна вещь, которая отсутствует в вашей реализации, что, я думаю, нам нужно добавить, - это обработка TickerMode (которая является точкой TickerProviderStateMixin).

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

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

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

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

Хм, я думаю, что весь смысл свойств предназначен для невизуальных объектов. Если что-то хочет иметь контекстный слот в дереве, то это должно быть конструктором (на самом деле, я думаю, это единственное, что теперь должно быть построителем?)

Таким образом, у нас будет StatefulValueListenableProperty, который мы используем большую часть времени, когда просто хотим привязать все представление. Затем у нас также есть ValueListenableBuilder на случай, если мы хотим, чтобы какой-то подраздел нашего дерева был перестроен.

Это также решает проблему вложенности, поскольку использование построителя в качестве конечного узла не так сильно мешает читаемости, как вложение 2 или 3 в верхней части дерева виджетов.

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

@Hixie
А что насчет этого для строителей?

class _ExampleSimpleState extends State<ExampleSimple> with StatefulPropertyManager {
  StatefulAnimationProperty _anim1;
  StatefulAnimationBuilderProperty _anim2;

  <strong i="7">@override</strong>
  void initStatefulProperties({bool firstRun = false}) {
    _anim1 = initProperty(_anim1, StatefulAnimationProperty(duration: widget.duration1, playOnInit: true));
    _anim2 = initProperty(_anim3, StatefulAnimationBuilderProperty(duration: widget.duration3, playOnInit: true));
    super.initStatefulProperties(firstRun: firstRun);
  }

  <strong i="8">@override</strong>
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red.withOpacity(_anim1.controller.value),
      child: _anim2(child: SomeChildWidget()),
    );
  }
}

Вы можете уточнить? Я не уверен, что понимаю предложение.

Я думаю, он говорит, что StatefulProperty может предоставить дополнительный метод сборки для свойств, которые имеют некоторый визуальный компонент:

return Column(
   children: [
      TopContent(),
      _valueProperty.build(SomeChildWidget()),
   ]
)

Что довольно imo,

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

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

Предположим, у вас есть такой код:

Widget build(BuildContext context) {
  return ExpensiveParent(
    child: ValueListenableBuilder(
      valueListenable: foo,
      child: ExpensiveChild(),
      builder: (BuildContext context, value, Widget child) {
        return SomethingInTheMiddle(
          value: value,
          child: child,
        );
      }
    ),
  );
}

... как бы вы это преобразовали?

@esDotDev Мне нравится ваша идея о том, что само свойство должно быть поставщиком тикера, я не думал об этом.

Один из самых сильных аспектов этого подхода, основанного на крючке, заключается в том, что вы можете _ полностью_ инкапсулировать логику с отслеживанием состояния, какой бы она ни была. Итак, в этом случае полный список для AC:

  1. Создать AC
  2. Дайте ему тикер
  3. Обработка изменений виджета
  4. Обработка очистки AC и тикера
  5. Перестроить представление по галочке

В настоящее время все разделено: разработчики обрабатывают (или не обрабатывают) 1,3,4 вручную и повторно, а полумагический SingleTickerProviderMixin заботится о 2 и 5 (мы передаем this как vsync, что сбивало меня с толку на несколько месяцев! ). И сам SingleTickerProviderMixin явно является попыткой исправить этот тип проблемы, иначе почему бы просто не пойти полностью и не реализовать TickerProvider для каждого класса, это было бы намного яснее.

Предположим, у вас есть такой код:

Widget build(BuildContext context) {
  return ExpensiveParent(
    child: ValueListenableBuilder(
      valueListenable: foo,
      child: ExpensiveChild(),
      builder: (BuildContext context, value, Widget child) {
        return SomethingInTheMiddle(
          value: value,
          child: child,
        );
      }
    ),
  );
}

... как бы вы это преобразовали?

class _ExampleSimpleState extends State<ExampleSimple> with StatefulPropertyManager {
  StatefulValueListenableBuilder _fooBuilder;

  <strong i="10">@override</strong>
  void initStatefulProperties({bool firstRun = false}) {
    _fooBuilder = initProperty(StatefulValueListenableProperty(valueListenable: widget.foo)); 
    super.initStatefulProperties(firstRun: firstRun);
  }

  <strong i="11">@override</strong>
  Widget build(BuildContext context) {
    return ExpensiveParent(
      child: SomethingInTheMiddle(
        _fooBuilder.value,
        _fooBuilder.builder(childBuilder: () => ExpensiveChild()),
      ),
    );
  }
}

@Hixie
Спасибо за пример. Я старался изо всех сил. Я мог что-то упустить.

Важно отметить, что конструктор кэширует ребенка. Вопрос будет в том, когда действительно нужно перестраивать ребенка? Я думаю, это был вопрос, который вы пытались поднять ...

@Hixie , вы видели https://github.com/flutter/flutter/issues/51752#issuecomment -671104377
Думаю, есть несколько действительно хороших моментов.
Сегодня я создаю что-то с большим количеством ValueListenableBuilder и могу только сказать, что это неприятно читать.

@Hixie
Спасибо за пример. Я старался изо всех сил. Я мог что-то упустить.

Я не думаю, что это сработает, потому что Property привязывается к состоянию, в котором оно определено, поэтому здесь всегда перестраивается ExudgetParent. Тогда я думаю, что кеширование дочернего элемента также проблематично, поскольку в примере Builder он знал бы, что нужно перестраивать дочерний элемент только тогда, когда создается родительское состояние, но в этом методе Property не знает, когда сделать его кеш недействительным (но, возможно, это разрешимо?)

Но, честно говоря, это идеальный вариант использования для разработчиков, когда вы хотите ввести новый контекст. Я считаю довольно элегантным просто иметь контекст StatefulProperties (чистое состояние) и StatefulWidgets (сочетание состояния и макета).

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

@escamoteur@sahandevs , написавшие этот комментарий) да, я изучал это раньше. Я думаю, это определенно помогает показать логику, от которой люди хотят избавиться. Я думаю, однако, что сам пример несколько сомнительный, поскольку я ожидал бы, что большая часть логики (например, все, что связано с кешированием) будет в бизнес-логике состояния приложения, а не рядом с виджетами. Я также не вижу хорошего способа получить такой краткий синтаксис, как предлагается в этом комментарии, без прерывания горячей перезагрузки (например, если вы измените количество используемых хуков, неясно, как они могут сохраняться с сохранением состояния при перезагрузке ).

Тем не менее, я думаю, что работа @esDotDev и @TimWhiting, показанная выше, очень интересна и может решить эти проблемы. Он не такой короткий, как Hooks, но более надежный. Я думаю, что было бы разумно упаковать что-то подобное, это могло бы даже стать фаворитом Flutter, если оно хорошо работает. Я не уверен, что это имеет смысл в качестве основной функции фреймворка, потому что улучшение не столь существенно, если принять во внимание сложность построения свойств и влияние на производительность, а также то, как разные люди предпочтут разные стили. В конце концов, для разных людей должно быть нормально использовать разные стили, но мы бы не хотели, чтобы в базовой структуре было несколько стилей, это просто вводит в заблуждение новых разработчиков.

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

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

Хуки без проблем работают с горячей перезагрузкой.
Первая ловушка с несовпадающим runtimeType приводит к уничтожению всех последующих ловушек.

Это поддерживает добавление, удаление и изменение порядка.

Я думаю, что есть аргумент, что полная абстракция предпочтительнее частичной, которая существует в настоящее время.

Если я хочу понять, как Animator работает в контексте свойства, я либо полностью его игнорирую, либо вмешиваюсь, и все в порядке, самодостаточно и связно.

Если я хочу понять, как AnimatorController работает в контексте StatefulWidget, мне нужно (я вынужден) понимать основные крючки жизненного цикла, но тогда я избавляюсь от понимания того, как работает базовый механизм тиков. В каком-то смысле это худшее из обоих миров. Недостаточно волшебства, чтобы заставить его «просто работать», но достаточно, чтобы сбить с толку новых пользователей и заставить их слепо доверять некоторому миксину (который сам по себе является новой концепцией для большинства) и волшебному свойству vsync.

Я не уверен в других примерах в базе кода, но это применимо к любой ситуации, когда для StatefulWidget были предоставлены некоторые вспомогательные миксины, но есть еще несколько других вариантов начальной загрузки, которые необходимо выполнять всегда. Разработчики изучат начальную загрузку (скучная часть) и проигнорируют Mixin (интересный / сложный бит).

Тем не менее, я думаю, что работа @esDotDev и @TimWhiting, показанная выше, очень интересна и может решить эти проблемы. Он не такой короткий, как Hooks, но более надежный

Насколько это надежнее?

Мы по-прежнему не можем создавать / обновлять свойства условно или вне его жизненного цикла, так как мы можем войти в плохое состояние. Например, условный вызов свойства не приведет к удалению свойства, если условие ложно.
И все свойства по-прежнему пересматриваются при каждой перестройке.

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

Например, что, если кто-то прочитает свойство внутри didUpdateWidget ?

  • Выполнялся ли initProperties до жизненного цикла? Но тогда это означает, что нам, возможно, придется обновлять свойства несколько раз за сборку.
  • Выполнилось ли initProperties после didUpdateWidget? Тогда использование свойств внутри didUpdateWidget может привести к устареванию состояния

Итак, в конце концов, у нас есть все проблемы с хуками, но:

  • мы не можем использовать свойства внутри `StatelessWidget. Так что читабельность StreamBuilder / ValueListenableBuilder / ... все еще остается проблемой, и это было главной проблемой.
  • есть множество крайних случаев
  • сложнее создавать настраиваемые свойства (мы не можем просто извлечь кучу свойств в функцию)
  • труднее оптимизировать перестройки

В конце концов, приведенный пример по поведению не отличается от:

class Example extends StatelessWidget {
  <strong i="29">@override</strong>
  Widget build(context) {
    final value1 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));
    final value2 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));
    final value3 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));

    return Container(
     margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30),
     color: Colors.red.withOpacity(value1),
      child: _anim2(child: SomeChildWidget()),
    );
  }
}

Но этот синтаксис поддерживает гораздо больше вещей, например:

Раннее возвращение:

class Example extends StatelessWidget {
  <strong i="34">@override</strong>
  Widget build(context) {
    final value1 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));

    if (condition) {
      return Container();
    }

    final value2 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));

    ...
  }
}

который будет избавляться от value2 когда condition переключается на false

Извлечение пакетов сборщиков в функцию:

Widget build(context) {
  final foo = keyword FooBuilder();
  final bar = keyword BarBuilder();

  return Text('$foo $bar');
}

можно изменить на:

Builder<String> LabelBuilder() builder* {
  final foo = keyword FooBuilder();
  final bar = keyword BarBuilder();

  return '$foo $bar';
}

Widget build(context) {
  final label = keyword LabelBuilder();

  return Text(label);
}

Оптимизировать перестройки

Параметр child все еще возможен:

Widget build(context) {
  final value = keyword StreamBuilder();

  return Builder(
    builder: (context, child) {
      final value2 = keyword TweenAnimationBuilder();
      final value = keyword ValueListenableBuilder();

      return Whatever(child: child);
    },
    child: ExpensiveChild()
  );
}

Как часть языка, мы могли бы даже иметь для этого синтаксический сахар:

Widget build(context) {
  return Scaffold(
    body: {
      final value = keyword TweenAnimationBuilder();
      final value2 = keyword ValueListenableBuilder();

      return Text();
    },
  );
}

Бонус: в качестве языковой функции поддерживаются условные вызовы

В рамках языка мы можем поддерживать такой сценарий:

Widget build(context) {
  String label;

  if (condition) {
    label = keyword LabelBuilder();
  } else {
    label = keyword AnotherBuilder();
  }

  final value2 = keyword WhateverBuilder();

  return ...
}

Это не очень полезно, но поддерживается - поскольку синтаксис скомпилирован, он может различать каждое использование keyword , полагаясь на метаданные, которые в противном случае недоступны.

Что касается удобочитаемости построителей, вот предыдущий пример, но сделанный с помощью построителей. Он решает все потребности в надежности и использовании кода, но посмотрите, что он сделал с моим бедным деревом виджетов: '(

class _ExampleSimpleBuilderState extends State<ExampleSimpleBuilder> {
  <strong i="6">@override</strong>
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
        tween: Tween(begin: 0, end: 1),
        duration: widget.duration1,
        builder: (_, value1, __) {
          return TweenAnimationBuilder<double>(
              tween: Tween(begin: 0, end: 1),
              duration: widget.duration2,
              builder: (_, value2, __) {
                return TweenAnimationBuilder<double>(
                    tween: Tween(begin: 0, end: 1),
                    duration: widget.duration3,
                    builder: (_, value3, __) {
                      return Container(
                        margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30),
                        color: Colors.red.withOpacity(value1),
                      );
                    });
              });
        });
  }
}

Намного труднее (по крайней мере, для моего глаза) определить код, который имеет значение. Кроме того, fwiw, когда я писал это, мне приходилось начинать заново примерно 3 раза, так как я постоянно запутывался относительно того, какой кронштейн где принадлежит, куда должны идти мои полуколаны и т. Д. Вложенные конструкторы просто неинтересны, чтобы писать или работать внутри. Одна неправильная точка с запятой и dartfmt полностью разрушают все.

Насколько это надежнее?

Это прекрасный пример того, почему этот _должен_ быть основным плагином imo. Здесь требуется глубокое знание предметной области. У меня есть знания сценариев для реализации простой системы кэширования, подобной этой, у меня даже нет близких знаний предметной области, чтобы знать каждый крайний случай, который может произойти, или плохие состояния, в которые мы можем попасть. Помимо Реми, я думаю, что в мире есть около 4 разработчиков, не входящих в команду Flutter, которые разбираются в этом материале! (явно преувеличивая).

Проблема поддержки виджетов без сохранения состояния - хорошая. С одной стороны, я понимаю, что StatefulWidgets странно многословны. С другой стороны, здесь мы действительно говорим о чистом многословии. Нет никаких ошибок, которые могут возникнуть из-за необходимости определять 2 класса, вы не можете это испортить, компилятор не позволяет вам, никогда нет ничего интересного, что я хотел бы сделать в StatelessWidget. Так что я не уверен, что это серьезная проблема ... ОПРЕДЕЛЕННО было бы неплохо иметь, но это последние 5%, имо, а не то, на чем можно застрять.

С другой стороны ... этот синтаксис remi с поддержкой ключевых слов абсолютно красивый и безумно гибкий / мощный. И если он дает вам бесплатную поддержку StatelessWidget, то это просто лишнее 🔥

ИМО, поддержка StatelessWidget - это большое дело. Необязательно, но все равно очень круто.

Хотя я согласен с тем, что это не критично, люди уже борются за использование функций вместо StatelessWidget.
Требование от людей использовать StatefulWidget для использования Builders (поскольку у большинства Builders, вероятно, будет эквивалент Property) только усугубит конфликт.

Не только это, но в мире, где мы можем создавать функции высшего порядка в dart (https://github.com/dart-lang/language/issues/418), мы могли бы полностью избавиться от классов:

<strong i="9">@StatelessWidget</strong>
Widget Example(BuildContext context, {Key key, String param}) {
  final value = keyword StreamBuilder();

  return Text('$value');
}

затем используется как:

Widget build(context) {
  // BuildContext and Key are automatically injected
  return Example(param: 'hello');
}

Это то, что поддерживается function_widget - генератором кода, в котором вы пишете функцию, и он генерирует для вас класс, - который также поддерживает HookWidget .

Разница в том, что поддержка функций высшего порядка в Dart устранит необходимость в генерации кода для поддержки такого синтаксиса.

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

Но и ваше предложение с ключевым словом тоже. Я думаю, что аргументы в пользу нового ключевого слова довольно сильны:

  • Более гибкий и компоновочный, чем прививка на State
  • Еще более лаконичный синтаксис
  • Работает без сохранения состояния, что очень удобно.

Что мне не нравится в этом, так это то, что мы беспокоимся о стоимости установки свойств для некоторого простого объекта несколько раз / сборки, но затем отстаиваем решение, которое в основном создаст миллион уровней контекста и кучу затрат на макет. Я не понимаю?

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

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

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

Это не проблема

Точно так же, если пользовательские ошибки недопустимы, тогда, как я упоминал ранее, Property страдает точно такой же проблемой.
Мы не можем разумно написать:

Property property;

<strong i="12">@override</strong>
void initProperties() {
  if (condition) {
    property = init(property, MyProperty());
  }
}

поскольку переключение condition с true на false не уничтожит свойство.

Мы также не можем вызвать это в цикле. На самом деле это не имеет смысла, так как это разовое задание. Каков вариант использования свойства в цикле?

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

Property first;
Property second;

<strong i="20">@override</strong>
void initProperties() {
  // The state of first depends on second, but second is updated after first
  // So we could end up in a bad state, similar to how the build method of a Widget should depend
  // on the context.size
  first = init(property, MyProperty(second?.value));

  second = init(property, Whatever());
}
> class _ExampleSimpleBuilderState extends State<ExampleSimpleBuilder> {
>   <strong i="5">@override</strong>
>   Widget build(BuildContext context) {
>     return TweenAnimationBuilder<double>(
>         tween: Tween(begin: 0, end: 1),
>         duration: widget.duration1,
>         builder: (_, value1, __) {
>           return TweenAnimationBuilder<double>(
>               tween: Tween(begin: 0, end: 1),
>               duration: widget.duration2,
>               builder: (_, value2, __) {
>                 return TweenAnimationBuilder<double>(
>                     tween: Tween(begin: 0, end: 1),
>                     duration: widget.duration3,
>                     builder: (_, value3, __) {
>                       return Container(
>                         margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30),
>                         color: Colors.red.withOpacity(value1),
>                       );
>                     });
>               });
>         });
>   }
> }

Это такой странный пример. Вы уверены, что AnimatedContainer еще не может этого сделать?

Конечно. Пример здесь - использование 3 анимаций в каком-то виджете для выполнения «X». X намеренно упрощен в примере, чтобы выделить количество шаблонов.

Не зацикливайтесь на том, как я их использую. В реальном примере «ядро» виджета будет состоять из сотни строк или около того, анимированные свойства не будут такими простыми, и у нас будет определено несколько обработчиков и других функций. Предположим, я делаю что-то, что не обрабатывается одним из неявных виджетов (несложно, поскольку кроме AnimatedContainer они исключительно одноцелевые).

Дело в том, что при создании чего-то вроде этого конструкторы не работают хорошо, поскольку они с самого начала вставляют вас в дыру для удобочитаемости (и возможности записи), как таковые они хорошо подходят для простых случаев использования, они не "компоновка" хорошо. Составьте композицию из двух или более вещей.

Не зацикливайтесь на том, как я их использую. В реальном примере ...

... и обратно на круги своя. Почему бы тебе не привести реальный пример?

Вам нужен реальный пример использования сложной анимации?
https://github.com/gskinnerTeam/flutter_vignettes

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

Конечно. Пример здесь - использование 3 анимаций в каком-то виджете для выполнения «X». X намеренно упрощен в примере, чтобы выделить количество шаблонов.

"ядро" виджета будет сто строк или что-то в этом роде

В другом посте вы опубликовали пример с шаблоном, который скрывает «ядро», но теперь вы говорите нам, что ядро ​​будет состоять из сотен строк? Значит, на самом деле шаблон будет крохотным по сравнению с ядром? Вы не можете получить и то, и другое.
Вы постоянно меняете свои аргументы.

Не зацикливайтесь на том, как я их использую. В реальном примере ...

... и обратно на круги своя. Почему бы тебе не привести реальный пример?

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

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

Думаю, я сказал, что было бы глупо иметь класс, состоящий из 100 строк, где половина его является шаблоном. Это именно то, что я здесь описываю. Ядро, каким бы большим оно ни было, не должно запутываться из-за шума, который, безусловно, возникает при использовании нескольких компоновщиков.

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

В другом посте вы опубликовали пример с шаблоном, который скрывает «ядро», но теперь вы говорите нам, что ядро ​​будет состоять из сотен строк? Значит, на самом деле шаблон будет крохотным по сравнению с ядром? Вы не можете получить и то, и другое.
Вы постоянно меняете свои аргументы.

Опять же, эта проблема не в шаблоне, а в удобочитаемости и возможности повторного использования.
Неважно, есть ли у нас 100 строк.
Важно то, насколько эти строки доступны для чтения / сопровождения / повторного использования.

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

Вам нужен реальный пример использования сложной анимации?
https://github.com/gskinnerTeam/flutter_vignettes

Конечно, вы не можете ожидать, что я буду копаться в вашем проекте. Какой именно файл мне следует посмотреть?

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

С точностью до наоборот. Показаны некоторые сколь угодно сложную анимацию , которая не может быть решена любым существующим решением будет пример, и это то, что Хикси продолжает спрашивать, я считаю.

Просто отсканируйте гифки и начните представлять, как вы могли бы создать что-то из этого. Это репо на самом деле 17 автономных приложений. Вы также не можете ожидать, что я напишу вам произвольную анимацию, чтобы доказать вам, что сложные анимации могут существовать. Я создавал их 20 лет, начиная с Flash, и все они отличаются от предыдущих. И в любом случае это не относится к анимациям, это просто самый простой и знакомый API, чтобы проиллюстрировать более крупную точку зрения.

Как вы знаете, когда вы используете аниматор, есть примерно 6 вещей, которые вам нужно делать каждый раз, но для этого также нужны хуки жизненного цикла ?? Хорошо, теперь расширите это до ВСЕГО, в котором есть 6 шагов, которые вы должны делать каждый раз ... И вам нужно использовать это в 2-х местах. Или нужно использовать сразу 3 штуки. Это настолько очевидная проблема на первый взгляд, что я не знаю, что еще я могу добавить, чтобы объяснить это.

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

__Все__? Значит производительность, ремонтопригодность не актуальны?

Наступает момент, когда «автоматизация» труда и «выполнение» работы - одно и то же.

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

@TimWhiting, не репозиторий https://github.com/TimWhiting/local_widget_state_approaches ? Некоторые люди не могут внести свой вклад без соответствующей лицензии (в идеале BSD, MIT или аналогичные).

почему я, пользователь, должен терпеть такой шаблон в любом случае, учитывая достаточно эквивалентный способ выражения того же самого?

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

Просто отсканируйте гифки и начните представлять, как вы могли бы создать что-то из этого.

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

Как бы то ни было, эта дискуссия, похоже, выходит из-под контроля, и возникает больше личных разногласий. Мы должны сосредоточиться на основной задаче. Я не уверен, как сторонники крючка могут показывать более мелкие примеры, если оппоненты, как я сказал ранее, найдут улучшения, которые на самом деле не решают поставленную проблему. Я думаю, что мы должны сейчас внести свой вклад в репозиторий @TimWhiting .

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

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

Любой запрос на предоставление чего-то, что сегодня невозможно, не по теме.

@TimWhiting, не репозиторий https://github.com/TimWhiting/local_widget_state_approaches ? Некоторые люди не могут внести свой вклад без соответствующей лицензии (в идеале BSD, MIT или аналогичные).

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

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

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

Любой запрос на предоставление чего-то, что сегодня невозможно, не по теме.

Позвольте мне перефразировать то, что я сказал.

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

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

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

@TimWhiting, не репозиторий https://github.com/TimWhiting/local_widget_state_approaches ? Некоторые люди не могут внести свой вклад без соответствующей лицензии (в идеале BSD, MIT или аналогичные).

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

Запрос состоит в том, чтобы превратить это в нечто, что не может быть обработано AnimatedContainer:

Container(margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30), color: Colors.red.withOpacity(value1));

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

Я думаю, что предоставил отличные кананонические примеры проблемы как с построителями, так и с повторным использованием ванильного состояния:
https://github.com/flutter/flutter/issues/51752#issuecomment -671566814
https://github.com/flutter/flutter/issues/51752#issuecomment -671489384

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

Не могли бы вы, @Rudiksz , @esDotDev и которые показывают, как вложение затрудняет чтение кода ? Чего им не хватает? Я полагаю, что там нет показателей производительности, но @rrousselGit уверен, что они не менее производительны, чем конструкторы.

@esDotDev Я думаю, что суть в том, чтобы иметь уникальный канонический пример, с которым можно сравнивать все решения по управлению жизненным циклом (не только хуки, но и другие в будущем). Это тот же принцип, что и TodoMVC, вам не обязательно указывать на различные другие реализации в React, Vue, Svelte и т. Д., Показывая разницу между ними, вы бы хотели, чтобы все они реализуют одно и то же приложение, _ тогда_ вы можете сравнить.

Для меня это имеет смысл, но я не понимаю, почему он должен быть больше одной страницы.

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

Конечно, в репозитории @TimWhiting нет полноценного приложения, у него есть отдельные страницы в качестве примеров, как вы говорите, если вы можете сделать канонический пример анимации для этого репозитория, из которого другие могли бы реализовать свое решение, это сработает.

Я также не думаю, что нам нужно огромное приложение или что-то в этом роде, но оно должно быть достаточно сложным, как у TodoMVC. По сути, этого должно быть достаточно, чтобы ваши оппоненты не могли сказать: «Хорошо, я мог бы сделать это лучше таким-то и таким-то образом».

@Hixie Запрос реальных приложений для сравнения подходов ошибочен.

Есть два недостатка:

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

Например, мы не можем написать приложение, используя:

final snapshot = keyword StreamBuilder();

поскольку это не реализовано.

Мы также не можем судить о производительности, поскольку это сравнение POC и производственного кода.

Мы не можем оценить, подвержено ли ошибкам что-то вроде «хуки не могут быть вызваны условно», поскольку нет интеграции с компилятором, чтобы указать на ошибки при неправильном использовании.

Оценка производительности проектов, оценка удобства использования API, реализация вещей до того, как у нас появятся реализации ... все это часть дизайна API. Добро пожаловать на мою работу. :-) (Мелочи Flutter: знаете ли вы, что первые несколько тысяч строк RenderObject и RenderBox и др. Были реализованы до того, как мы создали dart: ui?)

Это не отменяет того факта, что вы требуете невозможного.

Некоторые из сделанных здесь предложений являются частью языка или анализатора. Сообщество не может реализовать это.

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

Я собрал пример «сложного» сценария анимации, который хорошо использует 3 анимации, и он довольно загружен шаблонным и беспорядочным.

Важно отметить, что я мог бы просто сделать любую анимацию, для которой требуется жесткая привязка к исходной позиции (исключая все неявные виджеты), или поворот по оси z, или масштабирование по одной оси, или любой другой вариант использования, не охватываемый IW. Я волновался, что это могут не восприниматься всерьез (хотя мои дизайнеры будут передавать мне эти вещи весь день), поэтому я построил что-то более «реальное».

Итак, вот простые строительные леса, у них есть 3 панели, которые открываются и закрываются. Он использует 3 аниматора с дискретными состояниями. В этом случае мне действительно не нужен полный контроль над AnimatorController, TweenAnimationBuilder подойдет, но результирующее вложение в мое дерево было бы очень нежелательным. Я не могу вложить TAB в дерево, так как панели зависят от других значений. AnimatedContainer здесь не подходит, поскольку каждая панель должна соскользнуть с экрана, они не «сжимаются».
https://i.imgur.com/BW6M3uM.gif
image

class _SlidingPanelViewState extends State<SlidingPanelView> with TickerProviderStateMixin {
  AnimationController leftMenuAnim;
  AnimationController btmMenuAnim;
  AnimationController rightMenuAnim;

  <strong i="12">@override</strong>
  void initState() {
    // Here I have to pass vsync to AnimationController, so I have to include a SingleTickerProviderMixin and somewhat magically pass 'this' as vsync.
    leftMenuAnim = AnimationController(duration: widget.slideDuration, vsync: this);
    btmMenuAnim = AnimationController(duration: widget.slideDuration, vsync: this);
    rightMenuAnim = AnimationController(duration: widget.slideDuration, vsync: this);
    // Here I have to call forward 3 times, cause there's no way to automate this common setup behavior
    leftMenuAnim.forward();
    btmMenuAnim.forward();
    rightMenuAnim.forward();
    // Here I have to manually bind to build, cause there is encapsulate this common setup behavior
    leftMenuAnim.addListener(() => setState(() {}));
    btmMenuAnim.addListener(() => setState(() {}));
    rightMenuAnim.addListener(() => setState(() {}));
    super.initState();
  }

  // Following 2 fxn are a blind spot as far as compiler is concerned.
  // Things may, or may not be implemented correctly, no warnings, no errors.
  <strong i="13">@override</strong>
  void dispose() {
    btmMenuAnim.dispose();
    leftMenuAnim.dispose();
    rightMenuAnim.dispose();
    super.dispose();
  }

  <strong i="14">@override</strong>
  void didUpdateWidget(SlidingPanelView oldWidget) {
    if (leftMenuAnim.duration != widget.slideDuration) {
      leftMenuAnim.duration = widget.slideDuration;
      btmMenuAnim.duration = widget.slideDuration;
      rightMenuAnim.duration = widget.slideDuration;
    }
    super.didUpdateWidget(oldWidget);
  }

  // End error-prone blind spot without a single line of unique code
  // ~50 lines in we can start to see some unique code

  void _toggleMenu(AnimationController anim) {
    bool isOpen = anim.status == AnimationStatus.forward || anim.value == 1;
    isOpen ? anim.reverse() : anim.forward();
  }

  <strong i="15">@override</strong>
  Widget build(BuildContext context) {
    double leftPanelSize = 320;
    double leftPanelPos = -leftPanelSize * (1 - leftMenuAnim.value);
    double rightPanelSize = 230;
    double rightPanelPos = -rightPanelSize * (1 - rightMenuAnim.value);
    double bottomPanelSize = 80;
    double bottomPanelPos = -bottomPanelSize * (1 - btmMenuAnim.value);
    return Stack(
      children: [
        //Bg
        Container(color: Colors.white),
        //Content Panel
        Positioned(
          top: 0,
          left: leftPanelPos + leftPanelSize,
          bottom: bottomPanelPos + bottomPanelSize,
          right: rightPanelPos + rightPanelSize,
          child: ChannelInfoView(),
        ),
        //Left Panel
        Positioned(
          top: 0,
          left: leftPanelPos,
          bottom: bottomPanelPos + bottomPanelSize,
          width: leftPanelSize,
          child: ChannelMenu(),
        ),
        //Bottom Panel
        Positioned(
          left: 0,
          right: 0,
          bottom: bottomPanelPos,
          height: bottomPanelSize,
          child: NotificationsBar(),
        ),
        //Right Panel
        Positioned(
          top: 0,
          right: rightPanelPos,
          bottom: bottomPanelPos + bottomPanelSize,
          width: rightPanelSize,
          child: SettingsMenu(),
        ),
        // Buttons
        Row(
          children: [
            Button("left", ()=>_toggleMenu(leftMenuAnim)),
            Button("btm", ()=>_toggleMenu(btmMenuAnim)),
            Button("right", ()=>_toggleMenu(rightMenuAnim)),
          ],
        )
      ],
    );
  }
}

//Demo helpers
Widget Button(String lbl, VoidCallback action) => FlatButton(child: Text(lbl), onPressed: action, color: Colors.grey);
Widget ChannelInfoView() => _buildPanel(Colors.red);
Widget ChannelMenu() => _buildPanel(Colors.pink);
Widget SettingsMenu() => _buildPanel(Colors.blue);
Widget NotificationsBar() => _buildPanel(Colors.grey);
Widget _buildPanel(Color c) => Container(color: c, child: Container(color: Colors.white.withOpacity(.5)), padding: EdgeInsets.all(10));

Итак, из 100 или около того строк в этом теле примерно 40% - это чистый шаблон. В частности, 15 строк, в которых что-то пропущено или неправильно набрано, может вызвать труднодоступные ошибки.

Если мы используем что-то вроде StatefulProperty, это уменьшит шаблон до 15% или около того (сэкономит около 25 строк). Крайне важно, что это полностью решит проблему скрытых ошибок и дублирования бизнес-логики, но это все еще немного многословно, особенно потому, что для этого требуется StatefulWidget, который сразу занимает 10 строк.

Если мы используем что-то вроде ключевого слова, мы сокращаем стандартные строки практически до 0%. Все внимание класса может быть уделено (уникальной) бизнес-логике и элементам визуального дерева. В целом мы делаем использование StatefulWIdgets гораздо реже, подавляющее большинство представлений становится на 10 или 20% менее подробным и более сфокусированным.

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

Мы создали специальный виджет SlidingPanel, который принимает свойство IsOpen и открывается и закрывается. Как правило, это решение в каждом из этих случаев использования, когда вам нужно какое-то конкретное поведение, вы перемещаете логику с отслеживанием состояния вниз в какой-то сверхспецифический виджет и используете его. Вы в основном пишете свой собственный ImplicitlyAnimatedWidget.

Обычно это работает нормально, но это все еще время и усилия, и буквально ЕДИНСТВЕННАЯ причина, по которой это существует, заключается в том, что использование анимаций настолько сложно (что существует, потому что повторное использование компонентов с отслеживанием состояния в целом так сложно). В Unity или AIR, например, я бы не создавал специальный класс просто для перемещения панели по одной оси, это была бы просто одна строка кода, которую можно было бы открыть или закрыть, выделенному виджету нечего было бы делать. Во Flutter мы должны создать специальный виджет, потому что это буквально единственный разумный способ инкапсулировать загрузку и удаление AnimatorController (если мы не хотим вкладывать, вкладывать, вкладывать с помощью TAB)

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

Для полноты, вот тот же вариант использования, сделанный со строителями. Количество строк сильно сокращено, нет никаких шансов для ошибок, нет необходимости изучать чужую концепцию RE TickerProviderMixin ... но эта вложенность - печаль, и то, как различные переменные разбросаны по всему дереву (динамические конечные значения, значение1, значение2 и т. Д. ) делает бизнес-логику более трудной для чтения, чем это должно быть.

дротик
class _SlidingPanelViewState расширяет состояние{
bool isLeftMenuOpen = true;
bool isRightMenuOpen = true;
bool isBtmMenuOpen = true;

@override
Сборка виджета (контекст BuildContext) {
return TweenAnimationBuilder(
tween: Tween (начало: 0, конец: isLeftMenuOpen? 1: 0),
duration: widget.slideDuration,
строитель: (_, leftAnimValue, __) {
return TweenAnimationBuilder(
tween: Tween (начало: 0, конец: isRightMenuOpen? 1: 0),
duration: widget.slideDuration,
строитель: (_, rightAnimValue, __) {
return TweenAnimationBuilder(
tween: Tween (начало: 0, конец: isBtmMenuOpen? 1: 0),
duration: widget.slideDuration,
строитель: (_, btmAnimValue, __) {
двойной leftPanelSize = 320;
двойной leftPanelPos = -leftPanelSize * (1 - leftAnimValue);
двойной rightPanelSize = 230;
двойной rightPanelPos = -rightPanelSize * (1 - rightAnimValue);
double bottomPanelSize = 80;
double bottomPanelPos = -bottomPanelSize * (1 - btmAnimValue);
вернуть стек (
дети: [
// Bg
Контейнер (цвет: Цвета. Белый),
// Область основного содержимого
Позиционировано (
верх: 0,
слева: leftPanelPos + leftPanelSize,
bottom: bottomPanelPos + bottomPanelSize,
справа: rightPanelPos + rightPanelSize,
дочерний: ChannelInfoView (),
),
// Левая панель
Позиционировано (
верх: 0,
слева: leftPanelPos,
bottom: bottomPanelPos + bottomPanelSize,
ширина: leftPanelSize,
дочерний: ChannelMenu (),
),
//Нижняя панель
Позиционировано (
осталось: 0,
справа: 0,
внизу: bottomPanelPos,
высота: bottomPanelSize,
ребенок: NotificationsBar (),
),
// Правая панель
Позиционировано (
верх: 0,
справа: rightPanelPos,
bottom: bottomPanelPos + bottomPanelSize,
ширина: rightPanelSize,
ребенок: SettingsMenu (),
),
// Кнопки
Ряд(
дети: [
Кнопка ("влево", () => setState (() => isLeftMenuOpen =! IsLeftMenuOpen)),
Кнопка ("btm", () => setState (() => isBtmMenuOpen =! IsBtmMenuOpen)),
Кнопка ("вправо", () => setState (() => isRightMenuOpen =! IsRightMenuOpen)),
],
)
],
);
},
);
},
);
},
);
}
}

Последний интересен ... Изначально я собирался предложить строителям располагаться вокруг виджетов Positioned, а не Stack (и я бы все равно предложил это для левой и правой панелей), но потом я понял, что нижний влияет на все три, и я понял, что построителя, просто предоставившего вам один аргумент child самом деле недостаточно, потому что вы действительно хотите сохранить константы Container и Row в строит. Я полагаю, вы можете просто создать их над первым строителем.

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

Для справки, это не случайность. Это сделано специально. Использование этих виджетов в качестве собственных виджетов повышает производительность. Мы очень хотели, чтобы люди использовали Flutter именно так. Это то, что я имел в виду выше, когда упомянул, что «большая часть философии дизайна Flutter направлена ​​на то, чтобы направлять людей к правильному выбору».

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

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

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

Вероятно, хороший вариант использования выделенного RenderObject.

Или 3 легко управляемых объекта AnimatorObject, которые могут подключиться к жизненному циклу виджета: D

Использование этих виджетов в качестве собственных виджетов повышает производительность.

Поскольку мы здесь конкретизируем: в этом случае, поскольку это эшафот, каждое подчиненное представление уже является собственным виджетом, этот парень отвечает только за размещение и перевод дочерних элементов. Дитя будет BottomMenu (), ChannelMenu (), SettingsView (), MainContent () и т. Д.

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

[Edit] Я обновлю примеры, чтобы добавить этот контекст

Причина, по которой я предлагаю специальный RenderObject, заключается в том, что он улучшит макет. Я согласен с тем, что аспект анимации будет в виджете с отслеживанием состояния, он просто передаст три 0..1 двойных (значение1, значение2, значение3) объекту рендеринга вместо использования математики стека. Но это в основном второстепенная тема для данного обсуждения; в момент, когда вы это делаете, вы все равно захотите сделать упрощение, предоставляемое хуками или чем-то подобным в этом виджете с отслеживанием состояния.

Что касается более уместной заметки, у меня была трещина при создании демонстрации для проекта @TimWhiting : https://github.com/TimWhiting/local_widget_state_approaches/pull/1
Мне любопытно, как будет выглядеть версия Hooks. Я не уверен, что вижу хороший способ сделать это проще, особенно поддерживая характеристики производительности (или улучшая их; там есть комментарий, показывающий место, где это в настоящее время неоптимально).

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

Является ли реставрация критичной для этого примера? Из-за этого мне тяжело следить, и я действительно не знаю, что вообще делает RestorationMixin. Я предполагаю, что ... мне потребуется немного времени, чтобы понять это. Я уверен, что Реми раскрутит версию с крючками за 4 секунды :)

Использование API восстановления с использованием HookWidget вместо StatefulHookWidget настоящее время не поддерживается.

В идеале мы должны уметь менять

final value = useState(42);

в:

final value = useRestorableInt(42);

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

Кстати, перехватчики React имеют "ключевую" функцию, которая обычно используется так:

int userId;

Future<User> user = useMemo(() => fetchUser(id), [id]);

где этот код означает «кэшировать результат обратного вызова и повторно оценивать обратный вызов всякий раз, когда что-либо внутри массива изменяется»

Flutter_hooks повторно реализовал это 1: 1 (поскольку это всего лишь порт), но я не думаю, что мы хотели бы сделать это для кода, оптимизированного для Flutter.

Мы, вероятно, захотим:

int userId;

Future<User> user = useMemo1(id, (id) => fetchUser(id));

который будет делать то же самое, но снимает давление на память, избегая выделения списка и используя отрыв функции

На данном этапе это не критично, но стоит упомянуть, если мы планируем использовать flutter_hooks в качестве примеров.

@Hixie Я портировал ваш пример анимации на хуки

Это был интересный пример, спасибо за то, что об этом подумал!
Это хороший пример, поскольку по умолчанию реализации «active» и «duration» повсюду (и при этом зависят друг от друга).
По сути, было множество вызовов "if (active)" / "controller.repeat"

В то время как с хуками вся логика обрабатывается декларативно и сосредоточена в одном месте, без дублирования.

В этом примере также показано, как можно использовать хуки для простого кеширования объектов, что слишком часто решало проблему перестройки ExrivateWidget.
Мы получаем преимущества конструкторов const, но они работают с динамическими параметрами.

Мы также получили лучшую горячую перезагрузку. Мы можем изменить длительность Timer.periodic для цвета фона и сразу увидеть изменения в действии.

@rrousselGit у вас есть ссылка? Ничего нового в репозитории @TimWhiting не увидел.

Использование API восстановления с использованием HookWidget вместо StatefulHookWidget настоящее время не поддерживается.

Какое бы решение мы ни придумывали, мы должны убедиться, что ему не нужно знать о каждом последнем миксине. Если кто-то хочет использовать наше решение в сочетании с каким-либо другим пакетом, который вводит миксин, такой как TickerProviderStateMixin или RestorationMixin, они должны иметь возможность это сделать.

https://github.com/TimWhiting/local_widget_state_approaches/pull/3

Согласен, но меня это не беспокоит. useAnimationController не требует, чтобы пользователи заботились, например, об SingleTickerProvider.

AutomaritKeepAlive может выиграть от такого же лечения.
Одна из вещей, о которой я подумал, - это иметь хук "useKeepAlive (bool)"

Это позволяет избежать как примеси, так и "super.build (context)" (последнее довольно запутанно)

Еще один интересный момент - изменения, которые потребуются при рефакторинге.

Например, мы можем сравнить разницу между изменениями, необходимыми для реализации TickerMode для необработанного подхода и хуков:

Некоторые другие вещи смешаны в различии, но из него мы видим, что:

  • StatefulWidget необходим для перевода логики в совершенно другой жизненный цикл
  • Изменения крючков чисто аддитивные. Существующие строки не редактировались / не перемещались.

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

Согласовано!
Это также упрощает чтение кода:

final value = useSomething();
+ final value2 = useSomethingElse();

return Container(
  color: value.color,
-  child: Text('${value.name}'),
+  child: Text('${value.name} $value2'),
);

против:

return SomethingBuilder(
  builder: (context, value) {
-    return Container(
-      color: value.color,
-      child: Text('$value'),
+    return SomethingElseBuilder(
+      builder: (context, value2) {
+        return Container(
+          color: value.color,
+          child: Text('${value.name} $value2'),
+        );
+      }
    );
  },
);

Во втором различии не ясно, что Container не изменилось и что изменилось только Text

@rrousselGit Как вы думаете, имеет ли смысл попробовать сделать версию, представляющую, как это могло бы выглядеть с поддержкой ключевых слов? По сути, это похоже на хуки с выделенным ключевым словом use, или его стало еще легче отслеживать? Трудно сравнивать подход с крючками на равных, потому что в нем так много чужих концепций, как useEffect, useMemo и т. Д., Которые, как мне кажется, делают его более волшебным, чем он есть на самом деле?

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

В подходе с отслеживанием состояния мы вынуждены

  • копировать и вставлять логику (очень неудобно поддерживать)
  • с использованием построителя (не очень читабельно, особенно при использовании вложенности)
  • смешивание (плохо сочетается, очень легко для разных миксинов вступать в конфликт в их общем состоянии)

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

Я ни в коем случае не эксперт-разработчик. Но этот новый стиль повторного использования логики и записи ее в одном месте действительно удобен.

Я давно не использую Vue.js, но у них даже есть собственный API (вдохновленный хуками) для их следующей версии, возможно, стоит взглянуть.

И CMIIW, я думаю, правила перехвата реагирования (не использовать условные) не применимы к Composition API. Таким образом, вам не нужно использовать линтер для обеспечения соблюдения правил.

Опять же, раздел мотивации сильно усиливает ОП здесь:

МОТИВАЦИЯ
Код сложных компонентов становится все труднее рассуждать, поскольку функции со временем растут. Особенно это происходит, когда разработчики читают код, который они написали не сами.
[Было] Отсутствие чистого и бесплатного механизма для извлечения и повторного использования логики между несколькими компонентами.

Ключевые слова - «чистый» и «бесплатный». Миксины бесплатны, но они не чистые. Конструкторы чисты, но они не бесплатны ни с точки зрения удобочитаемости, ни с точки зрения архитектуры виджетов, поскольку их труднее перемещать по дереву и рассуждать с точки зрения иерархии.

Я также думаю, что при обсуждении удобочитаемости важно отметить следующее: «_в особенности случается, когда разработчики читают код, который они не писали_». Конечно, _ваш вложенный конструктор может быть легко читаемым, вы знаете, что там есть, и можете с комфортом пропустить его, он читает чужой код, как вы это делаете в любом более крупном проекте, или ваш собственный код недели / месяца назад, когда он становится совсем раздражает / сложно разбирать и рефакторировать эти вещи.

Некоторые другие особо актуальные разделы.

Почему просто иметь компоненты недостаточно:

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

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

фрагментация - это то, что затрудняет понимание и обслуживание сложного компонента. Разделение вариантов затемняет лежащие в основе логические проблемы. Вдобавок, работая над одной логической проблемой, мы должны постоянно «прыгать» по блокам опций для соответствующего кода. Было бы намного лучше, если бы мы могли объединить код, связанный с той же логической проблемой.

Мне любопытно, есть ли другие предложения по каноническим примерам, которые мы пытаемся улучшить, помимо того, что я представил.
Если это отправная точка, которую люди хотят использовать, тогда отлично. Однако я бы сказал, что сейчас самая большая проблема с этим - многословие; в этом примере не так много кода для повторного использования. Поэтому мне не ясно, хорошо ли это отражает проблему, описанную в OP.

Я еще немного подумал о том, как выразить характеристики, которые я лично буду искать в решении, и это заставило меня осознать одну из больших проблем, которые я вижу в текущем предложении Hooks, и это одна из причин, по которой я бы не стал хочу объединить его с фреймворком Flutter: Locality и Encapsulation, а точнее, их отсутствие. В конструкции хуков используется глобальное состояние (например, статика для отслеживания того, какой виджет создается в данный момент). ИМХО, это характеристика дизайна, которой следует избегать. В общем, мы стараемся сделать API-интерфейсы автономными, поэтому, если вы вызываете функцию с одним параметром, вы должны быть уверены, что она не сможет ничего делать со значениями за пределами этого параметра (вот почему мы передаем BuildContext вокруг , а не эквивалент useContext ). Я не говорю, что это характеристика, которая обязательно нужна каждому; и, конечно, люди могут использовать хуки, если для них это не проблема. Просто это то, чего я бы не хотел делать во Flutter. Каждый раз, когда у нас было глобальное состояние (например, в привязках), мы в конечном итоге сожалели об этом.

Хуки, вероятно, могут быть методами в контексте (я думаю, что они были в ранней версии), но, честно говоря, я не вижу особой ценности в их объединении в том виде, в каком они есть сейчас. Слияние должно иметь некоторые преимущества для разделения пакета, такие как повышение производительности или подключение определенных инструментов отладки. В противном случае они только усугубят путаницу, например, у вас будет 3 официальных способа прослушивания слушаемого: AnimatedBuilder, StatefulWidget и useListenable.

Итак, для меня путь заключается в улучшении генерации кода - я предложил некоторые изменения: https://github.com/flutter/flutter/issues/63323

Если бы эти предложения действительно были реализованы, люди, которым нужны были волшебные решения, подобные SwiftUI, в своем приложении могли бы просто создать пакет и никого не беспокоить.

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

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

Эта функция может пойти по пути, совершенно отличному от хуков, таких как SwiftUI или Jetpack Compose; предложение "именованного миксина" или синтаксический сахар для предложения Builders.

Я настаиваю на том, что эта проблема по своей сути требует упрощения шаблонов, таких как StreamBuilder:

  • StreamBuilder плохо читается / записывается из-за вложенности
  • Миксины и функции не являются возможной альтернативой StreamBuilder.
  • Копирование реализации StreamBuilder во все StatefulWidgets нецелесообразно.

Во всех комментариях до сих пор упоминались альтернативы StreamBuilder, как для различного поведения (создание одноразового объекта, выполнение HTTP-запросов, ...), так и для предложения разных синтаксисов.

Я не уверен, что еще можно сказать, поэтому не вижу, как мы можем двигаться дальше.
Что именно вы не понимаете / не согласны в этом заявлении @Hixie?

@rrousselGit Не могли бы вы создать демонстрационное приложение, которое это демонстрирует? Я попытался создать демонстрационное приложение, которое показало бы то, что я считал проблемой, но, видимо, я не понял этого правильно. (Я не уверен, в чем разница между «шаблонами вроде StreamBuilder» и тем, что я сделал в демонстрационном приложении.)

  • StreamBuilder плохо читается / записывается из-за вложенности

Вы уже сказали, что проблема не в многословии. Вложенность - это еще один аспект многословности. Если вложенность действительно является проблемой, мы должны перейти к Padding, Expanded, Flexible, Center, SizedBox и всем другим виджетам, которые добавляют вложенность без реальной причины. Но вложение можно легко решить, разделив монолитные виджеты.

Копирование реализации StreamBuilder во все StatefulWidgets нецелесообразно.

Вы имеете в виду копирование и вставку строк кода, которые создают и удаляют потоки, которые StatefulWidgets должны создавать и удалять? Да, это абсолютно разумно.

Если у вас есть десятки или не дай бог сотни _ разных_ настраиваемых виджетов StatefulWidget, которым нужно создавать / удалять свои собственные потоки - «использовать» в терминологии хуков - у вас есть более серьезные проблемы, о которых нужно беспокоиться, чем повторное использование или вложение «логики». Я бы побеспокоился о том, почему мое приложение вообще должно создавать так много разных потоков.

Честно говоря, я думаю, что кто-то считает, что какой-то конкретный шаблон не подходит для своего приложения, это нормально. (Это не обязательно означает, что фреймворк должен поддерживать это изначально, но было бы хорошо, по крайней мере, позволить пакету решить эту проблему.) Если кто-то не хочет использовать stream.listen или StreamBuilder(stream) , это их право, и, возможно, в результате мы сможем найти шаблон, который будет лучше для всех.

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

Я на 100% нахожусь на одной странице с вами.
Конечно, люди могут делать в своих приложениях все, что захотят. Я пытаюсь понять, что все проблемы и трудности, описанные в этой ветке, являются результатом плохих навыков программирования и на самом деле имеют мало общего с Dart или Flutter. Это похоже на мое мнение, но я бы сказал, что если кто-то пишет приложение, которое создает десятки потоков повсюду, возможно, стоит пересмотреть дизайн своего приложения, прежде чем просить «улучшить» фреймворк.

Например, реализация ловушки, которая попала в репозиторий примеров.

  <strong i="10">@override</strong>
  Widget build(BuildContext context) {
    final value = useAnimation(animation);

    final screenHeight = MediaQuery.of(context).size.height;
    final textHeight =
        useMemoized(() => math.sqrt(screenHeight), [screenHeight]);

    return Text(
      'Change Duration',
      style: TextStyle(fontSize: 10.0 + value * textHeight),
    );
  }

У меня плохое предчувствие по этому поводу, поэтому я проверил некоторые внутренние компоненты и добавил несколько отладочных отпечатков, чтобы проверить, что происходит.
Из вывода ниже видно, что ловушка Listenable проверяет, обновлялась ли она на каждом тике анимации. Например, был ли обновлен виджет, который "использует" эту прослушиваемую информацию? Была ли изменена продолжительность? Был заменен экземпляр?
Мемоизированный хук, я даже не знаю, в чем дело. Вероятно, это предназначалось для кеширования объекта, но при каждой сборке виджет проверяет, изменился ли объект? Какие? Почему? Конечно, поскольку он используется внутри виджета с отслеживанием состояния, а некоторые другие виджеты в дереве могут изменить значение, поэтому нам нужно запросить изменения. Это буквальное поведение при опросе - полная противоположность «реактивному» программированию.

Что еще хуже, «новые» и «старые» хуки имеют один и тот же тип экземпляра, оба имеют одинаковые значения, и все же функция перебирает значения, чтобы проверить, изменились ли они. На каждом тике анимации.

Это результат, который я получаю до бесконечности.

/flutter (28121): Use hook:_ListenableHook
I/flutter (28121): Is this the current hook:false
I/flutter (28121): 1: --- inside shouldPreserveState ----
I/flutter (28121): Hook1:Instance of '_ListenableHook'
I/flutter (28121): Hook1 keys:null
I/flutter (28121): Hook2 :Instance of '_ListenableHook'
I/flutter (28121): Hook2 keys:null
I/flutter (28121): 2. Shoud we preserve the  state of _ListenableHook:true
I/flutter (28121): 3: --------------
I/flutter (28121): checking if the listenable did change
I/flutter (28121): Did the listenable change?false
I/flutter (28121): Use hook:_MemoizedHook<double>
I/flutter (28121): Is this the current hook:false
I/flutter (28121): 1: --- inside shouldPreserveState ----
I/flutter (28121): Hook1:Instance of '_MemoizedHook<double>'
I/flutter (28121): Hook1 keys:[1232.0]
I/flutter (28121): Hook2 :Instance of '_MemoizedHook<double>'
I/flutter (28121): Hook2 keys:[1232.0]
I/flutter (28121): iterating over the hooks keys
I/flutter (28121): 2. Shoud we preserve the  state of _MemoizedHook<double>:true
I/flutter (28121): Use hook:_ListenableHook
I/flutter (28121): Is this the current hook:false
I/flutter (28121): 1: --- inside shouldPreserveState ----
I/flutter (28121): Hook1:Instance of '_ListenableHook'
I/flutter (28121): Hook1 keys:null
I/flutter (28121): Hook2 :Instance of '_ListenableHook'
I/flutter (28121): Hook2 keys:null
I/flutter (28121): 2. Shoud we preserve the  state of _ListenableHook:true

Вся эта работа выполняется на каждом тике анимации. Если я добавлю еще один крючок, например "final color = useAnimation(animationColor); ", чтобы также оживить цвет, теперь виджет проверяет _два_ раза, обновлялся ли он.

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

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

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

@ Хикси, ты не ответил на вопрос. Что вы не понимаете / не согласны с перечисленным выше списком маркеров?

Я не могу привести пример, не зная, что вы хотите, чтобы я продемонстрировал.

@Rudiksz Наверняка любое решение, которое мы действительно рассматриваем, должно быть профилировано и протестировано, чтобы убедиться, что оно не ухудшает ситуацию. Часть способа, которым я создал демонстрационное приложение, которое я отправил в

@rrousselGit Я действительно не хотел

  • Во-первых, я бы вообще избегал использования Streams. ValueListenable - это, IMHO, гораздо лучший шаблон для более или менее одинаковых вариантов использования.
  • Я не думаю, что StreamBuilder особенно сложно читать или писать; но, как ранее прокомментировал @satvikpendem , моя история
  • Что касается того, могут ли миксины и функции быть возможной альтернативой StreamBuilder, я думаю, что хуки довольно хорошо демонстрируют, что вы определенно можете использовать функции для прослушивания потоков, а миксины могут явно делать все, что классы могут здесь делать, поэтому я не понимаю, почему они не тоже не решение.
  • Наконец, что касается реализаций копирования и вставки, это субъективный вопрос. Я лично не копирую и не вставляю логику в initState / didUpdateWidget / dispose / build, я каждый раз пишу ее заново, и в основном это нормально. Когда он выходит из-под контроля, я добавляю его в такой виджет, как StreamBuilder. И снова мое мнение, вероятно, здесь не актуально.

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

Я хотел бы, чтобы вы продемонстрировали шаблон кодирования, который вы считаете неприемлемым, в демонстрации, которая достаточно важна, чтобы, когда кто-то создает решение, вы не отклонили бы его, сказав, что его может быть трудно использовать в другой ситуации. (т.е. включите все соответствующие ситуации, например, убедитесь, что включены части «обновления» или любые другие части, которые, по вашему мнению, важны для обработки), или заявите, что решение работает в одном случае, но не в общем случае (например, чтобы взять ваш предыдущий отзыв, убедившись, что параметры поступают из разных мест и обновляются в нескольких ситуациях, чтобы было очевидно, что решение, подобное Property выше, не будет исключать общий код).

Проблема в том, что вы задаете примеры того, что для меня является «очевидным».

Я не против привести несколько примеров, но я понятия не имею, чего вы ожидаете, поскольку не понимаю того, чего вы не понимаете.

Я уже сказал все, что должен был сказать.
Единственное, что я могу сделать, не понимая того, чего не понимаете вы, - это повторяться.

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

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

Крючки не следует рассматривать как функции.
Это новая языковая конструкция, похожая на Iterable / Stream.

Функции не могут делать то, что делают хуки - у них нет состояния или возможности вызывать перестроение виджетов.

Проблема с миксинами демонстрируется в OP. TL; DR: конфликт имен переменных, и невозможно повторно использовать один и тот же миксин несколько раз.

@rrousselGit Что ж, поскольку вы не против привести несколько примеров, и поскольку примеры, которые я прошу, очевидны, давайте начнем с некоторых из этих очевидных примеров и продолжим от них.

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

Я не могу придумать, что добавить к этим примерам.

Но, FWIW, я работаю над погодным приложением с открытым исходным кодом, используя Riverpod. Я свяжу это здесь, когда это будет сделано.


Я провел опрос в твиттере, задав несколько вопросов о Строителях, связанных с проблемами, обсуждаемыми здесь:

https://twitter.com/remi_rousselet/status/1295453683640078336

Опрос еще не завершен, но вот текущие цифры:

Screenshot 2020-08-18 at 07 01 44

Тот факт, что 86% из 200 человек хотят писать Builders, не требующие вложенности, говорит сам за себя.

Чтобы быть ясным, я никогда не предлагал не решать эту проблему. Если бы я подумал, что мы не должны его решать, вопрос был бы закрыт.

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

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

Например, вот суть о многочисленных ValueListenableBuilder + TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

Например, вот суть о многочисленных ValueListenableBuilder + TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

FWIW, этот конкретный пример проще реализовать в mobx.
На самом деле это короче, чем ваша реализация хуков.

Наблюдаемые Mobx - это ValueNotifiers на стероидах, а его виджет Observer - это эволюция ValueListenableBuilder от Flutter - он может прослушивать более одного ValueNotifier.
Быть заменой комбо ValueNotifier / ValueListenableBuilder означает, что вы по-прежнему пишете идиоматический код Flutter, что на самом деле является важным фактором.

Поскольку он по-прежнему использует встроенный в Tween конструктор Flutter, нет необходимости изучать / внедрять новые виджеты / хуки (другими словами, он не нуждается в новых функциях), и он не имеет никаких ударов по производительности хуков.

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'counters.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  <strong i="13">@override</strong>
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home1(),
    );
  }
}

var counters = Counters();

class Home1 extends StatelessWidget {
  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Observer(
              builder: (context) => TweenAnimationBuilder<int>(
                duration: const Duration(seconds: 5),
                curve: Curves.easeOut,
                tween: IntTween(end: counters.firstCounter),
                builder: (context, value, child) {
                  return Text('$value');
                },
              ),
            ),
            RaisedButton(
              onPressed: () => counters.firstCounter += 100,
              child: Text('+'),
            ),
            // Both counters have voluntarily a different Curve and duration
            Observer(
              builder: (context) => TweenAnimationBuilder<int>(
                duration: const Duration(seconds: 2),
                curve: Curves.easeInOut,
                tween: IntTween(end: counters.secondCounter),
                builder: (context, value, child) {
                  return Text('$value');
                },
              ),
            ),
            RaisedButton(
              onPressed: () => counters.secondCounter += 100,
              child: Text('+'),
            ),
            const Text('total:'),
            // The total must take into account the animation of both counters
            Observer(
              builder: (context) => TweenAnimationBuilder<int>(
                duration: const Duration(seconds: 5),
                curve: Curves.easeOut,
                tween: IntTween(
                    end: counters.firstCounter + counters.secondCounter),
                builder: (context, value, child) {
                  return Text('$value');
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Counters.dart прост как ...

part 'counters.g.dart';

class Counters = _Counters with _$Counters;
abstract class _Counters with Store {
  <strong i="18">@observable</strong>
  int firstCounter = 0;

  <strong i="19">@observable</strong>
  int secondCounter = 0;
}

Вот еще одна реализация, для которой даже не нужны конструкторы анимации. Метод сборки виджета настолько чист, насколько это возможно, почти как семантический html-файл ... как шаблон.

https://gist.github.com/Rudiksz/cede1a5fe88e992b158ee3bf15858bd9

@Rudiksz В вашем фрагменте
Точно так же я не уверен, что этот пример добавляет к варианту ValueListenableBuilder .

Что касается вашей последней сути, TickerProvider работает, поскольку он не поддерживает TickerMode - при этом не удаляются слушатели или контроллеры.

И Mobx скорее всего не по теме. Мы не обсуждаем, как реализовать окружающее состояние / ValueListenable vs Stores vs Streams, а скорее как работать с локальным состоянием / вложенными построителями, которые Mobx никак не решает.

––––

Также имейте в виду, что в примере с хуками useAnimatedInt можно / нужно извлечь в пакет, и что нет дубликата продолжительности / кривой между отдельной текстовой анимацией и общей.

Что касается производительности, с помощью Hooks мы перестраиваем только один элемент, тогда как с помощью Builders с помощью перестраиваем 2-4 Builder.
Так что хуки вполне могли быть быстрее.

В вашем фрагменте нарушено поведение поля "total". Это не соответствует примеру, где оба счетчика могут анимироваться вместе, но завершать анимацию в разное время и с разными кривыми.

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

Что касается вашей последней сути, TickerProvider не работает, поскольку он не поддерживает TickerMode.

Я не понимаю, что вы имеете в виду. Я отредактировал _ваш_ пример, в котором не используется режим TickerMode. Вы снова меняете требования.

Что касается производительности, с помощью Hooks мы перестраиваем только один элемент, тогда как с помощью Builders с помощью перестраиваем 2-4 Builder. Так что хуки вполне могли быть быстрее.

Нет, просто нет. Ваши виджеты-ловушки постоянно запрашивают изменения в каждой отдельной сборке. Строители, основанные на ценностях, являются «реактивными».

Точно так же я не уверен, что этот пример добавляет к варианту ValueListenableBuilder .

И Mobx скорее всего не по теме. Мы не обсуждаем, как реализовать окружающее состояние / ValueListenable vs Stores vs Streams, а скорее как работать с локальным состоянием / вложенными построителями, которые Mobx никак не решает.

Ты должно быть шутишь. Я взял ваш пример и «разобрался» с вложенными ValueListenableBuilders * и промежуточными конструкторами ?! Вопрос, который вы конкретно затронули как проблему.
Но это предложение здесь описывает все ваше отношение к этой дискуссии. Если это не хуки, это "не по теме", но вы говорите, что вам все равно, будут ли эти хуки использоваться в качестве решения.
Дай мне перерыв.

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

Это не так. Первый счетчик анимирует более 5 секунд, а второй - более 2 секунд - и оба используют разные кривые.

С помощью обоих приведенных мной фрагментов вы можете увеличивать оба счетчика одновременно, и во время каждого кадра анимации «итоговое значение» будет правильным. Даже когда второй счетчик перестает анимировать, а первый счетчик все еще работает

С другой стороны, ваша реализация не учитывает этот случай, потому что она объединила 2 TweenAnimationBuilder в один.
Чтобы исправить это, нам нужно написать:

Observer(
  builder: (context) {
    return TweenAnimationBuilder<int>(
      duration: const Duration(seconds: 5),
      curve: Curves.easeOut,
      tween: IntTween(end: counters.firstCounter),
      builder: (context) {
        return Observer(
          valueListenable: secondCounter,
          builder: (context, value2, child) {
            return TweenAnimationBuilder<int>(
              duration: const Duration(seconds: 2),
              curve: Curves.easeInOut,
              tween: IntTween(end: counters.secondCounter),
              builder: (context, value2, child) {
                return Text('${value + value2}');
              },
            );
          },
        );
      },
    );
  },
)

Два TweenAnimationBuilders необходимы, чтобы учитывать тот факт, что оба счетчика могут анимироваться индивидуально. И два Observer необходимы, потому что первый Observer не может соблюдать counters.secondCounter


Нет, просто нет. Ваши виджеты-ловушки постоянно запрашивают изменения в каждой отдельной сборке. Строители, основанные на ценностях, являются «реактивными».

Вы игнорируете то, что делает Element , что совпадает с тем, что делают хуки: сравнивая runtimeType и ключи, и решаете, создавать ли новый элемент или обновлять существующий.

Я взял ваш пример и «разобрался» с вложенными ValueListenableBuilders * и промежуточными конструкторами.

Предполагая, что проблема с общим счетом устранена, какое вложение было удалено?

Observer(
  builder: (context) => TweenAnimationBuilder<int>(
    duration: const Duration(seconds: 5),
    curve: Curves.easeOut,
    tween: IntTween(end: counters.firstCounter),
    builder: (context, value, child) {
      return Text('$value');
    },
  ),
),

ничем не отличается от:

ValueListenableBuilder<int>(
  valueListenable: firstCounter,
  builder: (context, value, child) => TweenAnimationBuilder<int>(
    duration: const Duration(seconds: 5),
    curve: Curves.easeOut,
    tween: IntTween(end: value),
    builder: (context, value, child) {
      return Text('$value');
    },
  ),
),

в плане вложенности.

Если вы имеете в виду свою суть, то, как я уже упоминал ранее, этот подход нарушает TickerProvider / TickerMode . vsync необходимо получить с помощью SingleTickerProviderClientStateMixin иначе он не поддерживает логику отключения звука, что может вызвать проблемы с производительностью.
Я объяснил это в своей статье: https://dash-overflow.net/articles/why_vsync/

И при таком подходе мы должны заново реализовать логику Tween в каждом месте, где изначально был бы нужен TweenAnimationBuilder. Это приводит к значительному дублированию, особенно учитывая, что логика не такая тривиальная.

С помощью обоих приведенных мной фрагментов вы можете увеличивать оба счетчика одновременно, и во время каждого кадра анимации «итоговое значение» будет правильным. Даже когда второй счетчик перестает анимировать, а первый счетчик все еще работает

С другой стороны, ваша реализация не учитывает этот случай, потому что она объединила 2 TweenAnimationBuilder в один.

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

Я подозревал, что вы возразите, поэтому вторая версия решает именно эту проблему, делая код еще чище.

Если вы имеете в виду свою суть, то, как я уже упоминал ранее, этот подход нарушает TickerProvider / TickerMode. Vsync необходимо получить с помощью SingleTickerProviderClientStateMixin, иначе он не поддерживает логику отключения звука, что может вызвать проблемы с производительностью.

Таким образом, вы создаете поставщика тикеров в виджете и передаете его счетчикам. Я также не избавлялся от контроллеров анимации. Эти детали настолько тривиальны для реализации, что я не чувствовал, что они добавят что-нибудь к примеру. Но здесь мы придираемся к ним.

Я реализовал класс Counter (), чтобы делать то, что делает ваш пример, и не более того.

И при таком подходе мы должны заново реализовать логику Tween в каждом месте, где изначально был бы нужен TweenAnimationBuilder. Это приводит к значительному дублированию, особенно учитывая, что логика не такая тривиальная.

Какие? Почему? Пожалуйста, объясните, почему я не мог создать более одного экземпляра класса Counter и использовать их в различных виджетах?

@Rudiksz Я не уверен, что ваши решения действительно решают указанные проблемы. Ты говоришь

Я реализовал класс Counter (), чтобы делать то, что делает ваш пример, и не более того.

и все еще

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

Таким образом, вы создаете поставщика тикеров в виджете и передаете его счетчикам. Я также не избавлялся от контроллеров анимации. Эти детали настолько тривиальны для реализации, что я не чувствовал, что они добавят что-нибудь к примеру. Но здесь мы придираемся к ним.

Вы предоставили код, который только якобы эквивалентен версии хука @rrousselGit , но на самом деле он не эквивалентен, поскольку вы не репозитория @TimWhiting . Вы можете отправить туда свое решение, если считаете, что выполнили все требования.

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

Нет, просто нет.

Ты должно быть шутишь. Я взял ваш пример и «разобрался» с вложенными ValueListenableBuilders * и промежуточными конструкторами.

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

@rrousselGit

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

Причина, по которой я прошу более сложное приложение, чем просто фрагмент, заключается в том, что когда я опубликовал пример, который обрабатывал один из ваших фрагментов, вы сказали, что этого недостаточно, потому что он также не обрабатывал другой случай, который не был 't в вашем фрагменте (например, не обработал didUpdateWidget, или не справился с двумя из этих фрагментов рядом, или другие вполне разумные вещи, которые вы хотели бы обработать). Я надеюсь, что с помощью более сложного приложения мы сможем сделать его достаточно проработанным, чтобы, как только у нас будет решение, не будет момента, когда возникнет какая-то новая проблема, которая также требует решения. Очевидно, все еще возможно, что мы все упустим что-то, что требует обработки, но идея состоит в том, чтобы минимизировать этот шанс.

Что касается недавних не совсем приятных постов, пожалуйста, ребята, давайте просто сконцентрируемся на создании примеров в репозитории @TimWhiting , а не на

Вы можете отправить туда свое решение, если считаете, что выполнили все требования.

Требования были изменены постфактум. Я предложил два решения. Тот, который идет на компромисс (очень разумный), и тот, который реализует точное поведение, указанное в примере.

Вы предоставили код, который только якобы эквивалентен версии хуков @rrousselGit , но на самом деле он не эквивалентен,

Я не реализовал «решение хуков», я реализовал пример ValueListenableBuilder, специально сосредоточившись на «проблеме вложенности». Он не делает все, что делают хуки, я просто продемонстрировал, как можно упростить один пункт в маркированном списке жалоб, используя альтернативное решение.

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

Возможность повторного использования: взгляните на пример в репо ниже
https://github.com/Rudiksz/cbl_example

Примечание:

  • он предназначен как демонстрация того, как вы можете инкапсулировать "логику" вне виджетов и иметь виджеты, которые выглядят скудно, почти в формате HTML.
  • он не покрывает все, что покрывает крючки. Не в этом дело. Я думал, что мы обсуждаем альтернативы стандартному фреймворку Flutter, а не пакету хуков.
  • Он не охватывает _все_ сценарии использования, которые может придумать каждая маркетинговая команда.
  • но сам объект Counter довольно гибкий, его можно использовать автономно (см. заголовок AppBar), как часть сложного виджета, который должен вычислять итоги различных счетчиков, быть реактивным или отвечать на вводимые пользователем данные.
  • Потребляющие виджеты могут настроить количество, начальное значение, продолжительность, тип анимации счетчиков, которые они хотят использовать.
  • детали того, как обрабатываются анимации, могут иметь недостатки. Если команда Flutter скажет, что да, использование контроллеров анимации и анимации вне виджетов каким-то образом нарушает структуру, я передумаю. Определенно есть что улучшить. Замена пользовательского провайдера тикеров, который я использую, на созданный миксином потребляющего виджета тривиальна. Я этого не делал.
  • Это всего лишь еще одно альтернативное решение паттерну «Строитель». Если вы говорите, что вам нужно или хотите использовать Builders, то все это не применимо.
  • Тем не менее, факт остается фактом: есть способы упростить код без дополнительных функций. Если вам это не нравится, не покупайте. Я не выступаю за то, чтобы все это вошло во фреймворк.

Изменить: в этом примере есть ошибка, если вы инициируете «приращение» во время анимации изменения, счетчик сбрасывается и увеличивается с текущего значения. Я специально не исправлял, потому что не знаю точных требований, которые могут быть у вас к этим «счетчикам». И снова тривиально изменить методы увеличения / уменьшения, чтобы исправить эту проблему.

Это все.

@Hixie Должен ли я интерпретировать ваш комментарий как способ сказать, что мой пример (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/hooks/animated_counter.dart) недостаточно хорош?

Кроме того, можно ли организовать встречу с Zoom / Google?

Я также не уверен, каков эффект смайлика-реакции на ваш собственный пост.

Ничего такого. Это совершенно ни к чему не относится. Почему ты поднял это?

@rrousselGit Только вы можете знать, достаточно ли он хорош. Если мы найдем способ реорганизовать этот пример, чтобы он был чистым, коротким и не содержал повторяющегося кода, вы останетесь довольны? Или есть вещи, которые, по вашему мнению, мы должны поддерживать, но которые не рассматриваются в этом примере, которые нам нужно обработать, чтобы устранить эту ошибку?

Только ты можешь знать, достаточно ли это хорошо

Я не могу судить об этом. Начнем с того, что я не верю, что мы можем решить проблему с помощью ограниченного набора приложений.

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

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

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

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

Мне кажется, что мы забили 2 очка, и я не думаю, что можно серьезно спорить ни с одним из них.

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

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

Опрос Реми показывает нам, что ~ 80% разработчиков Flutter предпочли бы какую-то способность избегать вложенных конструкторов в свой код, когда это возможно. Это действительно говорит само за себя, имо. Вам не нужно забирать это у нас в этой ветке, когда настроения сообщества настолько ясны.

С моей точки зрения, проблемы ясны, и они становятся еще более ясными, если вы посмотрите на конкурирующие фреймворки, которые посвящают абзацы описанию обоснования здесь. Vue, React, Flutter - все они двоюродные братья, все они унаследованы от React, и все они сталкиваются с этой проблемой из-за повторного использования состояния, которое должно быть связано с жизненным циклом виджета. Все они подробно описывают, почему они реализовали нечто подобное. Все в порядке. Это все актуально.

@rrousselGit не могли бы вы привести пример наличия множества хуков? Например, я делаю анимацию, возможно, с десятками AnimationController. С обычным Flutter я могу:

List<AnimationController> controllers = [];
int numAnimationControllers = 50;

<strong i="7">@override</strong>
void initState() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers.add(AnimationController(...));
}

<strong i="8">@override</strong>
void dispose() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers[i].dispose();
}

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

@satvikpendem

несколько примеров из моих приложений, которые находятся в разработке (некоторые из хуков, таких как отправка запроса с разбивкой на страницы, могут объединять / рефакторинг в один хук, но здесь это не имеет значения):

простая выборка данных с разбивкой на страницы:

    final selectedTab = useState(SelectedTab.Wallet);
    final isDrawerOpen = useValueNotifier(false);
    final pageNo = useValueNotifier(0);
    final generalData = useValueNotifier(initialData);
    final services = useXApi();
    final isLoading = useValueNotifier(false);
    final waybillData = useValueNotifier<List<WaybillResponseModel>>([]);
    final theme = useTheme();
    final router = useRouter();

    fetchNextPage() async {
      if (isLoading.value || selectedTab.value != SelectedTab.Wallet) return;
      isLoading.value = true;
      final request = WaybillRequestModel()..pageNo = pageNo.value;
      final result = await services.waybillGetList(model: request);
      if (result.isOk && result.data.length > 0) {
        pageNo.value += 1;
        waybillData.value = [...waybillData.value, ...result.data];
      }
      isLoading.value = false;
    }

    // first fetch
    useEffect(() {
      fetchNextPage();
      return () {};
    }, []);

логика формы (форма входа с проверкой номера телефона и таймером повторной отправки):

    final theme = useTheme();
    final loginState = useValueNotifier(LoginState.EnteringNumber);
    final error = useValueNotifier<String>(null);
    final phoneNumberController = useTextEditingController(text: "");
    final phoneNumberFocusNode = useMemoized(() => FocusNode(), []);
    final otpFocusNode = useMemoized(() => FocusNode(), []);
    final otpController = useTextEditingController(text: "");
    final appState = Provider.of<AppStateController>(context);
    final services = useXApi();
    final router = useRouter();
    final resendTimerValue = useValueNotifier(0);
    useEffect(() {
      var timer = Timer.periodic(Duration(seconds: 1), (t) async {
        if (resendTimerValue.value > 0) resendTimerValue.value--;
      });
      return () => timer.cancel();
    }, []);

    final doLogin = () async {
      // login
      loginState.value = LoginState.LoggingIn;
      final loginResult = await services.authLoginOrRegister(
        mobileNumber: phoneNumberController.text,
      );
      if (loginResult.isOk) {
        loginState.value = LoginState.EnteringOtpCode;
        WidgetsBinding.instance.addPostFrameCallback((_) {
          FocusScope.of(context).requestFocus(otpFocusNode);
        });
        resendTimerValue.value = 30;
      } else {
        error.value = loginResult.errorMessage;
        loginState.value = LoginState.EnteringNumber;
        WidgetsBinding.instance.addPostFrameCallback((_) {
          FocusScope.of(context).requestFocus(phoneNumberFocusNode);
        });
      }
    };

для анимации, я думаю, @rrousselGit уже предоставил достаточно примеров.

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

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

Я буквально привел примеры того, как можно уменьшить многословие и избежать вложенности построителей, используя пример, предоставленный Реми.
Я взял его код, вставил его в свое приложение, запустил и переписал. Конечный результат с точки зрения функциональности был почти идентичным - насколько я мог почерпнуть из простого запуска кода, потому что код не содержал требований. Конечно, мы можем обсудить крайние случаи и потенциальные проблемы, но вместо этого это было названо не по теме.

Для простых случаев использования я использую Builders, для сложных случаев я не использую Builders. Аргумент здесь в том, что без использования Builders нет простого способа написать краткий, многоразовый код. Неявно это также означает, что конструкторы необходимы и являются единственным способом разработки приложений Flutter. Это явно неверно.

Я только что показал работающий, доказательный концептуальный код, который это демонстрирует. Он не использовал Builders или хуки, и он не покрывает 100% «бесконечного набора проблем», который, похоже, хочет решить эта конкретная проблема с github. Это было названо не по теме.
Замечание, он также очень эффективен, даже без каких-либо тестов, я думаю, даже превосходит виджеты Builder. Я счастлив передумать, если окажется, что это не так, и если я когда-нибудь обнаружу, что Mobx станет узким местом в производительности, я сразу же откажусь от Mobx и переключусь на ванильные конструкторы.

Хикси работает в Google, он должен быть терпеливым и вежливым с вами и не может критиковать вас из-за того, что вы не заинтересованы. Лучшее, что он может сделать, - это привести больше примеров.

Я никого не называла по именам и не совершала личных нападок. Я только когда-либо реагировал на приведенные здесь аргументы, поделился своим мнением
(который, как я знаю, непопулярен и находится в меньшинстве), и даже пытался представить реальные контрольные примеры с помощью кода. Я мог бы сделать больше, я готов обсудить, в чем мои примеры не оправдывают ожиданий, и посмотреть, как мы можем их улучшить, но да, то, что меня называют не по теме, отчасти отталкивает.

Мне нечего терять, кроме того, что, может быть, меня забанят, так что мне все равно, что тебя называть.
Очевидно, что вы двое твердо уверены, что хуки - единственное решение («потому что это делает React») любых проблем, с которыми вы сталкиваетесь, и что если альтернатива не решит на 100% «бесконечный набор проблем», который вы воображаете, вы даже не подумаете об участии.

Это неразумно и свидетельствует об отсутствии желания по-настоящему заниматься.


Конечно, все вышесказанное «только мое мнение».

Я вижу полезность хуков в этом примере, но я не понимаю, как это будет работать в моем случае, когда кажется, что вы захотите инициализировать сразу несколько объектов, в данном случае AnimationController s, но на самом деле это могло быть что угодно. Как крючки справляются с этим делом?

В принципе, есть крючок, чтобы повернуть это

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

В

var xs = []
for (int i = 0; i < 3; i++)
     xs[i] = useState(i);

Не нарушая правил ловушки? Потому что я перечислил эквивалент в обычном Flutter. Я не слишком опытен с хуками во Flutter, так что терпите меня.

Я хочу просто создать массив объектов хуков (например, AnimationControllers) по запросу со всем его initState и удалить уже созданный экземпляр, я просто не уверен, как это работает в хуках.

@satvikhibitedm думает о

в вашем примере, определяющем это

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

полезно для этого варианта использования:

var isLoading = useState(1);
var selectedTab = useState(2);
var username = useState(3); // text field

вы видите, как каждый useState связан с именованной частью вашей логики состояния? (например, useState isLoading подключается, когда приложение находится в состоянии загрузки)

во втором фрагменте вы вызываете useState в цикле. вы думаете о useState как о держателе значения, не являющемся частью вашей логики состояния. нужен ли этот список для отображения группы элементов в ListView ? Если да, то вы должны думать о каждом элементе в списке как о состоянии, а не индивидуально.

final listData = useState([]);

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

var single = useTest("data");
var list = useTests(["data1", "data2"]);
// which is equivalent to
var single1 = useTest("data1");
var single2 = useTest("data2");

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

Это то, что у меня было изначально, но, похоже, не работает:

  final animationControllers = useState<List<AnimationController>>([]);

  animationControllers.value = List<AnimationController>.generate(
    50,
    (_) => useAnimationController(),
  );

но я полагаю, что если я напишу свой собственный хук для обработки нескольких элементов, это должно сработать, верно?

class _MultipleAnimationControllerHook extends Hook<MultipleAnimationController> {
  const _MultipleAnimationControllerHook({
    this.numControllers,
    this.duration,
    this.debugLabel,
    this.initialValue,
    this.lowerBound,
    this.upperBound,
    this.vsync,
    this.animationBehavior,
    List<Object> keys,
  }) : super(keys: keys);

  /// Take in number of controllers wanted
  /// This hook assumes all `AnimationController`s will have the same parameters
  final int numControllers; 

  final Duration duration;
  final String debugLabel;
  final double initialValue;
  final double lowerBound;
  final double upperBound;
  final TickerProvider vsync;
  final AnimationBehavior animationBehavior;

  <strong i="10">@override</strong>
  _AnimationControllerHookState createState() =>
      _AnimationControllerHookState();
}

class _AnimationControllerHookState
    extends HookState<AnimationController, _AnimationControllerHook> {
  List<AnimationController> _multipleAnimationController; // return a list instead of a singular item

  <strong i="11">@override</strong>
  void initHook() {
    super.initHook();
    for (int i = 0; i < hook.numControllers) // just added loops 
        _multipleAnimationController[i] = AnimationController(
          vsync: hook.vsync,
          duration: hook.duration,
          debugLabel: hook.debugLabel,
          lowerBound: hook.lowerBound,
          upperBound: hook.upperBound,
          animationBehavior: hook.animationBehavior,
          value: hook.initialValue,
        );
  }

  <strong i="12">@override</strong>
  void didUpdateHook(_AnimationControllerHook oldHook) {
      for (int i = 0; i < numControllers; i++) {
        if (hook.vsync != oldHook[i].vsync) {
           _multipleAnimationController[i].resync(hook.vsync);
        }

        if (hook.duration != oldHook[i].duration) {
          _multipleAnimationController[i].duration = hook.duration;
        }
      }
  }

  <strong i="13">@override</strong>
  MultipleAnimationController build(BuildContext context) {
    return _multipleAnimationController;
  }

  <strong i="14">@override</strong>
  void dispose() {
    _multipleAnimationController.map((e) => e.dispose());
  }
}

Означает ли это, что если у нас есть единственная версия ловушки, мы просто не можем использовать ее для версии с несколькими элементами и вместо этого придется переписать логику? Или есть лучший способ сделать это?

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

@satvikpendem

Я не думаю, что мое утверждение верно для AnimationController или useAnimationController

потому что, хотя у вас может быть более одного AnimationController но вы не обязательно храните их в массиве, чтобы использовать их в методе класса. Например:

useSingleTickProvider();
final animation1 = useAnimationController();
final animation2 = useAnimationController();
final animation3 = useAnimationController();
// ...
useEffect(() async {
  await animation1.forward();
  await Future.sleep(100);
  await animation1.reverse();
  await animation2.forward();
  await animation3.forward();
}, []);

(вы не создаете список и не ссылаетесь на них как animation[0] )

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

чтобы ответить на ваш вопрос, есть ли более простой способ справиться с несколькими AnimationController , да, есть:

final ticker = useTickerProvider();
final controllers = useMemo(() => [AnimationController(ticker), AnimationController(ticker)], []);

useEffect(() {
  controllers.forEach(x => x.resync(ticker));
  return () => controllers.forEach(x => x.dispose());
}, [ticker, controllers]);

  • вы также можете использовать useState если AnimationController s являются динамическими.

(он также повторно синхронизируется при изменении тикера)

@rrousselGit не могли бы вы привести пример наличия множества хуков? Например, я делаю анимацию, возможно, с десятками AnimationController. С обычным Flutter я могу:

List<AnimationController> controllers = [];
int numAnimationControllers = 50;

<strong i="8">@override</strong>
void initState() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers.add(AnimationController(...));
}

<strong i="9">@override</strong>
void dispose() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers[i].dispose();
}

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

Крючки делают это по-другому.

Мы больше не создаем список контроллеров, а перемещаем логику контроллера вниз к элементу:

Widget build(context) {
  return ListView(
    children: [
      for (var i = 0; i < 50; i++)
        HookBuilder(
          builder: (context) {
            final controller = useAnimationController();
          },
        ),
    ],
  );
}

Мы по-прежнему создали 50 контроллеров анимации, но они принадлежат другому виджету.

Может быть, вы могли бы поделиться примером того, почему вам это нужно, и мы могли бы попробовать преобразовать их в хуки и добавить их в репо Тима?

Хикси работает в Google, он должен быть терпеливым и вежливым с вами и не может критиковать вас из-за того, что вы не заинтересованы. Лучшее, что он может сделать, - это привести больше примеров.

@ Хикси , если ты так себя чувствуешь, скажи, пожалуйста (здесь или свяжитесь со мной в частном порядке).

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

Спасибо, но мне непонятно, как извлечь общий шаблон из этого кода, чтобы применить эту логику к различным вариантам использования.

В OP я упоминал, что в настоящее время у нас есть 3 варианта:

  • использовать Строители и иметь вложенный код
  • вообще не факторизуйте код, который не масштабируется до более сложной логики состояния (я бы сказал, что StreamBuilder и его AsyncSnapshot - это сложная логика состояния).
  • попробуйте создать некоторую архитектуру с помощью mixins / oop / ..., но в итоге получите решение, слишком специфичное для проблемы, что любой вариант использования, который немного отличается от _tiny_, потребует переписывания.

Мне кажется, что вы использовали третий вариант (который находится в той же категории, что и ранние итерации предложений Property или addDispose ).

Ранее я сделал оценочную сетку, чтобы судить о паттерне:

Не могли бы вы запустить свой вариант на этом? Особенно второй комментарий о реализации всех функций StreamBuilder без дублирования кода при многократном использовании.

Мой план на данный момент по этой ошибке:

  1. Возьмите примеры из https://github.com/flutter/flutter/issues/51752#issuecomment -675285066 и создайте приложение с использованием чистого Flutter, которое вместе показывает эти различные варианты использования.
  2. Попробуйте разработать решение, позволяющее повторно использовать код для тех примеров, которое удовлетворяет различным ключевым требованиям, которые обсуждались здесь, и которое соответствует нашим принципам проектирования.

Если кто-то захочет помочь с одним из них, я определенно буду рад получить помощь. Я вряд ли скоро до этого доберусь, потому что сначала я работаю над переходом NNBD.

@rrousselGit Конечно, я создаю приложение, в котором многие виджеты могут перемещаться по экрану (назовем их Box es), и они должны иметь возможность перемещаться независимо друг от друга (так что должно быть как минимум один AnimationController для каждого Box ). Вот одна версия, которую я сделал только с одним AnimationController, совместно используемым несколькими виджетами, но в будущем я могу анимировать каждый виджет независимо, например, для выполнения сложных преобразований, таких как реализация CupertinoPicker с его настраиваемым эффектом колеса прокрутки. .

В стеке есть три поля, которые перемещаются вверх и вниз при нажатии кнопки FloatingActionButton.

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main(List<String> args) => runApp(const App());

class App extends HookWidget {
  const App({Key key});

  static const Duration duration = Duration(milliseconds: 500);
  static const Curve curve = Curves.easeOutBack;

  <strong i="11">@override</strong>
  Widget build(BuildContext context) {
    final AnimationController controller =
        useAnimationController(duration: duration);
    final Animation<double> animation = Tween<double>(
      begin: 0,
      end: 300,
    )
        .chain(
          CurveTween(
            curve: curve,
          ),
        )
        .animate(controller);
    final ValueNotifier<bool> isDown = useState<bool>(false);
    final ValueNotifier<int> numBoxes = useState<int>(3);

    return MaterialApp(
      home: SafeArea(
        child: Scaffold(
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              if (!isDown.value) {
                controller.forward();
                isDown.value = true;
              } else {
                controller.reverse();
                isDown.value = false;
              }
            },
          ),
          body: AnimatedBuilder(
            animation: animation,
            builder: (_, __) => Boxes(
              numBoxes: numBoxes.value,
              animation: animation,
            ),
          ),
        ),
      ),
    );
  }
}

class Boxes extends StatelessWidget {
  Boxes({
    <strong i="12">@required</strong> this.numBoxes,
    <strong i="13">@required</strong> this.animation,
  });

  final int numBoxes;
  final Animation<double> animation;

  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    return Stack(
      children: List<Widget>.generate(
        numBoxes,
        (int index) => Positioned(
          top: (animation.value) + (index * (100 + 10)),
          left: (MediaQuery.of(context).size.width - 100) / 2,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          ),
        ),
      ),
      // ],
    );
  }
}

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

Проблема, по-видимому, в том, что AnimationControllers и Box es, которые используют их для управления своим движением, не принадлежат к одному и тому же классу, поэтому нужно либо пройти через AnimationController, сохранив их массив для использования в Builder, или пусть каждый Box поддерживает свой собственный AnimationController.

С помощью хуков, учитывая, что Box es и родительский виджет не находятся в одном классе, как мне составить список AnimationControllers для первого случая, когда каждый Box передается в AnimationController? Это кажется ненужным на основе вашего ответа выше с помощью HookBuilder, но затем, если я перейду вниз по состоянию в дочерний виджет, как вы говорите, и выберу, чтобы каждый Box имел свой собственный AnimationController через useAnimationController , Я столкнулся с другой проблемой: как мне предоставить созданный AnimationController родительскому классу, чтобы он мог координировать и запускать независимые анимации для каждого дочернего элемента?

В Vue вы можете отправить событие обратно родителю через шаблон emit , поэтому во Flutter мне нужно какое-то решение для управления состоянием более высокого уровня, например Riverpod или Rx, где родитель обновляет глобальное состояние, а потомок слушает глобальное штат? Кажется, я не должен, по крайней мере, для такого простого примера, как этот. Спасибо, что разобрались с моими недоразумениями.

@satvikpendem Извините, я не

Я хочу иметь четкое представление о том, что вы пытаетесь сделать, а не о том, где вы застряли

Но в качестве быстрого предположения я думаю, вы вместо этого ищете кривую Interval и имеете единственный контроллер анимации.

@rrousselGit Конечно, вот оно

import 'package:flutter/material.dart';

void main(List<String> args) => runApp(const App());

class Animator {
  Animator({this.controller, this.animation});
  AnimationController controller;
  Animation<double> animation;
}

class App extends StatefulWidget {
  const App({Key key});

  static const Duration duration = Duration(milliseconds: 500);
  static const Curve curve = Curves.easeOutBack;

  <strong i="7">@override</strong>
  _AppState createState() => _AppState();
}

class _AppState extends State<App> with TickerProviderStateMixin {
  List<Animator> animators = [];
  bool isDown = false;
  int numBoxes = 3;

  <strong i="8">@override</strong>
  void initState() {
    for (int i = 0; i < numBoxes; i++) {
      final AnimationController c = AnimationController(
        duration: App.duration,
        vsync: this,
      );
      animators.add(
        Animator(
          controller: c,
          animation: Tween<double>(
            begin: 0,
            end: 300,
          )
              .chain(
                CurveTween(
                  curve: App.curve,
                ),
              )
              .animate(c),
        ),
      );
    }
    super.initState();
  }

  <strong i="9">@override</strong>
  void dispose() {
    for (int i = 0; i < numBoxes; i++) {
      animators[i].controller.dispose();
    }
    super.dispose();
  }

  <strong i="10">@override</strong>
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SafeArea(
        child: Scaffold(
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              if (!isDown) {
                for (final Animator animator in animators) {
                  animator.controller.forward();
                }
                setState(() {
                  isDown = true;
                });
              } else {
                for (final Animator animator in animators) {
                  animator.controller.reverse();
                }
                setState(() {
                  isDown = false;
                });
              }
            },
          ),
          body: Stack(
            children: List<Box>.generate(
              numBoxes,
              (int index) => Box(
                index: index,
                animation: animators[index].animation,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class Box extends StatelessWidget {
  Box({
    <strong i="11">@required</strong> this.animation,
    <strong i="12">@required</strong> this.index,
  });

  final int index;
  final Animation<double> animation;

  <strong i="13">@override</strong>
  Widget build(BuildContext context) {
    return Positioned(
      top: (animation.value) + (index * (100 + 10)),
      left: (MediaQuery.of(context).size.width - 100) / 2,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
    );
  }
}

На самом деле мне нужно несколько контроллеров анимации, по одному для каждого виджета, поскольку они могут перемещаться независимо друг от друга, со своей длительностью, кривыми и т. Д. Обратите внимание, что в приведенном выше коде, похоже, есть ошибка, которую я не мог понять, где он должен анимироваться чисто, но в основном он должен анимировать 3 блока вверх и вниз по нажатию кнопки. Мы можем представить сценарий, в котором вместо того, чтобы у каждого из них была одна и та же кривая, я даю каждому из них другую кривую, или я делаю 100 коробок, каждая с длительностью больше или меньше, чем предыдущая, или я заставляю четные подниматься вверх. а лишние падают и так далее.

При обычном Flutter, initState и dispose могут иметь циклы, но не так, как кажется с хуками, поэтому мне просто интересно, как с этим бороться. Кроме того, я не хочу помещать класс Box в родительский виджет, поскольку я не хочу плотно инкапсулировать их оба; Я должен иметь возможность сохранить родительскую логику, но, например, поменять Box на Box2 .

Спасибо!
Я отправил ваш пример в

TL; DR, с крючками (или строителями) мы думаем декларативно, а не императивно. Таким образом, вместо того, чтобы иметь список контроллеров на одном виджете, а затем принудительно управлять ими, что перемещает контроллер к элементу и реализует неявную анимацию.

Спасибо @rrousselGit! Я некоторое время боролся с этим типом реализации после того, как начал использовать хуки, но теперь я понимаю, как это работает. Я только что открыл PR для версии с другой целью для каждого контроллера анимации, так как это может быть более убедительным, чтобы понять, почему хуки полезны, как я сказал выше:

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

Я пытался сделать декларативную версию, но полагаю, что я не понял, что это метод жизненного цикла didUpdateWidget/Hook , поэтому я не знал, как управлять анимацией, когда дочерняя опора изменяется от родительской, но ваш код прояснил это.

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

Итак, в этом сценарии я работаю с Firestore и у меня есть шаблон, который я хочу использовать с каждым StreamBuilder, поэтому я создал свой собственный пользовательский конструктор. Мне также нужно работать с ValueListenableчто позволяет пользователю переупорядочивать список. По причинам денежных затрат, связанных с Firestore, для этого требуется очень конкретная реализация (каждый элемент не может хранить свой собственный заказ, вместо этого список должен сохранять его как поле конкатенированных идентификаторов), это связано с тем, что firestore взимает плату за каждую запись, поэтому таким образом потенциально можно сэкономить много денег. В итоге получается что-то вроде этого:

return ClipRRect(
      borderRadius: BorderRadius.circular(CornerStyles.dialog),
      child: Scaffold(
        backgroundColor: state.theme.scaffoldBackgroundColor,
        body: FamilyStreamBuilder<DocumentSnapshot>(
          stream: state.listRef.snapshots(),
          builder: (context, AsyncSnapshot<DocumentSnapshot> documentSnapshot) {
            //When a list is updated, we need to update the listOrder
            state.updateListOrderFromSnapshot(documentSnapshot);
            return FamilyStreamBuilder<QuerySnapshot>(
                stream: state.itemsCollection.snapshots(),
                builder: (_, AsyncSnapshot<QuerySnapshot> itemsSnapshot) {
                  //Sort the list items by the idField on the list-doc
                  List<DocumentSnapshot> items = itemsSnapshot.data.documents;
                  state.handleDocsSync(items);
                  return ValueListenableBuilder(
                    valueListenable: state.listOrderNotifier,
                    builder: (_, List<String> listOrder, __) {
                      List<DocumentSnapshot> ordered = state.sortItems(items, listOrder);
                     //Set the firstCompleted item if we have one
                      state.firstCompletedItem = ordered
                          ?.firstWhere((element) => element.data[FireParams.kIsComplete] == true, orElse: () => null)
                          ?.documentID;
                      return _buildItemList(items, ordered);
                    },
                  );
                });
          },
        ),
      ),
    );

Похоже, что было бы намного проще рассуждать, если бы я мог написать это больше как:

    DocumentSnapshot list = useFamilyStream(state.listRef.snapshots());
    List<DocumentSnapshot> items = useFamilyStream(state.itemsCollection.snapshots()).data.documents;
    List<String> listOrder = useValueListenable(state.listOrderNotifier);

    //When a list is updated, we need to update the listOrder
    state.updateListOrderFromSnapshot(list);

   //Sort the list items by the idField on the list-doc
    state.handleDocsSync(items);
    List<DocumentSnapshot> ordered = state.sortItems(items, listOrder);

    //Set the firstCompleted item if we have one
    state.firstCompletedItem = ordered
        ?.firstWhere((element) => element.data[FireParams.kIsComplete] == true, orElse: () => null)
        ?.documentID;

    return ClipRRect(
      borderRadius: BorderRadius.circular(CornerStyles.dialog),
      child: Scaffold(
          backgroundColor: state.theme.scaffoldBackgroundColor,
          body: _buildItemList(items, ordered)
      ));

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

Как и во многих других сценариях реального мира, совет «Просто не используйте X» нереалистичен, поскольку Firebase имеет только один метод подключения, а именно потоки, и в любое время, когда я хочу это поведение, подобное сокету, у меня нет другого выбора, кроме как используйте Stream. Такова жизнь.

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

Это все еще имеет значение. То, является ли узел визуальным или нет, не влияет на то, стоит ли что-то перестраивать.

Я бы, вероятно, разделил этот пример на разные виджеты (в IDE есть инструменты рефакторинга в один щелчок, чтобы сделать это действительно легко). _buildItemList вероятно, должен быть виджетом, как и часть с корнем FamilyStreamBuilder .

Мы действительно не теряем гранулярную реконструкцию.
Фактически, хуки улучшают этот аспект, позволяя легко кэшировать экземпляр виджета с помощью useMemoized .

В репо Тима есть несколько примеров, которые это делают.

Я бы, вероятно, разделил этот пример на разные виджеты (в IDE есть инструменты рефакторинга в один щелчок, чтобы сделать это действительно легко). _buildItemList вероятно, должен быть виджетом, как и часть с корнем FamilyStreamBuilder .

Дело в том, что я действительно не хочу этого делать, потому что в этом представлении у меня вообще нет проблем с производительностью. Так что в 100% случаев я предпочитаю подобную микрооптимизации локальность и согласованность кода. Это представление перестраивается только тогда, когда пользователь инициирует действие (~ avg один раз в 10 секунд) или когда данные бэкэнда изменяются и они смотрят на открытый список (почти никогда не происходит). Также бывает, что это просто простое представление, которое в основном представляет собой список, и в этом списке есть множество собственных оптимизаций, выполняемых внутри. Я понимаю, что build () технически может запускаться в любое время, но на практике любые случайные перестроения довольно редки.

Я думаю, что значительно проще работать и отлаживать это представление, если вся эта логика сгруппирована в одном виджете, в основном, чтобы облегчить мою жизнь, когда я вернусь к нему в будущем :)

Следует также отметить, что вложение в основном «вытеснило меня» из метода сборки, так как я никак не мог начать строить свое дерево внутри 3 замыканий и 16 пробелов в отверстии.

И да, вы могли бы сказать, что имеет смысл просто перейти к отдельному виджету. Но почему бы просто не остаться в методе сборки? Если бы мы могли уменьшить шаблон до того, чем он действительно _ должен_ быть, тогда не было бы необходимости иметь удобочитаемость и хлопоты обслуживания, связанные с разделением вещей на 2 файла. (Предполагая, что производительность не является проблемой, чего часто не происходит)

Помните, что в этом сценарии я уже создал настраиваемый виджет для обработки моего Stream Builder. Теперь мне нужно было бы сделать еще один, чтобы обрабатывать состав этих строителей ?? Кажется, это немного преувеличивает.

потому что в этом представлении у меня нет никаких проблем с производительностью

О, я бы не стал преобразовывать его в виджеты для повышения производительности, разработчики уже должны позаботиться об этом. Я бы реорганизовал его для удобства чтения и повторного использования. Я не говорю, что это «правильный» способ сделать это, просто говорю, как я буду структурировать код. Во всяком случае, это ни здесь, ни там.

я никак не мог начать строить свое дерево внутри 3 закрытий и 16 пробелов в отверстии

У меня может просто монитор шире, чем у тебя ...: - /

тогда нет необходимости иметь удобочитаемость и проблемы с обслуживанием, связанные с разделением вещей на 2 файла

Я бы поместил виджеты в один файл FWIW.

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

У меня может просто монитор шире, чем у тебя ...: - /

У меня сверхширокий: D, но dartfmt, очевидно, ограничивает нас всех до 80. Так что проигрыш 16 значителен. Основная проблема заключается в том, что конец моего утверждения - это },);});},),),); не очень весело, когда что-то не так. Я должен быть предельно осторожен при редактировании этой иерархии, и обычные помощники IDE, такие как swap with parent перестают работать.

Я бы поместил виджеты в один файл FWIW.

100%, но я по-прежнему считаю, что вертикальные прыжки в одном файле сложнее поддерживать. Конечно, неизбежно, но мы стараемся сокращать, когда это возможно, и «держать все вместе».

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

dartfmt явно ограничивает нас до 80

Я имею в виду, что это одна из причин, по которой я обычно не использую dartfmt, и когда я использую, я устанавливаю его на 120 или 180 символов ...

Ваш опыт здесь полностью действителен.

Я тоже на самом деле, 120 весь день :) Но pub.dev активно снижает плагины, которые не отформатированы на 80, и у меня создается впечатление, что я (мы) оказываемся в меньшинстве, когда мы меняем это значение.

Что ж, это абсурд, мы должны это исправить.

pub.dev не ставит под сомнение плагины, не поддерживающие dartfmt. Он показывает только комментарий на странице результатов, но это не влияет на оценку.
Но, возможно, с dartfmt больше проблем, чем просто длина строки.

Слишком большая длина строки приводит к тому, что вещи, которые более читабельны в нескольких строках, оказываются в одной строке, например:

object
  ..method()
  ..method2();

который может стать:

object..method()..method2();

Я это вижу?
image
Рассматриваемый пакет: https://pub.dev/packages/sized_context/score

Интересно, что раньше такого точно не было, ведь провайдер какое-то время не пользовался dartfmt.
Я исправился.

Да, это определенно новое поведение, когда я первоначально опубликовал прошлой весной, я убедился, что отмечу все флажки, и dartfmt не требуется.

После всех этих обсуждений, я надеюсь, мы увидим, что нативная поддержка хук-подобных решений не работает. либо useHook или use Hook либо все, что команда трепещет, может посчитать, что их функция не похожа на React 😁🤷‍♂️

мы используем хуки как final controller = useAnimationController(duration: Duration(milliseconds: 800));
Не лучше ли использовать новую программную функцию Darts _Extension_, скопированную из kotlin / swift, чтобы красиво этот синтаксис?

что-то вроде: final controller = AnimationController.use(duration: Duration(milliseconds: 800));
при таком подходе, когда команда флаттера / дротика решает добавить use Hook вместо имеющегося в настоящее время синтаксиса useHook , я думаю, что Annotation к этой функции расширения заставил его читать для использования как
final controller = use AnimationController(duration: Duration(milliseconds: 800));

Также понятно / имеет смысл использовать ключевое слово use например const и new :
new Something
const Something
use Something

в качестве бонуса к этой рекомендации, я думаю, что, наконец, даже функции конструктора / генератора могут использовать / извлекать выгоду из предложенного Annotation . затем компилятор dart с некоторой настройкой преобразует его для поддержки ключевого слова use .

Такая красивая и характерная особенность флаттера / дротика 😉

Правильно ли я предполагаю, что примеры в https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful теперь представляют проблемы, которые люди хотят решить?

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

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

Я назвал это подходом lifecycleMixin, он очень близок к подходу LateProperty, который обсуждался здесь, но главное отличие в том, что в нем реализовано больше жизненных циклов, и его можно легко составлять. (что касается жизненных циклов, я не использовал другие жизненные циклы виджетов, кроме initState и dispose, так что я мог полностью напортачить).

Мне нравится этот подход, потому что:

  1. У него очень небольшие потери времени выполнения.
  2. В пути сборки нет логики / функций, создающих или управляющих состоянием (сборки могут быть чистыми - только получение состояния).
  3. Управление жизненным циклом становится более понятным при оптимизации перестроек с помощью сборщика. (Но вы не жертвуете возможностью повторного использования и компоновки небольших кусочков состояния).
  4. Поскольку вы можете повторно использовать создание битов состояния, библиотека может состоять из обычных битов состояния, которые должны создаваться и удаляться определенным образом, чтобы в вашем собственном коде было меньше шаблонов.

Мне не нравится этот подход (по сравнению с хуками) по следующим причинам:

  1. Я не знаю, охватывает ли он все, на что способны крючки.
  2. Вы должны использовать ключи для однозначной идентификации свойств. (Таким образом, при составлении частей логики, которые создают какое-то состояние, вы должны добавить ключ к ключу, чтобы однозначно идентифицировать каждую часть состояния - это помогает сделать ключ необходимым позиционным параметром, но мне бы хотелось получить решение на уровне языка для доступа к уникальный идентификатор переменной).
  3. Он активно использует расширения для создания многоразовых функций для создания общих битов состояния. И расширения не могут быть автоматически импортированы в IDE.
  4. Вы можете испортить себе жизнь, если смешиваете жизненные циклы разных виджетов / получаете к ним доступ между виджетами без явного управления ими правильно.
  5. Синтаксис построителя немного странный, так что созданное состояние находится в области действия функции сборки, но оставляет функцию сборки чистой.
  6. Я еще не реализовал все примеры, поэтому может быть случай использования, который я не могу описать.

Пример простого счетчика .
Пример анимированных счетчиков

фреймворк
общие биты логики создания повторно используемых состояний

Я не уверен, сколько у меня времени, учеба в аспирантуре всегда занимает меня, но мне бы хотелось получить отзывы. @rrousselGit Насколько это близко к

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

@TimWhiting Основная проблема, с которой я столкнулся с этим подходом, - это отсутствие надежности. Важным фактором здесь является потребность в надежности строителей в сжатой форме. Волшебный идентификатор и способность конфликтовать в жизненном цикле создают новые векторы для возникновения ошибок, и я бы продолжал рекомендовать своей команде использовать конструкторы, поскольку, несмотря на то, что их довольно неприятно читать, по крайней мере, мы знаем, что их 100 % без ошибок.

Что касается примеров, я по-прежнему считаю, что идеальным примером является простое использование AnimationController со значением продолжительности, привязанным к виджету. Держит его простым и знакомым. Нет необходимости углубляться в более эзотерический подход, это идеальный небольшой пример использования повторно используемого шаблона, для этого нужны хуки жизненного цикла, и все решения можно легко оценить по их способности кратко использовать несколько анимаций.

Все остальное - всего лишь вариант того же варианта использования «Контроллер с отслеживанием состояния». Я хочу сделать X в initState и Y в состоянии удаления и обновить Z при изменении моих зависимостей. Неважно, что такое X, Y и Z.

Интересно, может ли @rrousselGit дать здесь какое-то представление или какие-либо данные о том, какие хуки в настоящее время используются чаще всего. Я предполагаю, что это 80% потока и анимации, но было бы неплохо знать, что люди используют больше всего.

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

Так же, как мы могли бы сделать:

var anim = get AnimationController();
return Column(
  _someExpensiveBuildMethod(),
  FadeTransition(opacity: anim, child: ...)
)

Мы всегда могли:

var foo = get ComplicatedThingController();
return Column(
  _someExpensiveBuildMethod(),
  ComplicatedThing(controller: foo, child: ...)
)

@esDotDev Я согласен, ключи и синтаксис компоновщика являются основным недостатком подхода lifecycleMixin. Я не знаю, сможете ли вы обойти это, кроме как с помощью подхода в стиле хуков со связанными с ним ограничениями или изменения языка для возможности связывать объявления переменных с битами состояния с жизненными циклами. Вот почему я продолжу использовать хуки и позволю другим использовать виджеты с отслеживанием состояния, если не будет найдено лучшее решение. Тем не менее, я думаю, что это интересная альтернатива для тех, кому не нравятся ограничения хуков, хотя у нее есть свои ограничения.

Правильно ли я предполагаю, что примеры в https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful теперь представляют проблемы, которые люди хотят решить?

Честно говоря, я не уверен.
Я бы сказал _да_. Но это действительно зависит от того, как вы интерпретируете эти примеры.

В этой беседе у нас есть история непонимания друг друга, поэтому я не могу гарантировать, что это больше не повторится.

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

Интересно, может ли @rrousselGit дать здесь какое-то представление или какие-либо данные о том, какие хуки в настоящее время используются чаще всего. Я предполагаю, что это 80% потока и анимации, но было бы неплохо знать, что люди используют больше всего.

Я считаю, что это очень однородно.

Хотя во всяком случае, useStream и Animations, вероятно, используются меньше всего:

  • useStream обычно имеет лучший эквивалент в зависимости от вашей архитектуры. Можно использовать context.watch , useBloc , useProvider , ...
  • мало кто тратит время на создание анимации. Это редко является приоритетом, и TweenAnimationBuilder другие неявно анимированные виджеты покрывают большую часть потребности.
    Возможно, это изменится, если я добавлю свои useImplicitlyAnimatedInt хуки в flutter_hooks.

@esDotDev Просто удалила необходимость в ключах / идентификаторах в подходе lifecycleMixin. В синтаксисе компоновщика все еще немного неудобно. Но, возможно, в конечном итоге и этому можно было бы помочь. Единственная проблема, с которой я столкнулся, связана с системой типов. Он пытается разыграть вещи определенными способами, которые не работают. Но, вероятно, для этого просто требуется тщательное приведение типов или владение системой типов. Что касается смешивания жизненных циклов, я думаю, что это можно улучшить, выбрасывая некоторые разумные исключения, когда конкретная часть состояния, к которой вы пытаетесь получить доступ, недоступна для жизненного цикла этого виджета. Или линт о том, что в жизненном цикле построителя вы должны иметь доступ только к жизненному циклу построителя.

Спасибо, Реми, это меня удивляет, я думаю, что люди будут очень часто использовать анимацию для управления большой коллекцией виджетов Transition в ядре, но я думаю, что большинство людей просто используют различные неявные виджеты, так как они довольно приятны для чтения и не имеют вложенности .

Тем не менее, несмотря на то, что AnimatorController очень хорошо обслуживается набором неявных и явных виджетов, я по-прежнему считаю, что это отличный пример «вещи, которая должна поддерживать состояние и связываться с параметрами и жизненным циклом виджета». И служит прекрасным маленьким примером проблемы, которую нужно решить (факт полностью решен во Flutter, несмотря на дюжину виджетов), который мы все можем обсудить и сосредоточиться на архитектуре, а не на контенте.

Например, подумайте, как, если бы var anim = AnimationController.use(context, duration: widget.duration ?? _duration); был гражданином первого класса, фактически ни одна из этих неявных или явных анимаций действительно не существовала. Это делает их избыточными, поскольку все они созданы для решения основной проблемы: легкого создания объекта с сохранением состояния (AnimationController) в контексте виджета. TAB становится почти бессмысленным, поскольку вы можете сделать то же самое с AnimatedBuilder + AnimatorController.use() .

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

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

Как бы то ни было, я часто использую в своем приложении что-то вроде useAnimation а не обычные виджеты анимации. Это потому, что я использую SpringAnimation, который плохо поддерживается, например, такими виджетами, как AnimatedContainer; все они предполагают анимацию, основанную на времени, с curve и duration а не анимацию на основе моделирования, которая принимает аргумент Simulation .

Я сделал абстракцию над useAnimation но с пружинами, поэтому я назвал ее useSpringAnimation . Виджет-оболочка, с которым я использовал этот хук, похож на AnimatedContainer но его было намного проще сделать, потому что я мог повторно использовать весь код анимации, как вы говорите @esDotDev , поскольку большая часть логики остается той же. Я мог бы даже создать свою собственную версию всех анимированных виджетов, снова используя useSpringAnimation но мне это не обязательно было нужно для моего проекта. Это еще раз показывает мощь повторного использования логики жизненного цикла, которую обеспечивают хуки.

Например, рассмотрим, как, если var anim = AnimationController.use (context, duration: widget.duration ?? _duration); были первоклассным гражданином, и фактически ни одна из этих явных или неявных анимаций действительно не нуждалась в существовании. Это делает их избыточными, поскольку все они созданы для решения основной проблемы: легкого создания объекта с сохранением состояния (AnimationController) в контексте виджета. TAB становится почти бессмысленным, поскольку вы можете делать то же самое с AnimatedBuilder + AnimatorController.use ().

Читая мои комментарии выше, кажется, что это в основном именно то, что я сделал со своим весенним анимационным крючком. Я инкапсулировал логику, а затем просто использовал AnimatedBuilder. Чтобы сделать их неявными, чтобы когда я изменил опору, как в AnimatedContainer, она анимировалась, я просто добавил метод didUpdateWidget (называемый didUpdateHook в flutter_hooks ) в запустить анимацию от старого значения к новому значению.

Правильно ли я предполагаю, что примеры в https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful теперь представляют проблемы, которые люди хотят решить?

Честно говоря, я не уверен.
Я бы сказал _да_. Но это действительно зависит от того, как вы интерпретируете эти примеры.

В этой беседе у нас есть история непонимания друг друга, поэтому я не могу гарантировать, что это больше не повторится.

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

Я бы также сказал, что мы должны включить в этот вопрос все образцы кода, которые обсуждались, я думаю, что где-то выше есть список, созданный @rrousselGit . Я мог бы сделать PR, добавив их в репозиторий local_state, но они не все являются полными примерами кода, поэтому не все они могут компилироваться и запускаться. Но, по крайней мере, они показывают потенциальные проблемы.

Я мог бы сделать пиар, добавив их в репозиторий local_state

Это было бы очень полезно.

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

Мы только показали, что повторное использование _не_ в отношении Flutter.

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

Повторное использование может быть просто определено как: _ Все, что может делать виджет-строитель. _

Запрос относится к некоторому объекту с отслеживанием состояния, который может существовать внутри любого виджета, а именно:

  • Инкапсулирует собственное состояние
  • Может настраивать / демонтировать себя в соответствии с вызовами initState / dispose
  • Может реагировать на изменение зависимостей в виджете

И делает это красиво, лаконично, легко готово, без шаблонов, например:
AnimationController anim = AnimationController.stateful(duration: widget.duration);
Если это работает в виджетах Stateless и Stateful. Если он перестраивается при изменении widget.something, если он может запускать собственные init () и dispose (), то у вас в основном есть победитель, и я уверен, что все это оценят.

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

Я почти уверен, что это не проблема. Мы бы сделали это так же, как сейчас работают виджеты XTransition . Если бы у меня было какое-то сложное состояние, и я хотел бы, чтобы у него был какой-то дорогой дочерний элемент, я бы просто сделал для него небольшой виджет-оболочку. Так же, как мы могли бы сделать:
FadeTransition(opacity: anim, child: someChild)

Мы можем так же легко сделать это с любой вещью, которую мы хотим визуализировать, передав эту «вещь» в виджет для повторной визуализации.
MyThingRenderer(value: thing, child: someChild)

  • Это не _require_ вложенности, как это делает Builder, но при необходимости поддерживает его (.child может быть сборкой fxn)
  • Он сохраняет возможность использования напрямую без виджета-оболочки.
  • Мы всегда можем создать конструктор и использовать этот синтаксис внутри конструктора, чтобы он был более чистым. Это также открывает двери для нескольких типов построителей, построенных на одном и том же базовом объекте, которые не требуют копирования и вставки кода повсюду.

Согласен с @esDotDev. Как я упоминал ранее, альтернативным названием для этого может быть «Синтаксический сахар для строителей».

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

Я почти уверен, что это не проблема. Мы бы сделали это так же, как сейчас работают виджеты XTransition . Если бы у меня было какое-то сложное состояние, и я хотел бы, чтобы у него был какой-то дорогой дочерний элемент, я бы просто сделал для него небольшой виджет-оболочку. Так же, как мы могли бы сделать:

В этом нет необходимости.
Одним из преимуществ этой функции является то, что мы можем иметь логику состояния, которая «кэширует экземпляр виджета, если его параметры не изменились».

С хуками это будет useMemo в React:

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

С помощью этого кода myWidget будет перестраиваться _только_ при изменении value . Даже если виджет, вызывающий useMemo перестраивается по другим причинам.

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

В репо Тима есть такой пример.

Запрос относится к некоторому объекту с отслеживанием состояния, который может существовать внутри любого виджета, а именно:

  • Инкапсулирует собственное состояние
  • Может настраивать / демонтировать себя в соответствии с вызовами initState / dispose
  • Может реагировать на изменение зависимостей в виджете

Думаю, мне трудно понять, почему по этим параметрам StatefulWidget не выполняет свою работу лучше, чем это делает. Вот почему я задал вопрос о том, что нам действительно нужно в решении. Как человек, использующий flutter_hooks я считаю, что с ними гораздо интереснее работать, чем с StatefulWidget , но это просто для того, чтобы избежать многословия, а не потому, что я думаю в терминах хуков. На самом деле я нахожу сложным рассуждение об обновлениях пользовательского интерфейса с помощью хуков по сравнению с Widget s.

  • Может реагировать на изменение зависимостей в виджете

Вы имеете в виду зависимость, которая была создана / получена внутри виджета? Или зависимость далеко ниже виджета в дереве?

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

Поскольку использование SW для этого подходит для конкретного случая использования, но не подходит для абстрагирования многократно используемой логики варианта использования во многих SW. В качестве примера возьмем установку / разборку анимации. Это не само ПО, это то, что мы хотим использовать в них. Без первоклассной поддержки для совместного использования инкапсулированного состояния вам придется создать конструктор, то есть TweenAnimationBuilder, или создать тонну конкретных виджетов, например AnimatedContainer и т. Д. это так, как вы хотите внутри дерева.

С точки зрения зависимости виджета, я просто имею в виду, что если widget.foo изменяется, объект с сохранением состояния получает возможность выполнить любое необходимое обновление. В случае stateful AnimationController он проверит, изменилась ли продолжительность, и, если это так, обновит его внутренний экземпляр AnimatorController. Это избавляет каждого разработчика анимации от необходимости обрабатывать изменение свойства.

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

С этим кодом myWidget будет перестраиваться _ только_ при изменении value . Даже если виджет, вызывающий useMemo перестраивается по другим причинам.

А, я вижу, Memoized возвращает сам виджет, а затем вы передаете [значение] в качестве триггера перестроения, аккуратно!

Ключевым моментом в AnimatedOpacity является ни родительская, ни дочерняя перестройка. Фактически, когда вы запускаете анимацию с помощью AnimatedOpacity, буквально ничего не перестраивается после первого кадра, в котором вы запускаете анимацию. Мы полностью пропускаем этап сборки и делаем все это в объекте рендеринга (а в дереве рендеринга это только перерисовка, а не ретрансляция, и на самом деле он использует слой, поэтому даже отрисовка довольно минимальна). Это существенно влияет на производительность и использование батареи. Какое бы решение мы здесь ни предлагали, оно должно поддерживать такую ​​производительность, если мы хотим встроить его в основную структуру.

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

Что касается производительности определения хуков внутри метода сборки / рендеринга (который, я думаю, кто-то упоминал ранее в этом выпуске), я читал документы React и видел этот FAQ, может быть полезно. В основном он спрашивает, работают ли хуки медленно из-за создания функций при каждом рендеринге, и они отвечают «нет» по нескольким причинам, одна из которых - возможность запоминать функции с помощью таких хуков, как useMemo или useCallback .

https://reactjs.org/docs/hooks-faq.html#are -hooks-slow-because-of-Creating-functions-in-render

В основном он спрашивает, работают ли хуки медленно из-за создания функций при каждом рендеринге, и они отвечают «нет» по нескольким причинам, одна из которых - возможность запоминать функции с помощью таких хуков, как useMemo или useCallback .

Беспокойство не о стоимости создания закрытий, они действительно относительно дешевы. Разница между запуском любого кода и его отсутствием является ключом к производительности, которую Flutter демонстрирует сегодня в оптимальных случаях. Мы потратили много усилий на создание алгоритмов, которые буквально вообще избегают запуска определенных путей кода (например, этап сборки полностью пропускается для AnimatedOpacity или то, как мы избегаем обхода дерева для выполнения обновлений, а вместо этого просто нацеливаемся на затронутые узлы).

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

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

Как упоминалось ранее несколько раз, хуки улучшают это.
Анимированный пример репо Тима - тому подтверждение. Вариант хуков перестраивается реже, чем вариант StatefulWidget, благодаря useMemo

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

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

Это намного более читабельно IMO. В настоящее время вам нужно объявить два класса с виджетом с отслеживанием состояния, а не хуками, в которые вы просто добавляете usestate.

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

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

Я думаю, что определенно есть возможность улучшить соглашения об именах и сделать их более плавными.

Такие вещи, как UseMemoized и UseEffect, звучат довольно чуждо, и похоже, что мы хотим каким-то образом не запускать код init () в сборке fxn.

В настоящее время инициализация с помощью хуков выглядит так (я думаю?):

Widget build(){
   useEffect(
      (){
          // Do init stuff
         return (){  //Do dispose stuff };
      }, [ ] ) //<-- pass an empty list of rebuild triggers, so this can only fire once. Passing null here would let it fire every time.
   );
}

Я ценю краткость этого кода, но он определенно далеко не идеален с точки зрения удобочитаемости и "самодокументируемого кода". Здесь происходит много неявной магии. В идеале у нас есть что-то, что явно описывает его хуки init / dispose и не заставляет себя создавать при использовании с виджетом без сохранения состояния.

Такие вещи, как useMemoized и useEffect, можно было бы назвать более явно hook ComputedValue() и hook SideEffect()

Widget build(BuildContext context){
   List<int> volumes = hook ComputedValue(
        execute: ()=>_getVolumeFromAudioSamples(widget.bytes), 
        dependencies: [ widget.bytes ]);

   hook SideEffect(
       execute: ()=>_recordSongChangedAnalytics()
       dependencies: [ widget.songUrl ]);
   )

   return SongVisualizer(volumes: volumes);
}

Мне это нравится, хотя я не знаю, как я отношусь к использованию ключевого слова hook , и я не думаю, что это решает проблему чужеродных концепций. На мой взгляд, введение новых ключевых слов не кажется лучшим подходом, withSideEffect или withComputedValue ? Я не языковый дизайнер, поэтому мои слова ничего не стоят.

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

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

Я думаю, у этой медали две стороны. Новое ключевое слово - это важное событие, поэтому распространение знаний будет очень быстрым, но с другой стороны, это, безусловно, что-то новое для _все_. Если можно без этого тоже круто! Просто не уверен, что это так ... по крайней мере, не так элегантно.

Мнение: Склонность сообщества называть крючки де-факто решением этой проблемы коренится в предвзятости к функциям. Функции проще составлять, чем объекты, особенно в языке со статической типизацией. Я думаю, что ментальная модель виджетов для многих разработчиков - это просто метод build .

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

Что касается ключевого слова hook с точки зрения основ; можно было бы рассматривать это как объявление и определение функции из какого-то шаблона (макроса), а префикс hook самом деле просто указывает, что встроенная функция имеет внутреннее состояние (статика в стиле c. )

Интересно, нет ли в Swift FunctionBuilders какого-то предшествующего уровня техники.

Пока мы мечтаем, я уточню свое предположение о том, какой будет необходимый код:

Hook SideEffect(void Function() execute, List<Object> dependencies) {
  // Whatever happens each build.
}

Widget build(BuildContext context){
   List<int> volumes = hook ComputedValue(
        execute: ()=>_getVolumeFromAudioSamples(widget.bytes), 
        dependencies: [ widget.bytes ]);

   SideEffect(
       execute: ()=>_recordSongChangedAnalytics()
       dependencies: [ widget.songUrl ]);
   )

   return SongVisualizer(volumes: volumes);
}

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

Я немного съеживаюсь, когда пишу это, потому что это такая странность с точки зрения языка. Опять же, Dart - это язык, рожденный для написания пользовательских интерфейсов. Если такая странность и должна существовать где-нибудь, возможно, это место. Только не эту странность в частности.

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

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

Интересная статья про хуки React

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

Я предлагаю вам прочитать весь разговор, чтобы понять различные моменты.

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

Хотя это может быть правдой, эта проблема показывает, что есть проблемы, которые нельзя решить с помощью виджетов в том виде, в каком они есть в настоящее время, поэтому, если мы хотим решить проблемы, у нас нет другого выбора, кроме как создать что-то новое. Это та же концепция, что и при наличии Future s и более позднем введении синтаксиса async/await , последний делает возможным то, что было бы просто невозможно без нового синтаксиса.

Однако люди _are_ предлагают сделать это частью фреймворка. React не может добавить новый синтаксис в Javascript, потому что это не единственный доступный фреймворк (ну, он может через преобразования Babel), но Dart специально разработан для работы с Flutter (по крайней мере, Dart 2, а не исходная версия), поэтому у нас есть гораздо больше возможности заставить хуки работать вместе с основным языком. React, например, нуждается в Babel для JSX, и он должен использовать линтер для ошибок useEffect , в то время как мы могли бы сделать это ошибкой времени компиляции. Наличие пакета значительно усложняет внедрение, поскольку вы можете себе представить, какую тягу могли бы (не получить) обработчики React, если бы это был сторонний пакет.

Не было бы проблем, если бы существовал виджет третьего типа, то есть HookWidget, помимо текущих виджетов Stateless и Stateful. Пусть сообщество решит, какой из них использовать. Пакет от Реми уже есть, но он неизбежно имеет ограничения. Я попробовал это, и он значительно уменьшил шаблон, но мне пришлось отказаться от него безоговорочно из-за ограничений. Мне нужно создать виджеты с отслеживанием состояния только для использования метода инициализации. Могут быть дополнительные большие преимущества, если он будет частью базовой структуры с поддержкой языка. Кроме того, HookWidget может позволить сообществу создавать более оптимальные и более производительные приложения.

Мне нужно создать виджеты с отслеживанием состояния только для использования метода инициализации.

На самом деле вам не нужно этого делать, useEffect () может выполнять initCall внутри сборки. Документация вообще не пытается объяснить это, и в основном предполагает, что вы разработчик React, который уже знает, как работают хуки.

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

Была ли эта страница полезной?
0 / 5 - 0 рейтинги