Flutter: 重用状态逻辑要么太冗长,要么太难

创建于 2020-03-02  ·  420评论  ·  资料来源: flutter/flutter

.

与关于钩子的讨论有关 #25280

TL;DR:很难重用State逻辑。 我们要么最终得到一个复杂且深度嵌套的build方法,要么必须在多个小部件之间复制粘贴逻辑。

不可能通过 mixin 或函数重用这种逻辑。

问题

跨多个StatefulWidget重用State逻辑非常困难,只要该逻辑依赖于多个生命周期。

一个典型的例子是创建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 问题

分解此逻辑的第一次尝试是使用 mixin:

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,
    );
  }
}

但这有不同的缺陷:

  • 一个 mixin 每个类只能使用一次。 如果我们的StatefulWidget需要多个TextEditingController ,那么我们就不能再使用 mixin 方法了。

  • mixin 声明的“状态”可能与另一个 mixin 或State本身发生冲突。
    更具体地说,如果两个 mixin 使用相同的名称声明一个成员,则会发生冲突。
    最坏的情况是,如果冲突的成员具有相同的类型,这将默默地失败。

这使得 mixin 既不理想又太危险,无法成为真正的解决方案。

使用“建造者”模式

另一种解决方案可能是使用与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,
        );
      },
    );
  }
}

这解决了 mixins 遇到的问题。 但它会产生其他问题。

  • 用法非常冗长。 对于单个变量声明,这实际上是 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),
              ],
            );
          },
        );
      },
    );
    }
    

    这是一个非常缩进的代码,只是为了声明两个变量。

  • 这会增加一些开销,因为我们有一个额外的StateElement实例。

  • 这是很难用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 的角度添加一些想法。
如果它们不相关,请原谅,但我想简要解释一下我们如何看待 Hooks。

钩子绝对是“隐藏”的东西。 或者,根据您的看法,将它们封装起来。 特别是,它们封装了局部状态和效果(我认为我们的“效果”与“一次性用品”是一样的)。 “隐式”在于它们自动将生命周期附加到调用它们的组件中。

这种隐性不是模型固有的。 你可以想象一个参数被明确地贯穿所有调用——从组件本身到自定义 Hook,一直到每个原始 Hook。 但在实践中,我们发现它很嘈杂,实际上并没有用。 所以我们让当前正在执行的 Component 隐含全局状态。 这类似于throw在 VM 中向上搜索最近的catch块而不是在代码中传递errorHandlerFrame的方式。

好的,所以它的函数内部具有隐式隐藏状态,这看起来很糟糕吗? 但是在 React 中,组件也是如此。 这就是组件的全部意义所在。 它们是具有与其关联的生命周期的函数(对应于 UI 树中的一个位置)。 组件本身不是状态方面的枪支的原因是您不只是从随机代码中调用它们。 您可以从其他组件调用它们。 因此它们的生命周期是有意义的,因为您仍然处于 UI 代码的上下文中。

然而,并非所有问题都是组件形状的。 组件结合了两种能力:状态+效果,以及与树位置相关的生命周期。 但是我们发现第一个能力本身就很有用。 就像函数在一般情况下很有用一样,因为它们可以让你封装代码,我们缺乏一个原语,可以让我们封装(和重用)状态+效果包,而不必在树中创建新节点。 这就是Hooks。 组件 = Hooks + 返回的 UI。

正如我所提到的,隐藏上下文状态的任意函数是可怕的。 这就是我们通过 linter 强制执行约定的原因。 Hook 有“颜色” ——如果你使用 Hook,你的函数也是一个 Hook。 并且 linter 强制只有组件或其他 Hook 可以使用 Hook。 这消除了任意函数​​隐藏上下文 UI 状态的问题,因为现在它们并不比组件本身更隐含。

从概念上讲,我们不将 Hook 调用视为普通的函数调用。 如果我们有语法,就像useState()更像use State() 。 这将是一个语言功能。 您可以使用具有效果跟踪的语言对 Hooks with Algebraic Effects 之类的东西进行建模。 所以从这个意义上说,它们是常规函数,但它们“使用”状态的事实将成为它们类型签名的一部分。 然后你可以把 React 本身看作是这个效果的“处理程序”。 无论如何,这是非常理论化的,但我想在编程模型方面指出现有技术。

实际上,这里有一些事情。 首先,值得注意的是 Hooks 不是 React 的“额外”API。 在这一点上,它们是用于编写组件React API。 我想我同意,作为一个额外的功能,它们不会很引人注目。 所以我不知道它们对 Flutter 是否真的有意义,因为 Flutter 具有可以说是不同的整体范式。

至于它们允许什么,我认为关键特性是能够封装状态+有效逻辑,然后像使用常规函数组合一样将它们链接在一起。 因为原语是为组合而设计的,所以您可以采用一些 Hook 输出,如useState() ,将其作为输入传递给自定义useSpring(gesture) useGesture(state) ,然后将其作为输入传递给多个自定义useSpring(gesture)调用会给你提供交错的值,等等。 这些部分中的每一个都完全不知道其他部分,可能由不同的人编写,但它们组合得很好,因为状态和效果被封装并“附加”到封闭的组件。 这是一个类似这样的小演示,以及一篇我简要回顾一下 Hooks 是什么的

我想强调这不是关于减少样板,而是关于动态组合有状态封装逻辑的管道的能力。 请注意,它是完全反应式的——即它不会运行一次,但它会随着时间的推移对属性的所有变化做出反应。 考虑它们的一种方式是它们就像音频信号管道中的插件。 虽然我在实践中完全对“具有记忆的功能”持谨慎态度,但我们还没有发现这是一个问题,因为它们是完全孤立的。 事实上,这种隔离是他们的主要特征。 不然就会分崩离析。 因此,任何相互依赖都必须通过返回值并将其传递给链中的下一个事物来明确表达。 从第三方库的角度来看,任何自定义 Hook 都可以在不破坏(甚至影响)其消费者的情况下添加或删除状态或效果这一事实是另一个重要功能。

我不知道这是否有帮助,但希望它对编程模型有所帮助。
很高兴回答其他问题。

所有420条评论

抄送@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();
  }
}

这摆脱了一些重复的代码行。 您可以为调试属性编写一个类似的抽象类,甚至可以将两者结合起来。 您的 init 状态可能最终看起来像:

<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)

框架中有很多我们想要重用状态逻辑的例子:

  • 流生成器
  • 补间动画生成器
    ...

这些只不过是一种通过更新机制重用状态的方法。

但是他们遇到了与“builder”部分提到的问题相同的问题。

这会导致很多问题。
例如,Stackoverflow 上最常见的问题之一是人们试图使用StreamBuilder来产生副作用,例如“推动更改路线”。

最终他们唯一的解决方案是“弹出”StreamBuilder。
这包括:

  • 将小部件转换为有状态
  • 手动监听 initState+didUpdateWidget+didChangeDependencies 中的流
  • 当流改变时取消之前对 didChangeDependencies/didUpdateWidget 的订阅
  • 在处置时取消订阅

这是_很多工作_,而且它实际上是不可重复使用的。

问题

跨多个StatefulWidget重用State逻辑非常困难,只要该逻辑依赖于多个生命周期。

一个典型的例子是创建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 应用程序,但它看起来真的不是什么大问题? 即使在最坏的情况下,声明一个属性,初始化它,处置它,并将其报告给调试数据也是四行(实际上通常更少,因为你通常可以在初始化它的同一行声明它,应用程序一般不需要担心向调试属性添加状态,并且这些对象中的许多没有需要处理的状态)。

我同意每个属性类型的 mixin 不起作用。 我同意构建器模式不好(它实际上使用与上述最坏情况相同的行数)。

使用 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 隐藏了监听机制并跟踪当前的 Snapshot 是什么。
    在两个Future之间切换的逻辑也相当复杂,因为它没有subscription.close()
  • AnimatedContainer 隐藏了在先前值和新值之间进行补间的逻辑。
  • Listview 隐藏了“在出现时挂载小部件”的逻辑

应用程序通常不需要担心向调试属性添加状态

他们不这样做,因为他们不想处理维护 debugFillProperties 方法的复杂性。
但是,如果我们告诉开发人员“您希望 Flutter 的开发工具中的所有参数和状态属性都是开箱即用的吗?” 我敢肯定他们会说是的

许多人向我表达了他们对真正等同于 React 开发工具的渴望。 Flutter 的 devtool 还没有出现。
在 React 中,我们可以查看小部件的所有状态及其参数,并对其进行编辑,无需做任何事情。

同样,当我告诉他们使用provider + 我的其他一些包时,默认情况下他们的整个应用程序状态对他们是可见的,而无需做任何事情(模这个烦人的 devtool 错误)时,人们感到非常惊讶。

我不得不承认我不是 FutureBuilder 的忠实粉丝,它会导致很多错误,因为人们不会考虑何时触发 Future。 我认为我们放弃对它的支持并不是没有道理的。 我想 StreamBuilder 还可以,但是我认为 Streams 本身太复杂了(正如你在上面的评论中提到的)所以......

为什么有人必须考虑创建 Tweens 的复杂性?

ListView 并没有真正隐藏挂载小部件的逻辑; 它是 API 的重要组成部分。

问题不在于行数,而在于这些行是什么。

我真的不明白这里的担忧。 这些行看起来很像简单的样板。 声明事物,初始化事物,处置事物。 如果不是行数,那是什么问题?

我同意你的观点,FutureBuilder 是有问题的。

这有点离题,但我建议在开发中,Flutter 应该每隔几秒触发一次假热重载。 这将突出显示 FutureBuilder、密钥等的滥用。

为什么有人必须考虑创建 Tweens 的复杂性?

ListView 并没有真正隐藏挂载小部件的逻辑; 它是 API 的重要组成部分。

我们同意这一点。 我的观点是,我们不能用“它隐藏逻辑”来批评钩子之类的东西,因为钩子所做的事情严格等同于TweenAnimationBuilder / AnimatedContainer /... 所做的事情。
逻辑没有隐藏

最后,我认为动画是一个很好的比较。 动画具有隐式与显式的概念。
隐式动画因其简单性、可组合性和可读性而受到喜爱。
显式动画更灵活,但更复杂。

当我们将这个概念转化为监听流时, StreamBuilder是一个_隐式监听_,而stream.listen是_显式监听_。

更具体地说,使用StreamBuilder您_不能_忘记处理流更改的场景,或者忘记关闭订阅。
您也可以将多个StreamBuilder组合在一起

stream.listen稍微高级一些,也更容易出错。

构建器功能强大,可以简化应用程序。
但是正如我们之前所同意的,Builder 模式并不理想。 编写和使用都很冗长。
这个问题,以及钩子解决了什么,是关于 *Builders 的替代语法

例如, flutter_hooks有严格等价于FutureBuilderStreamBuilder

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

在延续中, AnimatedContainer等可以用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();
}

钩子创建它并处理它。

我不确定这有什么不清楚的。

对,就是这样。 我不知道该代码的控制器生命周期是什么。 它会持续到词法范围的末尾吗? 国家的一生? 还有什么? 谁拥有它? 如果我把它传给其他人,他们可以拥有所有权吗? 这些在代码本身中都不是显而易见的。

看起来您的争论更多是由于对钩子的作用缺乏了解而不是真正的问题。
这些问题有一个明确定义的答案,与所有钩子一致:

我不知道该代码的控制器生命周期是什么

你也不必考虑它。 这不再是开发商的责任。

它会持续到词法范围的末尾吗? 国家的一生

国家的一生

谁拥有它?

钩子拥有控制器。 它是useTextEditingController API 的一部分,它拥有控制器。
这适用于useFocusNodeuseScrollControlleruseAnimationController 、 ...

在某种程度上,这些问题适用于StreamBuilder

  • 我们不必考虑 StreamSubscription 的生命周期
  • 订阅持续国家的生命周期
  • StreamBuilder 拥有 StreamSubscription

一般来说,你可以想到:

final value = useX(argument);

严格等同于:

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

  },
);

他们有相同的规则和相同的行为。

不再是开发商的责任

我认为从根本上说,这就是这里的分歧。 有一个类似函数的 API 返回一个值,该值的定义生命周期尚不清楚,恕我直言,与基于将该值传递给闭包的 API 完全不同。

我对有人创建使用这种风格的包没有任何问题,但这种风格与我希望包含在核心 flutter API 中的风格相反。

@Hixie
我不认为@rrousselGit所说的是它们是同一件事,而只是它们在生命周期方面具有“相同的规则和相同的行为”? 正确的?

但是,它们并不能解决相同的问题。

也许我在这里错了,但去年秋天在尝试 flutter 时,我相信如果我需要在一个小部件中使用三个构建器,那将会有很多嵌套。 与三个钩子(三行)相比。
还。 钩子是可组合的,所以如果你需要共享由多个钩子组成的状态逻辑,你可以创建一个使用其他钩子和一些额外逻辑的新钩子,然后只使用一个新的钩子。

像在小部件之间轻松共享状态逻辑之类的东西是我在 2019 年秋季尝试 flutter 时缺少的东西。

当然可能还有很多其他可能的解决方案。 也许它已经解决了,我只是没有在文档中找到它。
但是,如果不是,如果像钩子或其他解决方案一样可以作为一等公民使用,那么可以做很多事情来大大加快开发速度。

我绝对不建议使用构建器方法,正如 OP 所提到的那样,它有各种各样的问题。 我建议只使用 initState/dispose。 我真的不明白为什么这是一个问题。

我很好奇人们对https://github.com/flutter/flutter/issues/51752#issuecomment -664787791 中的代码的感受。 我不认为它比 initState/dispose 更好,但是如果人们喜欢钩子,他们也喜欢它吗? 钩子更好吗? 更差?

@Hixie Hooks 很好用,因为它们将生命周期划分为单个函数调用。 如果我使用钩子,比如useAnimationController ,我就不必再考虑 initState 和 dispose 了。 它免除了开发者的责任。 我不必担心我是否处理了我创建的每个动画控制器。

initStatedispose对单一事物很好,但想象一下必须跟踪多种不同类型的状态。 钩子基于抽象的逻辑单元进行组合,而不是在类的生命周期中散布它们。

我认为您所问的问题相当于我们每次都可以手动处理效果时为什么要使用函数。 我同意它并不完全相同,但总体感觉相似。 看来你之前没有使用过钩子,所以问题对你来说似乎不太明显,所以我鼓励你使用钩子做一个中小型项目,也许用flutter_hooks包,然后看看感觉怎么样。 我是在所有尊重的情况下这么说的,作为 Flutter 的用户,我遇到了钩子提供解决方案的这些问题,就像其他人一样。 我不确定如何让您相信这些问题对我们来说确实存在,如果有更好的方法,请告诉我们。

我将从 React 的角度添加一些想法。
如果它们不相关,请原谅,但我想简要解释一下我们如何看待 Hooks。

钩子绝对是“隐藏”的东西。 或者,根据您的看法,将它们封装起来。 特别是,它们封装了局部状态和效果(我认为我们的“效果”与“一次性用品”是一样的)。 “隐式”在于它们自动将生命周期附加到调用它们的组件中。

这种隐性不是模型固有的。 你可以想象一个参数被明确地贯穿所有调用——从组件本身到自定义 Hook,一直到每个原始 Hook。 但在实践中,我们发现它很嘈杂,实际上并没有用。 所以我们让当前正在执行的 Component 隐含全局状态。 这类似于throw在 VM 中向上搜索最近的catch块而不是在代码中传递errorHandlerFrame的方式。

好的,所以它的函数内部具有隐式隐藏状态,这看起来很糟糕吗? 但是在 React 中,组件也是如此。 这就是组件的全部意义所在。 它们是具有与其关联的生命周期的函数(对应于 UI 树中的一个位置)。 组件本身不是状态方面的枪支的原因是您不只是从随机代码中调用它们。 您可以从其他组件调用它们。 因此它们的生命周期是有意义的,因为您仍然处于 UI 代码的上下文中。

然而,并非所有问题都是组件形状的。 组件结合了两种能力:状态+效果,以及与树位置相关的生命周期。 但是我们发现第一个能力本身就很有用。 就像函数在一般情况下很有用一样,因为它们可以让你封装代码,我们缺乏一个原语,可以让我们封装(和重用)状态+效果包,而不必在树中创建新节点。 这就是Hooks。 组件 = Hooks + 返回的 UI。

正如我所提到的,隐藏上下文状态的任意函数是可怕的。 这就是我们通过 linter 强制执行约定的原因。 Hook 有“颜色” ——如果你使用 Hook,你的函数也是一个 Hook。 并且 linter 强制只有组件或其他 Hook 可以使用 Hook。 这消除了任意函数​​隐藏上下文 UI 状态的问题,因为现在它们并不比组件本身更隐含。

从概念上讲,我们不将 Hook 调用视为普通的函数调用。 如果我们有语法,就像useState()更像use State() 。 这将是一个语言功能。 您可以使用具有效果跟踪的语言对 Hooks with Algebraic Effects 之类的东西进行建模。 所以从这个意义上说,它们是常规函数,但它们“使用”状态的事实将成为它们类型签名的一部分。 然后你可以把 React 本身看作是这个效果的“处理程序”。 无论如何,这是非常理论化的,但我想在编程模型方面指出现有技术。

实际上,这里有一些事情。 首先,值得注意的是 Hooks 不是 React 的“额外”API。 在这一点上,它们是用于编写组件React API。 我想我同意,作为一个额外的功能,它们不会很引人注目。 所以我不知道它们对 Flutter 是否真的有意义,因为 Flutter 具有可以说是不同的整体范式。

至于它们允许什么,我认为关键特性是能够封装状态+有效逻辑,然后像使用常规函数组合一样将它们链接在一起。 因为原语是为组合而设计的,所以您可以采用一些 Hook 输出,如useState() ,将其作为输入传递给自定义useSpring(gesture) useGesture(state) ,然后将其作为输入传递给多个自定义useSpring(gesture)调用会给你提供交错的值,等等。 这些部分中的每一个都完全不知道其他部分,可能由不同的人编写,但它们组合得很好,因为状态和效果被封装并“附加”到封闭的组件。 这是一个类似这样的小演示,以及一篇我简要回顾一下 Hooks 是什么的

我想强调这不是关于减少样板,而是关于动态组合有状态封装逻辑的管道的能力。 请注意,它是完全反应式的——即它不会运行一次,但它会随着时间的推移对属性的所有变化做出反应。 考虑它们的一种方式是它们就像音频信号管道中的插件。 虽然我在实践中完全对“具有记忆的功能”持谨慎态度,但我们还没有发现这是一个问题,因为它们是完全孤立的。 事实上,这种隔离是他们的主要特征。 不然就会分崩离析。 因此,任何相互依赖都必须通过返回值并将其传递给链中的下一个事物来明确表达。 从第三方库的角度来看,任何自定义 Hook 都可以在不破坏(甚至影响)其消费者的情况下添加或删除状态或效果这一事实是另一个重要功能。

我不知道这是否有帮助,但希望它对编程模型有所帮助。
很高兴回答其他问题。

我绝对不建议使用构建器方法,正如 OP 所提到的那样,它有各种各样的问题。 我建议只使用 initState/dispose。 我真的不明白为什么这是一个问题。

我很好奇人们对#51752 (comment) 中的代码的

late关键字使事情变得更好,但它仍然存在一些问题:

这样的Property可能对自包含或不依赖于随时间变化的参数的状态很有用。 但是在不同的情况下可能会难以使用。
更准确地说,它缺少“更新”部分。

例如,使用StreamBuilder监听的流会随着时间而改变。 但是这里没有简单的解决方案来实现这样的事情,因为对象只初始化一次。

类似地,钩子与 Widget 的Key等价 - 当键更改时,它可以导致一个状态被销毁并重新创建。

一个例子是useMemo ,它是一个缓存对象实例的钩子。
结合键,我们可以使用useMemo进行隐式数据获取。
例如,我们的小部件可能会收到一个消息 ID——然后我们用它来获取消息详细信息。 但是该消息 ID 可能会随着时间的推移而改变,因此我们可能需要重新获取详细信息。

使用useMemo ,这可能看起来像:

String messageId;

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

}

在这种情况下,即使再次调用 build 方法 10 次,只要messageId不发生变化,就不会再次执行取数据。
但是当messageId更改时,会创建一个新的Future


值得注意的是,我认为当前状态的flutter_hooks并不适合 Dart。 我的实现更像是一个 POC,而不是一个成熟的架构。
但我确实相信我们在 StatefulWidgets 的代码可重用性方面存在问题。

我不记得在哪里,但我记得在理想世界中的钩子应该是一个自定义函数生成器,在async*sync*旁边,这可能类似于 Dan 建议的use State而不是useState

@gaearon

我想强调这不是关于减少样板,而是关于动态组合有状态封装逻辑的管道的能力。

这不是这里讨论的问题。 我建议提交一个单独的错误来讨论无法执行您所描述的操作。 (这听起来是一个非常不同的问题,老实说,这是一个比这里描述的问题更引人注目的问题。)这个错误专门针对某些逻辑过于冗长。

不,他是对的,我的措辞可能令人困惑。
正如我之前提到的,这不是关于代码行数,而是代码行数本身。

这是关于分解状态。

这个错误非常清楚“重用状态逻辑太冗长/困难”的问题,以及当你有一个属性需要在 initState 中声明它时,状态中的代码太多了。处置,并在 debugFillProperties 中。 如果您关心的问题有所不同,那么我建议您提交一个描述该问题的新错误。

我真的非常强烈建议在您完全理解要解决的问题之前忘记挂钩(或任何解决方案)。 只有对问题有一个清晰的理解,你才能阐明一个有说服力的论点来支持新特性,因为我们必须根据它们解决的问题来评估特性。

我想你当时误解了我在那个问题上所说的。

问题绝不是样板,而是可重用性。

样板是可重用性问题的结果,而不是原因

这个问题描述的是:

我们可能想要重用/组合状态逻辑。 但是可用的选项要么是mixin、Builders,要么是不重用它——所有这些都有自己的问题。

现有选项的问题可能与样板有关,但我们试图解决的问题不是。
虽然减少 Builders 的样板是一种途径(这就是钩子所做的),但可能有不同的途径。

例如,我想建议一段时间是添加如下方法:

context.onDidChangeDependencies(() {

});
context.onDispose(() {

});

但是这些都有自己的问题,并没有完全解决问题,所以我没有。

@rrousselGit ,随时编辑顶部的原始问题陈述以更好地反映问题。 也可以随意创建一个设计文档: https : //flutter.dev/docs/resources/design-docs ,我们可以一起迭代(同样,正如

我又看了好几遍这个问题。 老实说,我不明白误解来自哪里,所以我不确定要改进什么。
最初的评论反复提到了对可重用性/分解的渴望。 关于样板的提及不是“Flutter 很冗长”而是“某些逻辑不可重用”

我认为设计文档的建议不公平。 编写这样的文档需要花费大量时间,而我是在空闲时间这样做的。
我个人对钩子很满意。 我不是为了我的兴趣创作这些问题,而是为了提高对影响大量人的问题的认识。

几周前,我受雇讨论有关现有 Flutter 应用程序的架构。 他们可能正是这里提到的:

  • 它们有一些需要在多个小部件中重用的逻辑(处理加载状态/在某些小部件可见时将“消息”标记为已读/...)
  • 他们尝试使用mixin,这导致了主要的架构缺陷。
  • 他们还尝试通过在多个位置重写该逻辑来手动处理“创建/更新/处置”,但这导致了错误。
    在某些地方,他们忘记关闭订阅。 在其他情况下,他们没有处理stream实例更改的情况
  • 当某些小部件可见时将“消息”标记为已读

这是一个有趣的案例,因为它类似于我在我自己的一个应用程序中遇到的问题,所以我查看了我是如何在那里实现代码的,我真的没有看到这个 bug 描述的很多问题,这可能这就是为什么我无法理解问题的原因。 这是有问题的代码:

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

您是否有我可以研究的实际应用程序示例以查看实际问题?

(顺便说一句,总的来说,我强烈建议根本不要使用 Streams。我认为它们通常会使事情变得更糟。)

(顺便说一句,总的来说,我强烈建议根本不要使用 Streams。我认为它们通常会使事情变得更糟。)

(我完全同意。但目前社区有相反的反应。也许将 Flutter 中的 ChangeNotifier/Listenable/ValueNotifier 提取到官方包中会有所帮助)

您是否有我可以研究的实际应用程序示例以查看实际问题?

可悲的是没有。 我只能分享我在帮助他人时的经验。 我手头没有应用程序。

这是一个有趣的案例,因为它类似于我在我自己的一个应用程序中遇到的问题,所以我查看了我是如何在那里实现代码的,我真的没有看到这个 bug 描述的很多问题,这可能这就是为什么我无法理解问题的原因。 这是有问题的代码:

在您的实现中,逻辑不依赖于任何生命周期并放置在 _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)]);
  }
  ...
)

我建议将所有这些逻辑放在小部件之外的应用程序状态中,并且只让应用程序状态反映当前的应用程序状态。 拉动刷新不需要小部件内的状态,它只需要告诉环境状态刷新正在挂起,然后等待它的未来完成。

确定如何呈现错误、加载和数据不是环境状态的责任

将此逻辑置于环境状态不会从 UI 中删除所有逻辑
UI 仍然需要确定是在全屏还是在小吃吧中显示错误
重新加载页面时仍然需要强制刷新错误

这不太可重用。
如果渲染逻辑在小部件中完全定义而不是环境状态,那么它将与 _any_ Future 一起使用甚至可以直接包含在 Flutter 中。

我真的不明白你在上一条评论中主张什么。 我的观点是,您不需要更改框架来执行与上面的刷新指示器代码一样简单的操作,正如我之前引用的代码所示。

如果我们有很多这种类型的交互,不仅用于刷新指示器,还用于动画等,最好将它们封装在最需要它们的地方,而不是将它们放在应用程序状态中,因为应用程序状态如果应用程序中的多个位置不需要它,则不需要知道应用程序中每个交互的细节。

我不认为我们就该功能的复杂性及其可重用性达成一致。
你有一个例子来展示这样的功能很简单吗?

我链接到我上面写的一个应用程序的来源。 这当然不是完美的代码,我计划在下一个版本中重写它的一部分,但是我没有遇到您在本期中描述的问题。

但你是 Flutter 的技术负责人之一。
即使遇到问题,您也有足够的技能立即想出解决方案。

然而另一方面,很多人不明白以下代码有什么问题:

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

在 StackOverflowQ/AI 的流行程度证明了这一事实。

问题不在于不可能以可重用和健壮的方式抽象状态逻辑(否则就没有意义提出这个问题)。
问题是这样做需要时间和经验。

通过提供官方解决方案,这降低了应用程序最终无法维护的可能性,从而提高了整体生产力和开发人员体验。
不是每个人都能提出您的房产建议。 如果这样的东西是在 Flutter 中构建的,它将被记录下来,获得可见性,并最终帮助那些一开始从未想过它的人。

问题在于它实际上取决于您的应用程序是什么,您的状态是什么样子,等等。 如果这里的问题只是“您如何管理应用程序状态”,那么答案与钩子不一样,有很多文档在讨论不同的方法并针对不同的情况推荐不同的技术……基本上,这组文档: https :

有临时状态和应用程序状态,但似乎还有另一个用例:状态只与单一类型的小部件有关,但您仍然希望在该类型的小部件之间共享。

例如, ScrollController可能会调用某种类型的动画,但不一定适合将其放入全局应用程序状态,因为它不是需要在所有应用程序中使用的数据。 但是,多个ScrollController可能具有相同的逻辑,并且您希望在每个之间共享该生命周期逻辑。 状态仍然只有ScrollController s,所以不是全局应用状态,但是复制粘贴逻辑容易出错。

此外,您可能希望打包此逻辑,使其更适合您未来的项目以及其他项目。 如果您查看站点useHooks ,您会看到许多划分某些常见操作的逻辑部分。 如果您使用useAuthinitStatedispose调用,或者异步函数是否具有thencatch 。 该函数只编写一次,因此错误的余地基本上消失了。 因此,这种解决方案不仅对于同一个应用程序的多个部分以及多个应用程序之间更具有可组合性,而且对于最终程序员来说也更安全。

我不反对人们使用钩子。 据我所知,没有什么可以阻止这一点。 (如果有什么 _is_ 阻止了它,那么请提交一个关于它的错误。)

这个错误与钩子无关,而是关于“重用状态逻辑过于冗长/困难”,我仍在努力理解为什么这需要对 Flutter 进行任何更改。 有很多例子(包括钩子)展示了如何通过以一种或另一种方式构建应用程序来避免冗长,并且已经有很多关于它的文档。

我明白了,所以你问为什么,如果存在像钩子包这样的东西,它已经在没有对 Flutter 进行任何更改的情况下构建的,那么需要有一个钩子的第一方解决方案? 我想@rrousselGit可以更好地回答这个问题,但答案可能涉及更好的支持、更多的集成支持和更多的人使用它们。

我同意你的看法,除此之外,我也很困惑为什么需要对 Flutter 进行任何根本性的改变来支持钩子,因为表面上 flutter_hooks 包已经存在。

我仍在努力理解为什么这需要对 Flutter 进行任何更改。

说这个问题是因为社区做了一个包而解决的,就像说Dart不需要数据类+联合类型,因为我做了Freezed
作为解决这两个问题的解决方案,Freezed 可能很受社区欢迎,但我们仍然可以做得更好。

Flutter 团队比社区拥有更多的影响力。 你有能力修改整个堆栈; 在各个方面都是专家的人; 以及赞助所需工作的薪水。

这个问题需要那个。
请记住:React 团队的目标之一是让钩子成为语言的一部分,有点像 JSX。

即使没有语言支持,我们仍然需要在分析器中工作; 飞镖; 颤振/开发工具; 和许多钩子来简化 Flutter 所做的所有不同的事情(例如隐式动画、表单等)。

这是一个很好的论点,我同意,尽管 Flutter 的一般理念是拥有一个小核心。 出于这个原因,我们越来越多地添加新功能作为包,即使它来自谷歌,请参见字符动画。 这为我们提供了更大的灵活性来随着时间的推移学习和改变。 我们会对这个空间做同样的事情,除非有一个令人信服的技术原因导致包不足(并且使用扩展方法,这比以往任何时候都更不可能)。

将东西放入 Flutter 的核心是很棘手的。 一个挑战是,正如您从第一手经验中所了解的那样,状态是一个不断发展的领域,因为我们都了解了在反应式 UI 架构中什么是有效的。 两年前,如果我们被迫选择一个赢家,我们可能会选择 BLoC,但是当然,您的提供程序包接管了,现在是我们的默认推荐。

我可以轻松地设想谷歌雇佣的贡献者支持 flutter_hooks 或具有吸引力的类似 hooks 包(尽管我们有很多其他工作正在争夺我们的注意力,显然)。 特别是,如果您希望我们从您手中接管它,那显然是一个不同的问题。

有趣的论点,@timsneath。 Rust 社区也做了类似的事情,因为一旦引入到语言或框架的核心或标准库中,就很难将其取出。 在 Rust 的情况下,这是不可能的,因为他们想要永远保持向后兼容性。 因此,他们等到包裹到达并相互竞争,直到只有少数获胜者出现,然后他们将其折叠到语言中。

这可能与 Flutter 类似。 以后可能会有比钩子更好的东西,就像 React 必须从类移动到钩子但仍然必须维护类,人们必须迁移。 在被添加到核心之前,最好有竞争的状态管理解决方案。 也许我们社区应该在钩子上进行创新或尝试寻找更好的解决方案。

我理解这种担忧,但这与状态管理解决方案无关。

这样的特性更接近于 Inheritedwidget & StatefulWidget。 它是一个低级原语,它可以低至语言特性。

Hooks 可能独立于框架,但这只是靠运气。
正如我之前提到的,解决这个问题的另一个途径可能是:

context.onDispose(() {

});

和类似的事件监听器。
但这在框架之外是不可能实现的。

我不知道团队会想出什么。
但我们不能排除这种解决方案必须直接在 Element 旁边的可能性

扩展有帮助吗?

(也许我们应该在不同的问题中讨论这个问题。这里有点跑题了。我真的希望我们每个人看到的问题都有一个问题,这样我们就可以在正确的地方讨论解决方案。这不是清楚context.onDispose如何帮助处理冗长。)

我强烈怀疑我们可以提出一些与此相关的非常好的语言建议。

我认为更具体地谈论它们会比它们如何启用特定的状态管理习语更有帮助。 然后,我们可以更认真地考虑它们将实现什么以及它们可能带来的权衡。

特别是,我们将能够考虑它们如何以及是否可以在 VM 和 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();
}

问题是:

  • this 与类定义紧密耦合,因此不能重用
  • 随着小部件的增长,初始化和处置之间的关系变得更难阅读,因为中间有数百行代码。

使用context.onDispose ,我们可以这样做:

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

有趣的部分是:

  • this 不再与类定义紧密耦合,因此可以将其提取到函数中。
    我们理论上可以有半复杂的逻辑:
    ```飞镖
    AnimationController someReusableLogic(BuildContext 上下文){
    最终控制器 = AnimationController(...);
    控制器。onDispose(控制器。处置);
    控制器。转发();
    无效侦听器(){}
    controller.addListener(listener);
    context.onDispose(() => controller.removeListener(listener));
    }
    ...

@覆盖
无效的初始化状态(){
控制器 = someReusableLogic(上下文);
}
```

  • 所有的逻辑都捆绑在一起。 即使小部件增长到 300 长, controller的逻辑仍然很容易阅读。

这种方法的问题是:

  • context.myLifecycle(() {...})不可热加载
  • 目前还不清楚如何让someReusableLogicStatefulWidget读取属性而不将函数与小部件定义紧密耦合。
    例如, AnimationControllerDuration可以作为小部件的参数传递。 所以我们需要处理持续时间变化的场景。
  • 目前还不清楚如何实现一个函数,该函数返回一个可以随时间变化的对象,而不必求助于ValueNotifier并处理侦听器

    • 这对于计算状态尤其重要。


我会考虑语言建议。 我有一些想法,但现在没有什么值得谈论的。

正如我之前提到的,这个问题更多的是关于代码可重用性而不是冗长

好的。 您能否提交一个新的错误然后专门讨论这个问题? 这个错误的字面意思是“重用状态逻辑太冗长/困难”。 如果冗长不是问题,那么_this_ 不是问题。

使用context.onDispose ,我们可以这样做:

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

我不确定为什么context与此相关(并且onDispose违反了我们的命名约定)。 但是,如果您只是想要一种在处置期间注册要运行的东西的方法,那么今天您可以轻松地做到这一点:

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 中调用的事物? 我错过了什么吗?

  • 目前还不清楚如何让someReusableLogicStatefulWidget读取属性而不将函数紧密耦合到小部件定义。
    例如, AnimationControllerDuration可以作为小部件的参数传递。 所以我们需要处理持续时间变化的场景。

添加一个 didChangeWidget 队列非常简单,就像 dispose 队列一样:

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 有什么问题?

正如我之前提到的,这个问题更多的是关于代码可重用性而不是冗长

好的。 您能否提交一个新的错误然后专门讨论这个问题? 这个错误的字面意思是“重用状态逻辑太冗长/困难”。 如果冗长不是问题,那么这不是问题。

我开始对这个讨论感到很不舒服。 这一点我之前已经回答过:
这个问题的主题是可重用性,并且详细讨论了可重用性问题的结果; 不作为主要话题。

顶部评论中只有一个要点提到了冗长,那就是 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 有什么问题?

此语法旨在避免必须使用构建器,因此我们又回到了最初的问题。

此外,如果我们真的想让我们的函数可组合,而不是你的ValueGetter + queueDidUpdateWidget建议,函数将不得不采用ValueNotifier作为参数:

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

因为我们可能希望从didUpdateWidget以外的其他地方获取isAnimating ,具体取决于哪个小部件正在使用此函数。
一处,可能是didUpdateWidget; 另一种可能是 didChangeDependencies; 在另一个地方,它可能在stream.listen的回调中。

但是我们需要一种方法来轻松地将这些场景转换为ValueNotifier并让我们的函数监听这样的通知程序。
因此,我们正在使我们的生活变得更加艰难。
使用ConditionalAnimatorBuilder比我认为的这种模式更可靠和更容易。

至于为什么context而不是StateHelper ,是因为这样更灵活(就像使用 StatelessWidget 一样)

StatelessWidget 适用于无状态小部件。 重点是它们不会创建状态、处理事物、对 didUpdateWidget 做出反应等。

重新热加载的东西,是的。 这就是为什么我们使用方法而不是在 initState 中放置闭包。

很抱歉我一直这么说,我明白这一定令人沮丧,但我仍然不明白我们在这里试图解决的问题是什么。 根据原始错误摘要和原始描述的大部分内容,我认为这是冗长的,但我明白事实并非如此。 那么问题是什么? 听起来这里有许多相互排斥的愿望,散布在这个错误的许多评论中:

  • 声明如何处置某物应该在分配它的同一个地方完成......
  • ...并且分配它的地方只需要运行一次,因为它正在分配它......
  • ...它需要使用热重载(根据定义,它不会重新运行只运行一次的代码)......
  • ...并且它需要能够创建与无状态小部件(根据定义没有状态)一起使用的状态......
  • ...它需要启用挂钩到 didUpdateWidget 和 didChangeDependencies 之类的东西...

我们在这里参与的这种迭代舞蹈并不是完成工作的有效方式。 正如我之前尝试说的,在这里获得一些东西的最好方法是以我们可以理解的方式描述您面临的问题,在一个地方描述所有需求并用用例进行解释。 我建议不要列出解决方案,尤其是不要列出您知道不能满足您需求的解决方案。 只需确保描述中列出了使这些解决方案不合适的需求。

老实说,从根本上说,在我看来,您要求的是完全不同的框架设计。 这完全没问题,但它不是 Flutter。 如果我们要做一个不同的框架,那将是一个不同的框架,我们在_这个_框架上还有很多工作要做。 实际上,您描述的很多内容与 Jetpack Compose 的设计方式非常相似。 我不是那种设计的忠实粉丝,因为它需要编译器魔法,所以调试正在发生的事情真的很难,但也许它更适合你的胡同?

听起来这里有许多相互排斥的愿望,散布在这个错误的许多评论中:

它们并不相互排斥。 Hooks 可以完成其中的每一项。 我不会详细介绍,因为我们不想专注于解决方案,但他们确实选中了所有复选框。

正如我之前尝试说的,在这里获得一些东西的最好方法是以我们可以理解的方式描述您面临的问题,在一个地方描述所有需求并用用例进行解释。

我仍然无法理解该最高评论是如何做到这一点的。
我不清楚别人不清楚什么。

实际上,您描述的很多内容与 Jetpack Compose 的设计方式非常相似。 我不是那种设计的忠实粉丝,因为它需要编译器魔法,所以调试正在发生的事情真的很难,但也许它更适合你的胡同?

我不熟悉它,但通过快速搜索,我会说 _yes_。

它们并不相互排斥。

我上面列出的所有要点都是我们在这里尝试解决的问题的一部分吗?

但他们确实检查了所有的框

你能列出箱子吗?

我仍然无法理解该最高评论是如何做到这一点的。

例如,OP 明确表示问题出在 StatefulWidgets 上,但最近对此问题的评论之一表示,某个特定建议不好,因为它不适用于 StatelessWidgets。

在 OP 你说:

很难重用State逻辑。 我们要么最终得到一个复杂且深度嵌套的build方法,要么必须在多个小部件之间复制粘贴逻辑。

所以从这里我假设要求包括:

  • 解决方案不能深度嵌套。
  • 解决方案不能在尝试添加状态的地方需要大量类似的代码。

第一点(关于嵌套)似乎很好。 绝对不是试图建议我们应该做深度嵌套的事情。 (也就是说,我们可能不同意什么是深度嵌套;这里没有定义。其他评论后来暗示构建器会导致深度嵌套的代码,但根据我的经验,构建器非常好,如我之前引用的代码所示。)

第二点似乎直截了当地要求我们不要冗长。 但是你已经多次解释过这与冗长无关。

OP 描述问题的下一个语句是:

跨多个StatefulWidget重用State逻辑非常困难,只要该逻辑依赖于多个生命周期。

老实说,我真的不知道这意味着什么。 “难”对我来说通常意味着某些事情涉及很多难以理解的复杂逻辑,但是对生命周期事件进行分配、处置和反应非常简单。 给出问题的下一个语句(这里我跳过了明确描述为“不复杂”的示例,因此可能不是问题的描述)是:

当我们想要扩展这种方法时,问题就开始了。

这向我表明,“非常困难”的意思是“非常冗长”,而困难来自于有很多类似的代码出现,因为您给出的“不复杂”示例与“非常困难”之间的唯一区别" 扩展示例的结果实际上只是相同的代码发生了很多次(即冗长,样板代码)。

下一个描述问题的语句进一步支持了这一点:

在任何地方复制粘贴这个逻辑“有效”,但在我们的代码中造成了一个弱点:

  • 很容易忘记重写其中一个步骤(例如忘记调用dispose

所以想必这很困难,因为冗长的代码很容易在复制和粘贴代码时出错? 但同样,当我试图解决这个问题时,我将其描述为“冗长”,你说问题不在于冗长。

  • 它在代码中增加了很多噪音

再次,这听起来像是对我说冗长/样板,但你又一次解释说,事实并非如此。

OP 的其余部分只是描述您不喜欢的解决方案,因此大概没有描述问题。

这是否解释了 OP 如何无法解释问题? OP 中实际描述问题的所有内容似乎都在描述冗长,但是每次我建议那是问题时,您都说它不是,而且还有另一个问题。

我认为误解归结为这个词的意思。
例如:

它在代码中增加了很多噪音

再次,这听起来像是对我说冗长/样板,但你又一次解释说,事实并非如此。

这一点不是关于controller.dispose() ,而是这些代码行给读者带来的价值。
那条线应该始终存在并且始终相同。 因此,它对读者的价值几乎为零。

重要的不是这条线的存在,而是它的缺失。

问题是,我们拥有的此类controller.dispose()越多,我们就越有可能错过 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 方法调用
  • 所有这些都分布在 2 个不同的生命周期中。 3 如果我们包括构建

第二个片段有:

  • 1 类实例化
  • 1 匿名函数
  • 没有生命周期。 1 如果我们包括构建

这使得 ValueListenableBuilder _更简单_。

还有这些行没有说的内容:
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;
}

但是,一个小部件可能想要同时收听nameagegender
这意味着我们必须这样做:

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”。
    此外,它不一定是直观的。 制作一个定制的 Builder 并不是人们会首先考虑的事情——尤其是因为许多人会反对样板(虽然我不是)。

    人们更有可能烘焙自定义状态管理解决方案,并可能最终得到糟糕的代码。

  • 操作生成器树很乏味

    假设我们想删除前面示例中的ValueListenableBuilder或添加一个新的,这并不容易。
    我们可以花几分钟的时间来计算 () 和 {} 以了解为什么我们的代码无法编译。


Hooks 可以解决我们刚刚提到的 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 等,成为一个完整的解决方案。)(我们不会把它放到基础框架中,因为它会违反我们的性能要求。)

正如您所说,正是由于可能发生的错误,这就是不应有条件地调用钩子的原因。 有关更多详细信息,请参阅 ReactJS 中的 Hooks 规则文档。 基本要点是,由于在它们的实现中它们是按调用顺序跟踪的,有条件地使用它们将破坏该调用顺序,因此无法正确跟踪它们。 为了正确使用钩子,你在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 + 更新,这也避免了错误。

这是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 对此没有采取任何措施(尽管我觉得这很可悲),只要我们有:

  • 一个合适的分析器插件系统
  • 在 dartpad 中使用包的能力

如果你想使用我强烈鼓励的 hooks 插件,但遇到了一些问题,那么我建议为这些问题提交问题,并提交 PR 来解决这些问题。 我们非常乐意与您合作。

这是早期Property想法的新版本。 它处理 didUpdateWidget 和处置(并且可以很容易地处理其他诸如 didChangeDependencies 之类的事情); 它支持热重载(您可以更改注册属性和热重载的代码,它会做正确的事情); 它是类型安全的,不需要显式类型(依赖于推理); 除了属性声明和用法之外,它把所有东西都放在一个地方,性能应该相当不错(虽然不如更冗长的做事方式那么好)。

物业/物业经理:

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);
      },
    );
  }
}

为方便起见,您可以为 AnimationControllers 等创建准备好的 Property 子类,

您可能可以制作一个也可以在 State.build 方法中使用的版本......

我分享了@Hixie带来的一些疑问。 另一方面,我看到了 Hooks 的明显优势,而且似乎有相当多的开发人员喜欢它。
我对@timsneath提出的包方法的问题是,使用 Hooks 的代码与不使用 Hooks 的代码看起来截然不同。 如果他们不将它们纳入官方规范,我们最终将得到 Flutter 代码,对于仅遵循 Flutter 规范的人来说,这些代码是不可读的。
如果包开始实现应该是框架响应能力的东西,我们将获得许多不同的 Flutter 方言,这使得学习新的代码库变得困难。 所以对我来说,当它成为 Flutter 的一部分时,我可能会开始使用钩子。
这很像我目前对冷冻包装的看法。 我喜欢这个功能,但除非联合和数据类不是 Dart 的一部分,否则我不想将它们包含在我的代码库中,因为它会让人们更难阅读我的代码。

@escamoteur就我所知,您是在建议我们从根本上改变小部件的工作方式吗? 或者你是说应该有一些特定的新能力? 考虑到 Hooks 和上面的 Property 提案之类的事情如何在不更改核心框架的情况下成为可能,我不清楚您实际上希望更改什么。

它与关于任何提议更改本身的对话是正交的,但我认为我从@escamoteur@rrousselGit和其他地方和其他地方的人那里听到的是,在 _in_ 框架被视为建立特定合法性的重要方式方法。 如果您不同意,请纠正我。

我理解这种思路——因为框架中有很多东西(例如 DartPad 今天不支持第三方包,一些客户对他们在被 NPM 烧毁后依赖多少包持怀疑态度,感觉更“官方”,它保证会随着诸如空安全之类的变化而前进)。

但被包括在内也有很大的成本:特别是,它使方法和 API 变得僵化。 这就是为什么我们都对我们添加的内容持有非常高的标准,特别是在没有一致同意的情况下(参见状态管理),存在进化的可能性,或者我们可以像添加包一样轻松地添加内容。

我想知道我们是否需要记录我们的包优先理念,但同样,_where_它的去向与关于_what_我们可能想要更改以改进状态逻辑重用的讨论是分开的。

我们的包政策记录在这里: https :

我完全理解包优先的方法,并同意这是一件重要的事情。
但我也认为有些问题需要在内核中解决,而不是通过包来解决。

这就是为什么我不认为provider应该合并到 Flutter 中,而是认为这个问题描述了 Flutter 应该在本地解决的问题(当然不一定使用钩子)。

通过 Provider,Flutter 提供了一个内置原语来解决此类问题:InheritedWidgets。
Provider 仅在顶部添加了一个自以为是的层,以使其“更好”。

钩子不一样。 他们_是_原始人。 它们是针对特定问题的一种无偏见的低级解决方案:跨多个状态重用逻辑。
它们不是最终产品,而是人们期望用来构建自定义包的东西(就像我对hooks_riverpod所做的

如果有人可以详细介绍我在上面涂鸦的 Property 方法与 hooks 的比较,这对我很有帮助(在理解这里的愿望以及 hooks 满足的需求等方面)。 (我对 Property 想法的目标是在框架之上分层意见,以解决如何跨多个状态重用逻辑的问题。)

我认为 Property 提案未能解决这个问题的一个关键目标:状态逻辑不应该关心参数来自哪里以及它们在什么情况下更新。

该提议通过将所有逻辑重新组合在一个地方,从而在一定程度上提高了可读性; 但它未能解决可重用性问题

更具体地说,我们无法提取:

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

_ExampleState并在不同的小部件中重用它,因为逻辑绑定到ExampleinitState + didUpdateWidget

使用钩子会是什么样子?

在 Rust 社区看到类似的东西后,我同意@timsneath 。 一旦进入核心,就很难从核心中提取出一些东西。 BLoC 模式是在 provider 出现之前指定的,但现在 provider 是推荐的版本。 也许 flutter_hooks 可以以同样的方式成为“祝福”版本。 我这样说是因为将来可能会对人们提出的钩子进行改进。 React,现在有了钩子,无法真正改变它们或摆脱它们。 它们必须支持它们,就像它们支持类组件一样,基本上是永远的,因为它们是核心。 因此,我同意包装理念。

问题似乎是采用率很低,人们会使用适合他们的任何东西。 这可以通过推荐人们使用 flutter_hooks 来解决。 如果我们以类比方式查看有多少状态管理解决方案,即使很多人使用 provider,这也可能不是一个大问题。 我还在其他框架中遇到了一些问题和“陷阱”机智钩子,应该解决这些问题,以便为可组合和可重用的生命周期逻辑创建卓越的解决方案。

使用钩子会是什么样子?

不使用 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);
  }
}

在这种情况下,逻辑完全独立于ExampleStatefulWidget的生命周期。
所以我们可以在不同的小部件中重用它,以不同的方式管理它的userId 。 也许其他小部件将是一个StatefulWidget ,它userId内部管理它的userId

这个语法应该清楚地表明钩子就像独立的State对象,有自己的生命周期。

作为旁注,包优先方法的一个缺点是:包作者不太可能发布依赖钩子来解决问题的包。

例如,Provider 用户面临的一个常见问题是,他们希望在不再使用时自动处理 Provider 的状态。
问题是,Provider 用户也非常喜欢context.watch / context.select语法,而不是冗长的Consumer(builder: ...) / Selector(builder:...)语法。
但是我们不能在没有钩子的情况下(或 https://github.com/flutter/flutter/pull/33213,被拒绝)同时使用这种漂亮的语法 _and_ 来解决前面提到的问题。

问题是:
Provider 不能依赖flutter_hooks来解决这个问题。
由于 Provider 的流行程度,依赖 hooks 是不合理的。

所以最后我选择了:

  • 分叉提供者(代号为Riverpod
  • 结果自愿失去“Flutter 最喜欢的”/Google 推荐
  • 解决这个问题(以及更多)
  • 添加对钩子的依赖,以提供喜欢context.watch人喜欢的语法。

我对自己的想法很满意,因为我认为它比 Provider 带来了显着的改进(它使 InheritedWidgets 编译安全)。
但是到达那里的方式给我留下了不好的回味。

据我所知,钩子版本和属性版本之间基本上有三个区别:

  • Hooks 版本有更多的支持代码
  • 属性版本是更多样板代码
  • Hooks 版本在构建方法中存在问题,如果您以错误的顺序调用钩子,事情就会变糟,并且实际上没有任何方法可以立即从代码中看到这一点。

样板代码真的有那么大吗? 我的意思是,您现在可以轻松地重用该属性,代码都在一处。 所以现在真的是_only_一个冗长的论点。

我认为一个好的解决方案不应该依赖于了解它的其他软件包。 它是否在框架中应该无关紧要。 不使用它的人应该没有问题。 如果人们不使用它是一个问题,那么恕我直言,这是 API 的一个危险信号。

我的意思是,您现在可以轻松地重用该属性,代码都在一处。

代码在一处并不意味着它是可重用的。
您介意制作一个辅助小部件,在不同的小部件中重用当前位于_ExampleState中的代码吗?
有一个转折:这个新的小部件应该在它的 State 内部管理它的 userID,这样我们就有:

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

如果人们不使用它是一个问题,那么恕我直言,这是 API 的一个危险信号。

人们不使用某些东西,因为它不是官方的并不意味着 API 不好。

不想添加额外的依赖项是完全合法的,因为这是额外的维护工作(由于版本控制、许可证、折旧和其他事情)。
据我所知,Flutter 要求尽可能少的依赖。

即使现在已经被广泛接受并且几乎是官方的 Provider 本身,我也看到有人说“我更喜欢使用内置的 InheritedWidgets 来避免添加依赖项”。

您介意制作一个辅助小部件,在不同的小部件中重用当前位于_ExampleState中的代码吗?

有问题的代码都是关于从小部件获取 userId 并将其传递给 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,恕我直言,如果没有人采用它也不会变坏; 即使没有其他人知道它,它也应该站得住脚。 例如,上面的 Property 示例不依赖于其他包,使用它本身就很有用。

有一个误解。

问题不在于人们一般不使用钩子。
这是关于 Provider 无法使用钩子来解决问题,因为钩子不是官方的,而 Provider 是。


用于管理在同一对象中本地更改的 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没有任何有趣的代码,它只是一个从 Hooks 到fetchUser函数的适配器。 为什么不直接调用fetchUser呢? 您正在重用的代码不是有趣的代码。

这是关于 Provider 无法使用钩子来解决问题,因为钩子不是官方的,而 Provider 是。

恕我直言,提供者根本不需要采用代码重用问题的一个很好的解决方案。 它们将是完全正交的概念。 这是 Flutter 风格指南在“避免完成”的标题下谈到的。

我不明白为什么这是可取的。 FetchUser 没有任何有趣的代码,它只是一个从 Hooks 到 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就我所知,您是在建议我们从根本上改变小部件的工作方式吗? 或者你是说应该有一些特定的新能力? 考虑到 Hooks 和上面的 Property 提案之类的事情如何在不更改核心框架的情况下成为可能,我不清楚您实际上希望更改什么。

@Hixie不,我的观点是,如果钩子变得更加流行,我们应该考虑将它们包含在框架中并将它们教授给所有人,以便我们对 Flutter 代码的外观和行为保持一致的理解。
我非常同意您的担忧,但另一方面,带有钩子的 Widget 看起来非常优雅。
不会禁止像以前那样做事。

不会禁止像以前那样做事。

我认为会,我不认为 Flutter 团队说“嘿,我们现在推荐 flutter hooks 但你仍然可以像以前一样做事情”不是一个好主意,人们会对此感到困惑。 此外,如果 Flutter 团队在未来推荐使用钩子,那么他们也将需要停止发布实际的 Flutter 代码作为示例。

人们总是遵循“官方方式”做事,我认为不应该有两种使用 Flutter 的官方方式。

没关系。 我们试图证明代码的可重用性。 fetchUser可以是任何东西——例如包括ChangeNotifier.addListener

当然。 这就是函数的好处:抽象出代码。 但是我们已经有了函数。 上面的 Property 代码和上面的 _setUserId 代码表明,您可以将调用这些函数的所有代码集中到一个地方,而无需框架的任何特定帮助。 为什么我们需要 Hooks 来包装对这些函数的调用?

恕我直言,提供者根本不需要采用代码重用问题的一个很好的解决方案。 它们将是完全正交的概念。 这是 Flutter 风格指南在“避免完成”的标题下谈到的。

这就是为什么我提到钩子是一个原始的

它们是一种便利,它们不是原始的。 如果他们是原始人,那么“问题是什么”的问题会更容易回答。 你会说“这是我想做但我做不到的事情”。

作为比喻:
package:animations取决于Animation 。 但这不是问题,因为这是核心原语。
如果package:animations使用由社区维护的Animation的分叉,那将是另一回事

Animation 类层次结构做了一些基本的事情:它引入了代码和控制它们和订阅它们的方法。 如果没有 Animation 类层次结构,您必须发明类似 Animation 类层次结构的东西来制作动画。 (理想情况下是更好的。这不是我们最好的工作。) Hooks 没有引入新的基本功能。 它只是提供了一种以不同方式编写相同代码的方法。 可能是该代码更简单,或者分解方式与其他方式不同,但它不是原始代码。 您不需要类似 Hooks 的框架来编写与使用 Hooks 的代码执行相同操作的代码。


从根本上说,我不认为这个问题中描述的问题是框架需要解决的问题。 对于如何解决这个问题,不同的人会有非常不同的需求。 有很多方法可以修复它,我们已经在这个 bug 中讨论了几个; 有的方法很简单,几分钟就可以写出来,所以几乎不是一个很难解决的问题,它为我们拥有和维护解决方案提供了价值。 每个提案都有优点和缺点; 在每种情况下,弱点都是阻碍某人使用它们的东西。 甚至不清楚每个人都同意这个问题根本需要解决。

Hooks _are_ 原语
这是 Dan 的一个帖子: https :

更具体地说,您可以将flutter_hooks视为动态状态混合。

如果他们是原始人,那么“问题是什么”的问题会更容易回答。 你会说“这是我想做但我做不到的事情”。

它在 OP 中:

很难重用状态逻辑。 我们要么最终得到一个复杂且深度嵌套的构建方法,要么必须在多个小部件之间复制粘贴逻辑。
不可能通过 mixin 或函数重用这种逻辑。

可能是该代码更简单,或者分解方式与其他方式不同,但它不是原始代码。 您不需要类似 Hooks 的框架来编写与使用 Hooks 的代码执行相同操作的代码。

您不需要类来编写程序。 但是类允许您构建代码并以有意义的方式分解它。
而类是原语。

混入也是一样,它们也是原语

钩子是一样的。

为什么我们需要 Hooks 来包装对这些函数的调用?

因为当我们需要在 _one_ 处而不是在 _two_ 处调用此逻辑时。

不可能通过 mixin 或函数重用这种逻辑。

请给我一个具体的例子,在这种情况下。 到目前为止,我们研究过的所有示例都是没有钩子的简单示例。

到目前为止,在这个线程中,除了@rrousselGit钩子之外,我还没有看到任何其他解决方案可以解决并使重用和组合状态逻辑变得容易。

当然,我最近没有做太多的 dart 和 flutter,所以我可能在上面的属性解决方案代码示例中遗漏了一些东西,但是,有什么解决方案吗? 今天有哪些选项不需要复制粘贴而不是重复使用?
@rrousselGit问题的答案是

您介意制作一个辅助小部件来重用当前位于不同小部件中 _ExampleState 内的代码吗?
有一个转折:那个新的小部件应该在它的 State 内部管理它的 userID

如果无法通过上述属性解决方案重用如此简单的状态逻辑,还有哪些其他选择?
答案是否只是在颤振中不应该容易重用? 这完全没问题,但恕我直言有点难过。

顺便说一句,SwiftUI 是否以一种新的/其他鼓舞人心的方式做到了这一点? 或者它们是否也缺乏相同的状态逻辑可重用性? 我自己根本没用过swiftui。 也许它只是太不同了?

基本上所有的建造者。 Builders 是目前重用状态的唯一方法。
Hooks 使 Builders 更具可读性和更容易创建


这是我或一些客户上个月为不同项目制作的自定义钩子集合:

  • useQuery – 相当于我之前给出的ImplicitFetcher钩子,但改为进行 GraphQL 查询。
  • useOnResume提供回调以在AppLifecycleState.resumed上执行自定义操作而无需执行
    去麻烦制作一个 WidgetsBindingObserver
  • useDebouncedListener侦听可听的(通常是 TextField 或 ScrollController),但在侦听器上有去抖动
  • useAppLinkService允许小部件对类似于AppLifecycleState.resumed的自定义事件执行一些逻辑,但具有业务规则
  • useShrinkUIForKeyboard用于平滑处理键盘外观。 它返回一个布尔值,指示 UI 是否应该适应底部填充(基于监听 focusNode)
  • useFilter ,它结合了useDebouncedListeneruseState (一个声明单个属性的原始钩子)来公开搜索栏的过滤器。
  • 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 级缩进——如果我们使用FilterBuilder来重用过滤器逻辑,
  • 过滤器逻辑不能重用。

    • 我们可能会错误地忘记取消timer

  • build方法的一半对读者没有用处。 建设者分散了我们对重要事情的注意力
  • 我花了 5 分钟的时间试图理解为什么代码由于缺少括号而无法编译

作为flutter_hooks自己的用户,我会发表我的意见。 在使用钩子之前,我对 Flutter 很满意。 我没有看到需要类似的东西。 在阅读并观看有关它的 YouTube 视频后,我仍然不相信,它看起来很酷,但我需要一些练习或示例来真正激励它。 但后来我注意到了一些事情。 我不惜一切代价避免使用有状态的小部件,只涉及很多样板,并在课堂上跳过试图找到东西。 因此,我将大部分临时状态与应用程序状态的其余部分一起移到了状态管理解决方案中,并且只使用了无状态小部件。 然而,这会导致业务逻辑很快依赖于 Flutter,因为依赖于获取NavigatorBuildContext来访问InheritedWidget s / Providers更高的那个树。 并不是说这是一种好的状态管理方法,我知道它不是。 但是我做了我能做的任何事情,不必担心 UI 中的状态管理。

使用 hooks 一段时间后,我发现自己的工作效率更高,使用 Flutter 更开心,将临时状态放在正确的位置(与 UI 一起)而不是应用程序状态。

对我来说,它就像一个临时状态/控制器的垃圾收集器。 我不必记得处理 UI 中的所有订阅,尽管我仍然非常清楚这就是flutter_hooks为我所做的事情。 它还使维护和重构我的代码变得更加容易。 在过去的一年里,我为我的研究生研究和乐趣编写了大约 10 个应用程序。

和其他人一样,我不知道将它包含在 Flutter SDK 本身的主要动机是什么。 但是,这里有两个关于该主题的想法。

  1. 有时,我会制作一个钩子,以便更轻松地使用包含需要初始化/处置的控制器的包。 (例如golden_layoutzefyr )。 我相信使用flutter_hooks的其他用户会从这样的包中受益。 但是,我似乎无法证明发布一个包含 1-3 个函数的包是合理的。 另一种方法是创建一个厨房水槽包,其中包含我使用的各种包的许多钩子,然后我可以只使用 git 依赖项,但是任何使用其他包 + flutter_hooks 的人都必须依赖我的 git为了受益(这不太容易被发现,并且可能包含对他们不关心的包的依赖),或者包含 3 个函数的包,或者我将一个 Garden-sink 包发布到 pub.dev。 所有的想法似乎都很荒谬,而且不太容易发现。 flutter_hooks的其他用户可以轻松地将这些函数复制并粘贴到他们的代码中或尝试自己找出逻辑,但这完全错过了共享代码/包的重点。 这些功能最好放在原始包中,而不是在某些“扩展包”中。 如果flutter_hooks是框架的一部分,或者甚至只是像characters这样使用或从框架导出的包,那么原始包的作者更有可能接受简单钩子的拉取请求函数,我们不会有 1-3 个函数包的混乱。
    如果 Flutter 不采用flutter_hooks ,我预计会出现一堆 1-3 个函数包,从而使 pub.dev 搜索结果变得混乱。 这些包非常小这一事实让我非常同意@rrousselGit 的观点,即这是一个原始版本。 如果flutter_hooks存储库上的 1228 颗星没有任何迹象表明它解决了@rrousselGit提到的问题,我不知道是什么。

  2. 我正在观看有关为 Flutter 存储库做出贡献的 YouTube 视频,因为我一直有兴趣了解我可以做些什么来提供帮助。 在我看的时候,创建视频的人很容易地添加到新属性中,但几乎忘记了更新disposedidUpdateWidgetdebugFillProperties 。 再次看到有状态小部件的所有复杂性,以及错过某些东西是多么容易让我再次不信任它们,并且让我对为主 Flutter 存储库做出贡献感到不那么兴奋。 并不是说它完全阻止了我,我仍然对贡献感兴趣,但感觉就像我会创建难以维护和审查的样板代码。 这不是关于编写代码的复杂性,而是关于阅读代码和验证您是否已正确处理和处理临时状态的复杂性。

对于冗长的回复,我很抱歉,但是,我一直在不时地查看这个问题,并且对 Flutter 团队的回复有些困惑。 您似乎还没有花时间同时尝试两种方式的应用程序,并亲自看到不同之处。 我理解不维护额外依赖项或将其过多集成到框架中的愿望。 然而, flutter_hook框架的核心部分只有 500 行左右的记录良好的代码。 再次,抱歉,如果这与谈话无关,我希望我没有冒犯任何人,因为我付出了 2 美分并大声说出来。 我没有早点说出来,因为我觉得@rrousselGit提出了很好的观点并且很清楚。

对于冗长的回复,我很抱歉,但是,我一直在不时地查看这个问题,并且对 Flutter 团队的回复有些困惑。 您似乎还没有花时间同时尝试两种方式的应用程序,并亲自看到不同之处。

公平地说,这是一个令人难以置信的长线程,该框架的创始人每天都积极贡献数次,提供多个解决方案,请求对它们的反馈,并与它们互动并努力了解所请求的内容。 老实说,我很难想到一个更清晰的维护者有帮助的例子。

我希望这对这个问题有更多的耐心 - 在阅读完这个线程后,我对钩子的理解不再深入,除了它们是将一次性物品的生命周期与状态联系起来的另一种方式。 我在风格上不喜欢这种方法,而且我觉得如果立场是“只需花时间在范式中编写一个全新的应用程序,那么你就会明白为什么它需要被硬塞进框架中!” ' - 正如 React 工程师在此线程中所指出的那样,对于 Flutter 来说确实不可取,并且与这种重新布线的成本相比,此线程中描述的好处很小,这意味着您需要一个全新的代码库才能看到好处。

老实说,我很难想到一个更清晰的维护者有帮助的例子。

同意。 我很感谢 Hixie 花时间参与这个讨论。

阅读完这篇文章后,我对钩子的理解不再深入了

公平地说,这个问题明确地试图避免专门讨论钩子。
这更多的是试图解释问题而不是解决方案

你觉得它做不到吗?

我可以感觉到双方( @rrousselGit和 @Hixie)在这里,并希望从 Flutter 框架的(我的)使用方面/角度留下一些反馈。

flutter_hooks方法确实减少了很多样板文件(仅从这里显示的示例来看,因为我们可以重用此类状态配置)并通过不必积极考虑初始化/处置资源来降低复杂性。 一般来说,它在改进和支持开发流程/速度方面做得很好......即使它不太适合 Flutter 本身的“核心”(主观上)。

就像我编写的至少 95% 的代码导致 build 方法仅是声明性的,在返回的小部件子树之外没有局部变量或调用,所有逻辑部分都在这些状态函数内部,以初始化、分配和处置资源并添加听众(在我的例子中是 MobX 反应)和这样的逻辑东西。 由于这也是 Flutter 本身的大部分方法,因此感觉非常原生。 这样做也让开发人员有机会始终对你所做的事情保持明确和开放 - 它确实迫使我总是将这些小部件转换为StatefulWidget并在 initState / dispose 中编写类似的代码,但它也总是会导致直接在正在使用的小部件中准确地写下您打算执行的操作。 就我个人而言,就像@Hixie已经提到自己一样,编写这种样板代码并不会以任何方式打扰我,并允许我作为开发人员决定如何正确处理它,而不是依赖于flutter_hooks为我做这件事并导致不理解为什么某些事情可能会像它那样。 以小部分提取小部件还可以确保这些样板文件在它所用于的用例中是正确的。 使用flutter_hooks我仍然需要考虑什么样的状态值得写成一个钩子并因此重用 - 不同的风格可能会导致各种“单一”使用钩子或根本没有钩子,因为我可能不要太频繁地重用配置,而是倾向于编写更多的自定义配置。

不要误会我的意思,这种钩子中的方法看起来非常好和有用,但对我来说,它感觉像是一个非常基本的概念,它改变了如何处理这个问题的核心概念。 如果开发人员对如何“本机”不满意,那么作为一个包本身感觉非常好,让他们有机会使用这种方法,但使其成为 Flutter 框架本身的一部分,至少是干净的/统一,导致要么重写 Flutter 的大部分以利用这个概念(大量工作),要么将它用于未来/选定的东西(使用这种混合方法可能会令人困惑)。

如果它被集成到 Flutter 框架本身并支持/积极使用,我显然会跳到这个。 由于我理解甚至喜欢当前的方法,并且看到了在本地实现它所需的(可能的)操作,我可以理解犹豫和/或为什么不应该这样做,而是将其作为一个包保留。

如果我错了,请纠正我,但该线程是关于以可读和可组合的方式在多个小部件中重用状态逻辑的问题。 不是专门的钩子。 我相信这个线程是打开的,因为希望以开放的方式讨论解决方案应该是什么。

虽然提到了钩子,因为它们是一个解决方案,我相信@rrousselGit一直在这里使用它们来尝试解释他们解决的问题/问题(因为它们是一个解决方案),以便可以提出另一个可能更适合颤振的解决方案与并提出。 到目前为止,据我所知,尽管解决了可重用性问题,但该线程中还没有提出任何其他解决方案?

话虽如此,我现在不知道线程的去向。
我认为这个问题确实存在。 还是我们在争论这个?
如果我们都同意今天很难在以 flutter 为核心的多个小部件中以可组合的方式重用状态逻辑,那么有哪些解决方案可以成为核心解决方案? 因为建造者真的是(引用)

可读性明显降低

财产解决方案似乎不太容易重用,或者是我得出的错误结论(?),因为没有关于如何使用它的答案:

制作一个辅助小部件,在不同的小部件中重用当前位于 _ExampleState 中的代码?
有一个转折:那个新的小部件应该在它的 State 内部管理它的 userID

我愿意按照@timsneath 的建议帮助设计文档。 我认为用几个案例研究示例来解释问题可能是一种更好的格式,并提及不同的解决方案并探索我们是否可以找到适合 Flutter 的解决方案以及它所处的位置。 我同意这个问题的讨论有点迷失了。

我目前对制作设计文档的想法持怀疑态度。
很明显, @Hixie目前反对直接在 Flutter 内部解决这个问题。

在我看来,我们似乎对问题的重要性以及 Google 在解决该问题中的作用存在分歧。
如果双方不同意这一点,我看不出我们如何就如何解决这个问题进行富有成效的讨论——无论解决方案是什么。

这个问题线程是一个非常有趣的阅读,我很高兴看到观点的交流仍然保持文明。 然而,我对目前的僵局感到有些惊讶。

谈到钩子,我的观点是,虽然 Flutter 不一定需要@rrousselGit提供的特定钩子解决方案,他也不是这么说的。 Flutter 确实需要一个能够提供与 hooks 类似的好处的解决方案,这正是 Remi 和其他支持者提到的所有原因。 @emanuel-lundman 总结了上面的论点,我同意他的观点。

由于缺乏提供相同功能的任何其他可行提案,并且鉴于钩子在 React 中具有良好的记录,并且存在可以基于 Flutter 的现有解决方案,我认为它不会这样做是个糟糕的选择。 我不认为钩子概念,作为 Flutter SDK(甚至更低版本)中包含的一个原语,会从 Flutter 中拿走任何东西。 在我看来,它只会丰富它,并使使用 Flutter 开发可维护且令人愉悦的应用程序变得更加容易。

虽然 hooks 可以作为一个包提供给那些想要获得它的好处的人的论点是一个有效的观点,但我觉得它对于像 hooks 这样的原始人来说并不是最佳的。 这就是原因。

很多时候,甚至在制作内部可重用的包时,我们都会争论包是否需要“纯”,从某种意义上说,它可能只依赖于 Dart+Flutter SDK,或者我们是否允许其中包含其他一些包,如果是这样,这那些。 甚至 Provider 也用于“纯”包,但通常允许用于更高级别的包。 对于应用程序,也总是存在同样的争论,哪些包可以,哪些不可以。 Provider 是绿色的,但是像 Hooks 这样的东西作为一个包仍然是一个问号。

如果像解决方案这样的钩子是 SDK 的一部分,那么使用它提供的功能将是一个明显的选择。 虽然我想使用 Hooks 并且现在已经允许它作为一个包加入,但我也担心它会创建 Flutter 代码风格并引入一些概念,不使用它的 Flutter 开发人员可能不熟悉。 如果我们在没有 SDK 支持的情况下沿着这条路走下去,感觉有点像岔路。 对于较小的个人项目,使用 Hooks 是一个简单的选择。 我建议与 Riverpod 一起尝试。

(我猜我们的包保守主义来自过去被包烧毁和对其他包管理器的依赖混乱,可能不是唯一的。)

我并不是说钩子是解决当前问题的唯一方法,即使它是迄今为止唯一可行的演示解决方案。 在提交解决方案之前,在更通用的级别上调查选项当然会很有趣并且是一种有效的方法。 为了实现这一点,需要认识到 Flutter SDK _目前在易于重用的状态逻辑_方面存在缺陷,尽管目前似乎没有详细的解释。

对我来说,不只是将 Hooks 放入核心框架有两个主要原因。 首先是 API 中存在危险的陷阱。 首先,如果你以某种方式最终以错误的顺序调用钩子,那么事情就会崩溃。 这对我来说似乎是一个致命的问题。 我知道通过纪律和遵循文档你可以避免它,但恕我直言,这个代码重用问题的一个很好的解决方案不会有这个缺陷。

第二是人们真的没有理由不能只使用 Hooks(或其他库)来解决这个问题。 现在,正如人们所讨论的那样,特别是 Hooks 不起作用,因为编写 hook 已经足够繁重,以至于人们希望不相关的库能够支持 hooks。 但我认为这个问题的一个好的解决方案不需要那个。 一个好的解决方案是独立的,不需要每个其他图书馆都知道。

我们最近在框架中添加了 RestorableProperties。 看看它们是否可以在这里以某种方式被利用会很有趣......

我同意@Hixie关于 API 存在需要分析器或

@Hixie,但您确实同意这个问题,那么在小部件之间以可组合的方式重用状态逻辑没有好方法吗? 这就是为什么您开始考虑以某种方式利用 ResuableProperties 的原因?

对我来说,不只是将 Hooks 放入核心框架有两个主要原因。 首先是 API 中存在危险的陷阱。 首先,如果你以某种方式最终以错误的顺序调用钩子,那么事情就会崩溃。 这对我来说似乎是一个致命的问题。 我知道通过纪律和遵循文档你可以避免它,但恕我直言,这个代码重用问题的一个很好的解决方案不会有这个缺陷。

从使用钩子和与其他使用钩子的人一起工作开始,恕我直言,这真的不是什么大问题。 与它们带来的所有巨大收益(开发速度、可重用性、可组合性和易于阅读的代码的巨大收益)相比,根本没有。
一个钩子是一个钩子,就像一个类是一个类,而不仅仅是一个函数,你不能有条件地使用它。 你学得那么快。 你的编辑器也可以帮助解决这个问题。

第二是人们真的没有理由不能只使用 Hooks(或其他库)来解决这个问题。 现在,正如人们所讨论的那样,特别是 Hooks 不起作用,因为编写 hook 已经足够繁重,以至于人们希望不相关的库能够支持 hooks。 但我认为这个问题的一个好的解决方案不需要那个。 一个好的解决方案是独立的,不需要每个其他图书馆都知道。

编写钩子并不麻烦。
恕我直言,它比现在可用的解决方案更容易(再次使用该短语😉)。
也许我误解了你在写什么。 但我想没有人说过吗?
我读它就像人们真的很欣赏钩子解决方案带来的所有好处,并希望他们可以在任何地方使用它。 收获所有的好处。 由于钩子是可重用的,如果第三方开发人员能够自信地编写和发布自己的钩子而不需要每个人编写自己的包装器,那就太好了。 从状态逻辑的可重用性中获益。
我认为@rrousselGit@gaearon已经解释了原始事物。 所以我不会进入那个。
也许我没有得到这个声明,因为我看不出它是对人们在这个线程中所写内容的一个很好的总结。 抱歉。

希望有前进的道路。 但我认为现在是至少同意这是一个问题的时候了,要么继续提出更好的替代解决方案,因为钩子似乎甚至不在桌面上。
或者只是决定跳过在颤振核心中修复问题。

谁决定前进的道路?
下一步是什么?

这对我来说似乎是一个致命的问题。 我知道通过纪律和遵循文档你可以避免它,但恕我直言,这个代码重用问题的一个很好的解决方案不会有这个缺陷。

在 React 中,我们使用 linter 来解决这个问题——静态分析。 根据我们的经验,即使在大型代码库中,这个缺陷也不重要。 还有其他问题我们可能会认为存在缺陷,但我只想指出,虽然人们直觉上认为依赖持久调用顺序是一个问题,但在实践中平衡最终会大不相同。

我写这篇评论的真正原因是 Flutter 使用的是编译语言。 “Linting”不是可选的。 因此,如果宿主语言和 UI 框架之间存在一致性,那么绝对有可能强制执行“条件”问题永远不会静态出现。 但这只有在 UI 框架可以激发语言变化(例如 Compose + Kotlin)时才有效。

@Hixie,但您确实同意这个问题,那么在小部件之间以可组合的方式重用状态逻辑没有好方法吗? 这就是为什么您开始考虑以某种方式利用 ResuableProperties 的原因?

这当然是人们提出的。 这不是我的本能体验。 在使用 Flutter 编写自己的应用程序时,我并不觉得这是个问题。 但这并不意味着这对某些人来说不是真正的问题。

由于钩子是可重用的,如果第三方开发人员能够自信地编写和发布自己的钩子而不需要每个人都编写自己的包装器,那就太好了

我的观点是这里的一个好的解决方案不需要任何人编写包装器。

下一步是什么?

接下来有很多步骤,例如:

  • 如果 Flutter 存在我们这里没有谈到的具体问题,请记录问题并描述问题。
  • 如果您对如何以比 Hooks 更好的方式解决此问题有一个好主意,请创建一个这样做的包。
  • 如果可以做一些事情来改进 Hooks,那就去做吧。
  • 如果 Flutter 存在阻止 Hooks 发挥其全部潜力的问题,请将其作为新问题提交。
    等等。

这个问题线程是一个非常有趣的阅读,我很高兴看到观点的交流仍然保持文明。

我不想看到一个不文明的线程是什么样子。 在这个线程中几乎没有同理心,以至于很难从旁观者阅读和跟进

我的观点是这里的一个好的解决方案不需要任何人编写包装器。

不过,您不必编写包装器。 但是您可能希望在您已经习惯的自己的代码中获得好处和可重用性。 您肯定仍然可以按原样使用这些库。 如果你确实写了一个钩子包装的东西(如果可能的话),这可能不是因为你认为它是负担,而是它比替代方案更好。

这实际上是一个很好的理由,并且提到了为什么这个线程中的问题的解决方案在核心上会很棒。 核心中的可重用可组合状态逻辑解决方案意味着人们不必编写包装器,因为这种可重用逻辑可以安全地在所有包中交付,而无需添加依赖项。

核心中的可重用可组合状态逻辑解决方案意味着人们不必编写包装器,因为这种可重用逻辑可以安全地在所有包中交付,而无需添加依赖项。

我想说明的一点是,恕我直言,一个好的解决方案不需要_任何人_来编写该逻辑。 不会有任何多余的逻辑可以重用。 例如,查看前面的“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 使 Widget Tree 更加干净,并防止您忘记处理或处理 dudChangeDependencues。
因此,当前的方式有其陷阱,您可能会陷入其中。所以我想调用序列的潜在问题并不像那些那样大。
我不确定我是否会开始编写自己的钩子,但是在框架内准备好一组经常需要的钩子会很好。
这只是处理它们的另一种方式。

@Hixie ,真的很抱歉无法理解您要描述的内容,我责怪这里已经深夜了,但可能只有我一个人😳 ..问题解决方案包装/封装的状态业务逻辑和生命周期事件逻辑可以轻松组合并在小部件之间共享? 您能否详细说明一个好的解决方案的作用以及您如何看待它在理想情况下的工作?

只是在这里插一句,看到有人提到了这次讨论的礼貌。 我个人不认为这里的任何人是不文明的。

也就是说,我认为值得注意的是,这是一个各方都非常关心的话题。

  • 多年来,@rrousselGit一直在回答有关 StackOverflow 和package:provider问题跟踪器上状态管理的初学者问题。 我只关注后者,我对雷米的勤奋和耐心表示敬意。
  • @Hixie和 Flutter 团队的其他人非常关心 Flutter 的 API,它的稳定性、表面、可维护性和可读性。 正是由于这一点,Flutter 的开发者体验才有了今天。
  • Flutter 开发人员非常关心状态管理,因为这是他们开发时间的很大一部分。

很明显,这次讨论中的所有各方都有充分的理由为他们的所作所为进行争论。 传递信息需要时间也是可以理解的。

所以,如果讨论继续下去,无论是在这里还是以其他形式,我都会很高兴。 如果我能以任何方式提供帮助,例如使用正式文档,请告诉我。

另一方面,如果人们认为这里的讨论失控了,那么让我们暂停一下,看看是否有更好的交流方式。

(另外,我想感谢@gaearon加入这个讨论

@emanuel-lundman

但是在您描述的好的解决方案中,问题解决方案包装/封装的状态值、状态业务逻辑和生命周期事件逻辑在哪里可以轻松组合并在小部件之间共享? 您能否详细说明一个好的解决方案的作用以及您如何看待它在理想情况下的工作?

不幸的是,我无法详细说明,因为我不知道。 :-)

@escamoteur

也许 fetchuser 示例不是理想的示例。
但是 useStream、useAnimstion 或 useStreamCintroller 使 Widget Tree 更加干净,并防止您忘记处理或处理 dudChangeDependencues。

这个问题的难点之一是“移动球门柱”,其中描述了一个问题,然后在对其进行分析时,该问题被视为不是真正的问题而被驳回,并描述了一个新问题,依此类推。 真正有用的是提出一些规范的例子,例如一个演示 Flutter 应用程序,它有一个真实的问题例子,为了说明而没有过度简化。 然后我们可以使用 Hooks 和其他提案重新实现它,并真正以具体的方式相互评估它们。 (我会自己做这件事,除非我真的不明白到底是什么问题,所以如果有人提倡 Hooks 会这样做,这可能是最好的。)

真正有用的是提出一些规范的例子,例如一个演示 Flutter 应用程序,它有一个真实的问题示例,一个为了说明而没有过度简化的示例

你怎么看我在这里举的例子? https://github.com/flutter/flutter/issues/51752#issuecomment -669626522

这是一个真实世界的代码片段。

我认为那将是一个很好的开始。 我们能不能让它作为一个独立的应用程序运行,一个不使用钩子的版本和一个使用钩子的版本?

抱歉,我的意思是作为代码片段,而不是作为应用程序。

我认为“演示 Flutter 应用程序”想法的问题之一是,该线程中的示例非常真实。
它们并不过分简化。
钩子的主要用例是分解微状态,如去抖动、事件处理程序、订阅或隐式副作用——将它们组合在一起以实现更有用的逻辑。

我有一些关于 Riverpod 的例子,比如https://marvel.riverpod.dev/#/源代码在这里: https :
但这与之前提到的不会有太大不同。

@Hixie

我真的很难理解为什么这是一个问题。 我写了很多 Flutter 应用程序,但它看起来真的不是什么大问题? 即使在最坏的情况下,声明一个属性,初始化它,处置它,并将其报告给调试数据也是四行(实际上通常更少,因为你通常可以在初始化它的同一行声明它,应用程序一般不需要担心向调试属性添加状态,并且这些对象中的许多没有需要处理的状态)。

我在同一条船上。
我承认,我也不太了解这里描述的问题。 我什至不明白人们所指的“状态逻辑”是什么,需要可重用。

我有许多有状态的表单小部件,有些有数十个表单字段,我必须自己管理文本控制器和焦点节点。 我在 statelesswidget 的生命周期方法中创建和处理它们。 虽然这很乏味,但我没有任何使用相同数量的控制器/focusNode 或用于相同用例的小部件。 它们之间唯一的共同点是有状态和形式的一般概念。 仅仅因为它是一种模式并不意味着代码是重复的。
我的意思是,在我的代码的很多部分我必须遍历数组,我不会在我的应用程序重复代码中调用“for(var thing in things)”。

我喜欢 StatefulWidget 的强大功能,它源于其生命周期 api 的简单性。 它允许我编写 StatefulWidgets 做一件事,并与应用程序的其余部分隔离。 我的小部件的“状态”始终是它们自己私有的,因此重用我的小部件不是问题,代码重用也不是问题。

我对这里提出的例子有几个问题,这与您的观点有些一致:

  • 使用完全相同的“状态逻辑”创建多个有状态小部件似乎是错误的,并且与让小部件自包含的想法背道而驰。 但同样,我对人们所说的常见“状态逻辑”是什么意思感到困惑。
  • 钩子似乎没有做任何我无法使用普通 dart 和基本编程概念(例如函数)做的事情
  • 这些问题似乎与某种编程风格有关或由某种风格引起,这种风格似乎有利于“可重用的全局状态”。
  • 抽象出几行代码“过早的代码优化”的味道,并增加了复杂性,以解决一个与框架几乎无关的问题,而与人们如何使用它有关。

明显不那么可读。

  • 我们有 10 级缩进——如果我们使用FilterBuilder来重用过滤器逻辑,
  • 过滤器逻辑不能重用。

    • 我们可能会错误地忘记取消timer
  • build方法的一半对读者没有用处。 建设者分散了我们对重要事情的注意力
  • 我花了 5 分钟的时间试图理解为什么代码由于缺少括号而无法编译

您的示例更多地展示了 Provider 是多么冗长,以及为什么滥用 InheritedWidgets 是一件坏事,而不是 Flutter 的 StatelessWidget 和 State 生命周期 api 的任何真正问题。

@rrousselGit对不起,如果我不清楚。 我在上面提出的建议是专门创建真实的 Flutter 应用程序(使用 StatefulWidget 等)并显示您所描述的问题,以便我们可以根据真实的完整应用程序提出建议。 我们在这里讨论的具体示例,例如“fetchUser”示例,总是以“好吧,你可以像这样处理这种情况,而且很简单,不需要 Hooks”这样的讨论结束。通过“好吧,这过于简单化了,在现实世界中你需要 Hooks”。 所以我的观点是,让我们创建一个真实世界的例子,它确实_确实_需要 Hooks,它并不过分简化,展示了重用代码的困难,这样我们就可以看到是否有可能在不使用任何新代码的情况下避免这些问题,或者如果我们确实需要新代码,在后一种情况下,它是否必须像 Hooks 一样,或者我们是否可以找到更好的解决方案。

我什至不明白人们所指的“状态逻辑”是什么,需要可重用。

很公平
与状态逻辑并行的是 UI 逻辑,以及小部件带来的内容。

我们可以从技术上移除 Widget 层。 在那种情况下,剩下的就是 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逻辑,使其具有声明性。
这是一个极简主义的改变。 但在大型代码库中,这是一个显着的简化。

Hooks 做同样的事情。
但是不是Text ,你会得到状态逻辑的自定义钩子。 其中包括侦听器、去抖动、发出 HTTP 请求、...


您的示例更多地展示了 Provider 是多么冗长,以及为什么滥用 InheritedWidgets 是一件坏事,而不是 Flutter 的 StatelessWidget 和 State 生命周期 api 的任何真正问题。

这与提供程序无关(此代码毕竟不使用提供程序)。
如果有的话, provider 会更好,因为它有context.watch而不是Consumer

标准的解决方案是用ValueListenableBuilder替换Consumer ValueListenableBuilder - 这会导致完全相同的问题。

我同意@Hixie ,我确实认为我们需要两个并排比较来判断 Flutter 与 hooks 的有效性。 这也将有助于说服其他人 hooks 是否更好,或者如果 vanilla 应用程序是使用第三个解决方案构建的,则另一个解决方案甚至更好。 这个普通的应用程序概念已经存在一段时间了,像TodoMVC这样的

@satvikpendem
我很乐意帮忙。
我认为flutter_hooks库中的示例应用程序可能展示了几种不同的钩子以及它们使它们变得更容易/解决的问题,并且将是一个很好的起点。

我还认为我们还可以使用本期中介绍的一些示例和方法。

更新:代码在这里, https://github.com/TimWhiting/local_widget_state_approaches
我不确定存储库的正确名称,所以不要认为这是我们试图解决的问题。 我已经在 stateful 和 hooks 中完成了基本的计数器应用程序。 今晚晚些时候我没有太多时间,但我会尝试添加更多用例,这些用例更能说明可能存在的问题。 任何想贡献的人,请请求访问。

我们在这里讨论的具体示例,例如“fetchUser”示例,总是以“好吧,你可以像这样处理这种情况,而且很简单,不需要 Hooks”这样的讨论结束。通过“好吧,这过于简单化了,在现实世界中你需要 Hooks”。

我不同意。 我不认为我见过这样的“你可以像这样处理这种情况”并同意生成的代码比钩子变体更好。

我一直以来的观点是,虽然我们可以做不同的事情,但生成的代码容易出错和/或可读性较差。
这也适用于fetchUser

Hooks 做同样的事情。
但是不是Text ,你会得到状态逻辑的自定义钩子。 其中包括侦听器、去抖动、发出 HTTP 请求、...

不,我仍然不明白这个公共状态逻辑应该是什么。 我的意思是我有许多小部件在它们的“initState/didUpdateDependency”方法中从数据库读取,但我找不到两个进行完全相同查询的小部件,因此它们的“逻辑”不一样。

使用发出 HTTP 请求的示例。 假设我的服务类中有一个“makeHTTPRequest(url, paramters)”,我的一些小部件需要使用它,为什么我要使用钩子而不是在需要时直接调用该方法? 在这种情况下,使用钩子与普通方法调用有何不同?

听众。 我没有听同样事情的小部件。 我的每个小部件都负责订阅他们需要的任何内容并确保他们取消订阅。 钩子可能是大多数事情的语法糖,但由于我的小部件不会侦听相同的对象组合,因此必须以某种方式对钩子进行“参数化”。 再次,钩子与普通的旧函数有何不同?


这与提供程序无关(此代码毕竟不使用提供程序)。
如果有的话, provider 会更好,因为它有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这样的微片段,并_objectively_ 使用可衡量的指标来确定哪个代码更好,并对各种小片段执行此操作。


这如何与提供者无关? 我糊涂了。 我不是 Provider 方面的专家,但这绝对像是使用 Provider 的代码。

此代码不是来自 Provider,而是来自一个不使用 InheritedWidgets 的不同项目。
提供者的Consumer没有provider参数。

正如我提到的,你可以替换Consumer -> ValueListenableBuilder/StreamBuilder/BlocBuilder/Observer/...

在他们的“initState/didUpdateDependency”方法中,但我找不到两个进行完全相同查询的小部件,因此他们的“逻辑”不一样。

我们要重用的状态逻辑不是“进行查询”,而是“在 x 更改时做某事”。 “做某事”可能会改变,但“当 x 改变时”是常见的

具体例子:
我们可能希望小部件在收到 ID 更改时发出 HTTP 请求。
我们还想使用package:async的 CancelableOperation 取消挂起的请求。

现在,我们有两个小部件想要做完全相同的事情,但是使用不同的 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);
}

VS

Widget build(context) {
  Future<Message> message = useUnaryCancelableOperation(messageId, fetchMessage);
}

在这种情况下,所有与发出请求和取消请求相关的逻辑都是共享的。 我们只剩下一个有意义的区别,即fetchUserfetchMessage
我们甚至可以用这个useUnaryCancelableOperation制作一个包,现在每个人都可以在他们的应用程序中重用它。

如果我们不同意这里给出的示例的价值,应用程序将不会为对话带来任何东西——因为我们不能用钩子做任何事情,而我们不能用 StatefulWidget 做。

如果情况确实如此,那么我想我们应该关闭此错误,因为我们已经讨论了此处给出的示例,但它们并没有令人信服。 不过,我真的很想更好地理解这种情况,而且从这个 bug 的早期评论中可以看出,好处是在应用程序级别,因此我建议我们研究应用程序示例。

唯一的区别是我们将fetchUser更改fetchMessage 。 否则逻辑是 100% 相同的。 但是我们不能重用它,这很容易出错。

什么容易出错,什么可以重用? 实现一个全新的抽象层和类层次结构,这样我们就不必在一个类中实现三个方法,这太过分了。

同样,仅仅因为某些东西是一种常见模式,并不意味着您需要为其创建新功能。 此外,如果您想在这种情况下减少重复代码,您可以扩展 StatefulWidget* 类并使用公共位覆盖 initstate/didUpdateWidget 方法。

使用钩子,我们可以将其分解为useUnaryCancelableOperation钩子。

这意味着使用相同的两个小部件将改为:

Widget build(context) {
  Future<User> user = useUnaryCancelableOperation(userId, fetchUser);
}

VS

Widget build(context) {
  Future<Message> message = useUnaryCancelableOperation(messageId, fetchMessage);
}

在这种情况下,所有与发出请求和取消请求相关的逻辑都是共享的。 我们只剩下一个有意义的区别,即fetchUserfetchMessage
我们甚至可以用这个useUnaryCancelableOperation制作一个包,现在每个人都可以在他们的应用程序中重用它。

我很抱歉,但我绝对不同意。 除了它仅节省少量代码冗余这一事实之外,将概念上属于“initState”和“update”生命周期方法的代码“分解”到构建方法中是一个很大的问题。

我希望我的构建方法只构建布局而不是其他任何东西。 设置和拆除依赖项绝对不属于 build 方法,我很高兴必须明确重写相同类型的代码,以使我未来的自己和其他人明确我的小部件正在做什么。 让我们不要把所有东西都放在构建方法中。

如果真是这样,那么我想我们应该关闭这个错误

@Hixie请不要。 人们关心这个问题。 我在 reddit 上和你谈过同样的事情,但在 SwiftUI 的背景下: https :

这不是关于钩子,而是关于以某种方式改进有状态的小部件。 人们只是讨厌编写样板。 对于编写 SwiftUI 代码、习惯于 RAII 和复制视图语义的开发人员来说,手动管理一次性用品似乎刚刚好。

因此,我鼓励 Flutter 团队至少将此视为一个问题并考虑替代解决方案/改进。

我希望我的构建方法只构建布局而不是其他任何东西。 建立和拆除依赖绝对不属于构建方法,
这是很重要的一点。 构建方法应该是纯的。 我还是希望我们能有优势而没有困难

我真的不明白这里有更多例子的推动。 它的脸上很明显。

钩子解决的问题简单明了,它使代码保持干燥。 这样做的好处是显而易见的,更少的代码 == 更少的错误,维护更容易,隐藏错误的地方更少,较低的行数总体增加了可读性,初级程序员更好地隔离等等。

如果你说的是一个真实世界的用例,它是一个应用程序,你在 12 个不同的视图中设置和拆除 12 个动画控制器,每次都打开门错过 dispose() 调用,或覆盖一些其他生命周期方法。 然后将其应用于数十个其他有状态实例,您将轻松查看数百或数千行毫无意义的代码。

Flutter 充满了我们需要不断重复自己,设置和拆除小对象的状态的情况,这为不需要存在但确实存在的错误创造了各种机会,因为目前没有优雅的方法来解决分享这个死记硬背的设置/拆卸/同步逻辑。

您可以从字面上的 _any_ 状态看到这个问题,它有一个设置和拆卸阶段,或者有一些总是需要绑定到的生命周期钩子。

我自己发现使用小部件是最好的方法,例如我很少使用 AnimatorController,因为设置/拆卸非常烦人,冗长且容易出错,而是尽可能使用 TweenAnimationBuilder。 但是这种方法有其局限性,因为您在给定视图中获得了更多的有状态对象,从而在真正不需要的地方强制嵌套和冗长。

@szotp我还没有...我更希望我们建立一个或多个演示问题的基线应用程序,以便我们可以评估解决方案。 我会自己做,但我不明白我们到底要解决什么问题,所以我做错了。

@escamoteur Baseline 应用程序将帮助我们设计没有困难的解决方案。

@esDotDev 到目前为止,我们已经在这个 bug 中讨论过这样的案例,但是每次我们遇到时,Hooks 以外的解决方案都会被驳回,因为它们没有解决解决方案解决的示例中未包含的一些问题。 因此,对问题的简单描述似乎不足以捕捉全部范围。 例如,“12 个动画控制器”的情况可能可以通过一系列动画控制器和 Dart 中的功能特性来解决。 TweenAnimationBuilder 可能是另一种解决方案。 这些都不涉及 Hooks。 但我敢肯定,如果我提出这个建议,有人会指出我遗漏的东西并说“它不起作用,因为......”并提出这个(新的,在示例的上下文中)问题。 因此,需要一个我们都同意的基线应用程序涵盖了问题的全部范围。

如果有人想推动这一点,我真的认为最好的方法就是我上面描述的(https://github.com/flutter/flutter/issues/51752#issuecomment-670249755 和 https://github.com /flutter/flutter/issues/51752#issuecomment-670232842)。 这将为我们提供一个我们都同意的起点,代表我们试图解决的问题的程度; 然后,我们可以设计解决方案,以解决所有需求的方式解决这些问题(例如, @rrousselGit需要重用, @Rudiksz需要干净的构建方法等),最重要的是,我们可以在基线应用程序的上下文。

我认为我们都可以很容易地就他的问题达成一致:
_没有一种优雅的方式来共享与 Streams、AnimatorControllers 等相关的设置/拆卸任务。这会导致不必要的冗长、漏洞和可读性降低。_

有人不同意吗? 我们不能从那里开始并继续寻找解决方案吗? 我们必须首先同意这是一个核心问题,但我们似乎仍然没有这样做。

不过,在我写的时候,我意识到与问题的名称完全匹配,这是开放式的,留有讨论的余地:
重用状态逻辑要么太冗长,要么太难

对我来说,这是一个非常明显的问题,我们应该迅速越过辩论阶段,开始集思广益,讨论什么可行,如果不可行,那么什么。 我们需要可重复使用的微观状态……我相信我们可以想出一些办法。 它真的会在一天结束时清理很多 Flutter 视图,并使它们更加健壮。

@Hixie请不要。 人们关心这个问题。 我在 reddit 上和你谈过同样的事情,但在 SwiftUI 的背景下: https :

通过简单地扩展 StatefulWidget 类,您的 SwiftUI 示例可以用几行代码在 dart 中复制。

我有 StatefulWidgets,它们不订阅任何通知程序和/或不进行任何外部调用,实际上它们中的大多数都是这样。 我有大约 100 个自定义小部件(尽管不是全部有状态),其中可能有 15 个具有任何类型的“通用状态逻辑”,如此处的示例所述。

从长远来看,为了避免不必要的开销,编写几行代码(又名样板)是一个很小的权衡。 同样,这个必须实现 initState/didUpdate 方法的问题似乎有点过分了。 当我创建一个使用这里描述的任何模式的小部件时,我可能会先花 5-10 分钟“实现”生命周期方法,然后几天实际编写和完善小部件本身,同时从不触及所述生命周期方法。 与我的应用程序代码相比,我花在编写所谓的样板设置/拆卸代码上的时间是微不足道的。

正如我所说,StatefulWidgets 对它们的用途做出如此少的假设,这使得它们如此强大和高效。

向 Flutter 添加一种新类型的小部件,为这个特定用例创建子类 StatefulWidget(或不子类)会很好,但我们不要将它烘焙到 StatefulWidget 本身中。 我有很多小部件不需要“钩子”系统或微观状态带来的开销。

@esDotDev我同意这是一些人面临的问题; 我什至在本期早些时候提出了一些解决方案(搜索我的各种版本的Property类,现在可能被埋没了,因为 GitHub 不喜欢显示所有评论)。 困难在于这些提议被驳回,因为它们没有解决特定问题(例如,在一种情况下,没有处理热重载,在另一种情况下,没有处理 didUpdateWidget)。 然后我提出了更多建议,但后来又被驳回,因为他们没有处理其他事情(我忘了是什么)。 这就是为什么我们同意某种基线来代表_整个_问题很重要,这样我们才能找到该问题的解决方案。

目标从未改变。 所提出的批评是所建议的解决方案缺乏灵活性。 它们都不会在实现它们的代码段之外继续工作。

这就是本期标题提到“困难”的原因:因为目前我们解决问题的方式没有灵活性。


另一种看待它的方法是:

这个问题基本上是在争论我们需要实现一个Widget层,用于State逻辑。
建议的解决方案是“但您可以使用 RenderObjects 做到这一点”。

_从长远来看,为了避免不必要的开销,编写几行代码(又名样板)是一个小小的折衷。_

对这句话表示不满:

  1. 这不是几行,如果您将括号、行间距@overides等纳入 acct,您可以查看 10-15+ 行以获得一个简单的动画控制器。 这在我看来是不平凡的......就像超越平凡的方式。 执行此操作的 3 行令我感到困扰(在 Unity 中为Thing.DOTween() )。 15 太可笑了。
  1. 这与打字无关,尽管这很痛苦。 这是关于拥有 50 行类的愚蠢,其中 30 行是死记硬背的样板。 它的混淆。 这是关于这样一个事实,如果你_不_写样板,没有警告或任何东西,你只是添加了一个错误。
  2. 我不认为有任何开销值得与 Hooks 之类的东西讨论。 我们谈论的是一组对象,每个对象上都有一些 fxns。 在 Dart 中,速度非常快。 这是一个红鲱鱼imo。

@esDotDev

对我来说,这是一个非常明显的问题,我们应该迅速越过辩论阶段,开始集思广益,讨论什么可行,如果不可行,那么什么。

扩展小部件。 就像 ValueNotifier 扩展 ChangeNotifier 以简化常见使用模式的方式一样,每个人都可以为自己的特定用例编写自己的 StatelessWidgets 风格。

是的,我同意这是一种有效的方法,但它确实有一些不足之处。 如果我有 1 个动画师,那么我可以只使用 TweenAnimationBuilder。 很酷,它仍然像 5 行代码,但无论如何。 它有效......还不错。 但是,如果我有 2 个或 3 个? 现在我陷入了嵌套地狱,如果出于某种原因我有一个 cpl 其他有状态对象,那么这一切都会变成一团糟的缩进,或者我正在创建一些非常具体的小部件,它封装了一个随机的设置、更新和拆卸集合逻辑。

扩展小部件。 就像 ValueNotifier 扩展 ChangeNotifier 以简化常见使用模式的方式一样,每个人都可以为自己的特定用例编写自己的 StatelessWidgets 风格。

一次只能扩展一个基类。 那不成比例

Mixin 是下一个合乎逻辑的尝试。 但正如 OP 所提到的,它们也没有扩展。

@esDotDev

或者我正在创建一些非常具体的小部件,它封装了设置、更新和拆卸逻辑的随机集合。

一种必须设置 3-4 种 AnimationControllers 的小部件听起来像是一个非常具体的用例,并且支持设置/拆卸逻辑的随机集合绝对不应该成为框架的一部分。 事实上,这就是为什么 initState/didUpdateWidget 方法首先公开的原因,因此您可以根据自己的意愿随机收集设置。

我最长的 initState 方法是 5 行代码,我的小部件没有过度嵌套,所以我们肯定有不同的需求和用例。 或者不同的开发风格。

@esDotDev

3. 我认为用 Hooks 之类的东西没有任何值得讨论的开销。 我们谈论的是一组对象,每个对象上都有一些 fxns。 在 Dart 中,速度非常快。 这是一个红鲱鱼imo。

如果提议的解决方案类似于 flutter_hooks 包,那完全是错误的。 是的,从概念上讲,它是一个包含函数的数组,但实现远非微不足道或高效。

我的意思是,我可能是错的,但似乎 HookElement 会检查它是否应该在自己的构建方法中重建自己?!
还要检查钩子是否应该在每个小部件构建上初始化、重新初始化或处置似乎是一个重大的开销。 只是感觉不对,所以我希望我是错的。

@brianegan的架构示例之一作为基础应用程序进行比较是否有意义?

如果我可以在这里插话,不确定这是否已经说过。 但是在 React 中,我们并没有真正考虑使用钩子的生命周期,如果您习惯于构建组件/小部件,这可能听起来很可怕,但这就是生命周期并不真正重要的原因。

大多数情况下,当您使用基于 props 的状态或要执行的操作构建组件/小部件时,您希望在该 state/props 更改时发生某些事情(例如,就像我在此线程中看到的那样,您需要重新- 当 userId 属性更改时获取用户的详细信息)。 通常更自然地将其视为 userId 更改的“效果”,而不是 Widget 的所有 props 更改时发生的事情。

清理也是一样,通常认为“当这个 prop/state 改变时我需要清理这个 state/listener/controller”而不是“我需要记住在所有 props/state 改变时清理 X 或者当整个组件被破坏时”。

我有一段时间没有写 Flutter,所以我不想让自己听起来像我知道当前的环境或这种方法对当前 Flutter 心态的限制,我愿意接受不同的意见。 我只是认为很多不熟悉 React 钩子的人都和我被介绍时一样困惑,因为我的心态在生命周期范式中根深蒂固。

@escamoteur @Rudiksz @Hixie有一个由@TimWhiting创建的 GitHub 项目,我已被邀请到我们开始创建这些示例的地方。 每个人/组都可以创建他们如何解决预定义的问题。 它们不是完整的应用程序,更像是页面,但如果它们用于显示更复杂的示例,我们也可以添加应用程序。 然后我们可以讨论问题并创建更好的 API。 @TimWhiting可以邀请我认为感兴趣的任何人。

https://github.com/TimWhiting/local_widget_state_approaches

传送插座撰写也具有类似于钩,将其用反应相比这里

@satvikpendem @TimWhiting太棒了! 谢谢你。

@esDotDev
非常具体的用例和支持设置/拆卸逻辑的随机集合绝对不应该成为框架的一部分。

这是钩子打在头上的钉子。 每种类型的对象都负责它自己的设置和拆卸。 动画师知道如何创建、更新和销毁自己,流等也是如此。 Hooks 专门解决了分散在整个视图中的状态脚手架的“随机集合”问题。 这允许大部分视图代码专注于业务逻辑和布局格式,这是一个胜利。 它不会强迫您创建自定义小部件,只是为了隐藏一些在每个项目中都相同的通用样板。

我最长的 initState 方法是 5 行代码,我的小部件没有过度嵌套,所以我们肯定有不同的需求和用例。 或者不同的开发风格。

我也是。 但它是 initState() + dispose() + didUpdateDependencies(),缺少最后两个中的任何一个都会导致错误。

我认为规范示例类似于:编写一个使用 1 个流控制器和 2 个动画控制器的视图。

据我所知,您有 3 个选择:

  1. 将 30 行左右的样板文件添加到您的类中,以及一些 mixin。 这不仅冗长,而且最初很难遵循。
  2. 使用 2 个 TweenAnimationBuilders 和一个 StreamBuilder,大约 15 个缩进级别,甚至在你到达你的视图代码之前,你仍然有很多流的样板。
  3. 在 build() 顶部添加 6 行非缩进代码,以获得 3 个有状态的子对象,并定义任何自定义的 init/destroy 代码

也许有第四个选项,它是 SingleStreamBuilderDoubleAnimationWidget,但这对开发人员来说只是一个制作工作,一般来说很烦人。

同样值得注意的是,对于新开发人员来说,3 的认知负荷明显低于其他 2。 大多数新开发人员甚至不知道 TweenAnimationBuilder 的存在,并且简单地学习 SingleTickerProvider 的概念本身就是一项任务。 只是说,“请给我一个动画师”,是一种更简单、更可靠的方法。

我今天会尝试编写一些代码。

2. 使用 2 个 TweenAnimationBuilders 和一个 StreamBuilder,大约 15 个缩进级别,甚至在你到达你的视图代码之前,你仍然有很多流的样板。

对。 向我们展示一个使用 15 级缩进的真实代码示例。

用一个库中的 6 行 + 数百行代码替换 30 行代码如何减少认知负担? 是的,我可以忽略图书馆所做的“魔法”,但不能忽略它的规则。 例如,hooks 包明确地告诉你,hooks 必须只在构建方法中使用。 现在你有一个额外的限制需要担心。

在一个有 15,000 行代码的项目中,我可能只有不到 200 行代码,涉及 focusnodes、textcontrollers、singletickerproviders 或我的 statefulwidget 的各种生命周期方法。 你说的是什么认知超载?

@Rudiksz请停止被动攻击。
我们可以不争吵不同意。


钩子约束是我最不担心的。

我们不是专门讨论钩子,而是讨论问题。
如果必须,我们可以想出不同的解决方案。

重要的是我们要解决的问题。

此外,小部件只能在构建内部和无条件地使用(否则我们正在改变树的深度,这是不行的)

这与 hooks 约束相同,但我认为人们不会抱怨它。

此外,小部件只能在构建内部和无条件地使用(否则我们正在改变树的深度,这是不行的)

这与 hooks 约束相同,但我认为人们不会抱怨它。

不,它不完全相同。 此处提出的问题似乎与_prepares_widgets_before_ 被(重新)构建的代码有关。 准备状态、依赖项、流、控制器等,并处理树结构中的更改。 这些都不应该出现在 build 方法中,即使它隐藏在单个函数调用后面。
该逻辑的入口点永远不应该在 build 方法中。

强迫我将任何类型的初始化逻辑放入 build 方法与“强迫”我在 build 方法中组合一个小部件树完全不同。 build 方法的全部原因是采用现有状态(一组变量)并生成一个小部件树,然后将其绘制。

相反,我也反对强迫我在 initState/didUpdateWidget 方法中添加构建小部件的代码。

就像现在一样,statefulwidget 生命周期方法具有非常明确的作用,并且可以非常容易和直接地分离具有完全不同关注点的代码。

从概念上讲,我开始理解这里描述的问题,但我仍然没有将其视为实际问题。 也许一些真实的例子(不是计数器应用程序)可以帮助我改变主意。

作为旁注,我最近的实验

例如,它解决了:

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);
    }
  },
)

我们甚至可以通过自定义StatelessWidget / StatefulWidget基类来完全摆脱Consumer

class Example extends ConsumerStatelessWidget {
  <strong i="21">@override</strong>
  Widget build(ConsumerBuildContext context) {
    final value = context.watch(provider);
    final value2 = context.watch(provider2);
  }
}

主要问题是,这是特定于一种对象的,它的工作原理是对象实例在重建时具有一致的 hashCode。

所以我们离钩子的灵活性还很远

@rrousselGit我认为如果不扩展StatelessWidget / StatefulWidget类并创建诸如 ConsumerStatelessWidget 之类的东西,则可以通过在BuildContext上使用扩展方法来获得类似context.watch的东西类并让提供者通过 InheritedWidgets 提供监视功能。

那是一个不同的话题。 但是 tl;dr,我们不能依赖 InheritedWidgets 作为这个问题的解决方案: https :

为了解决这个问题,使用 InheritedWidgets 会阻止我们,因为https://github.com/flutter/flutter/issues/12992https://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 显然改善了这种情况。

我在 6 到 7 年前做过 ios 和 android 开发(大约在 Android 切换到他们的材料设计的时候),我不记得有任何管理和回收视图是一个问题,而且 Flutter 似乎没有更好或更糟。 我不能谈论时事,当 Swift 和 Kotlin 推出时我就退出了。

我被迫在 StatefulWidgets 中编写的样板约占我代码库的 1%。 容易出错吗? 每一行代码都是潜在的错误来源,所以肯定。 很麻烦吗? 200 行代码是 15000 行吗? 我真的不这么认为,但这只是我的看法。 Flutter 的文本/动画控制器、焦点节点都有可以改进的问题,但冗长不是一个问题。

我真的很想知道人们正在开发什么他们需要这么多样板。

听这里的一些评论听起来像管理 5 行代码而不是 1 行代码要困难 5 倍。 它不是。

您是否同意,而不是为每个 AnimationController 设置 initState 和 dispose 可能比仅执行一次并重用该逻辑更容易出错? 与使用功能相同的原则,可重用性。 我同意将钩子放在构建函数中是有问题的,但肯定有更好的方法。

真的感觉看到这里的问题和没看到问题的区别在于,前者之前使用过类似钩子的构造,例如在 React、Swift 和 Kotlin 中,而后者没有使用过,例如在纯 Java 中工作或安卓。 根据我的经验,我认为唯一可以说服的方法是尝试 hooks,看看是否可以回到标准方式。 很多时候,根据我的经验,很多人都做不到。 当你使用它时你就知道了。

为此,我鼓励那些持怀疑态度的人将 flutter_hooks 用于一个小项目,看看它的表现如何,然后以默认方式重做。 我们仅仅按照@Hixie的建议创建供人们阅读的应用程序版本是

我们仅仅按照@Hixie的建议创建供人们阅读的应用程序版本是

我浪费了几天时间尝试提供程序,甚至更多时间尝试 bloc,我都没有发现它们中的任何一个是一个好的解决方案。 如果它对你有用,那就太好了。

为了我,甚至尝试你的建议解决方案,有一个问题,需要证明自己的优势。 我查看了带有颤振钩子的示例,并查看了它的实现。 就是不行。

无论将样板减少代码添加到框架中,我希望有状态/无状态小部件保持不变。 我没有更多可以添加到这次谈话中了。

让我们重新开始,在一个假设的世界中,我们可以改变 Dart,而不用谈论钩子。

争论的问题是:

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');
    }
  );
}

但这与可重用性问题有什么关系呢?

这是相关的原因是,Builders 在技术上是一种重用状态逻辑的方法。 但他们的问题是,如果我们计划使用 _many_ 构建器,它们会使代码不太可读,例如在此评论中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);
  }
)

然后我们可以将其与 new 关键字一起使用:

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 与 React 中 Hooks 的灵感非常相似。 Builder 模式似乎与 React 中曾经流行的 Render Props 模式相同(但导致类似的深树)。 后来@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. 两者都有魔法。 我不一定知道这些建设者在内部做什么。 非魔法版本正在这些构建器中编写实际的样板。 对于 95% 的 Flutter 开发者来说,使用 SingleAnimationTIckerProvider mixin 也很神奇。
  2. 一个混淆了稍后将在树中使用的非常重要的变量名称,即value1value2 ,另一个将它们放在构建顶部的前面和中间。 这是一个明确的解析/维护胜利。
  3. 一个在小部件树开始之前有 6 级缩进,另一个有 0
  4. 一条是5条竖线,一条是15条
  5. 一个突出显示实际内容片段(Text()),另一个将其隐藏,嵌套,向下进入树。 另一个明显的解析胜利。

一般来说,我可以看到这可能是一个品味问题。 但是 Flutter 尤其在行数、缩进和信噪比方面存在问题。 虽然我绝对_喜欢_在 Dart 代码中声明性地形成树的能力,但它可能导致一些非常不可读/冗长的代码,尤其是当您依赖多层包装构建器时。 因此,在 Flutter 本身的背景下,我们一直在与这场战斗作斗争,这种优化是一个杀手级功能,因为它为我们提供了一个非常出色的工具来解决这个有点普遍的冗长问题。

TL;DR - 任何在 Flutter 中显着减少缩进和行数的东西都具有双重价值,因为 Flutter 固有的行数和缩进通常很高。

@Rudiksz我认为您没有在此处的讨论中添加任何内容。 我们知道缓解这些问题的策略,因为我们整天都在做。 如果您不觉得这是一个问题,那么您可以继续按原样使用这些东西,这根本不会影响您。

除非它是核心框架的变化,否则它确实会影响我,不是吗?

显然有很多人确实认为这是一个基本的痛点,只是来回循环是没有意义的。 你不会通过各种争论说服人们他们不想要他们想要的东西或改变任何人的想法。 本次讨论中的每个人显然都在 Flutter 中度过了数百或数千个小时,预计我们不会在某些事情上达成一致。

对了,这匹马已经被打死无数次了,我不会再回复评论了。

构建器在技术上是一种重用状态逻辑的方法。 但他们的问题是,他们使代码不太可读。

这完美地说明了这一点。 用 Flutter 的术语来考虑它,我们需要单行构建器。

不需要数十个选项卡和行的构建器,但仍允许将一些自定义样板连接到小部件生命周期中。

“一切都是小部件”的口头禅在这里尤其不是一件好事。 构建器中的相关代码通常只是它的设置道具,以及它返回的构建 fxn 需要的有状态的东西。 每一个换行符和制表符基本上都是毫无意义的。

除非它是核心框架的变化,否则它确实会影响我,不是吗?

@Rudiksz我认为没有人建议我们更改有状态小部件。 如果需要,您可以随时以当前形式使用它们。 无论我们想出什么解决方案,要么使用没有变化的有状态小部件,要么完全使用另一种类型的小部件。 我们并不是说有状态的小部件不好,只是我们想要另一种类型的小部件,它允许更多可组合的小部件状态。 将其视为一个有状态小部件,而不是与其关联的一个状态对象包含多个状态对象,以及一个独立但可以访问这些状态对象的构建函数。 通过这种方式,您可以重用已为您实现的initStatedispose的公共状态位(以及与该状态关联的状态逻辑)。 本质上更模块化的状态,可以在不同情况下以不同方式组合。 同样,这不是一个具体的建议,但可能是另一种思考方式。 也许它可以变成一个更像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 个示例足够详细,以至于您无法将其简化为“好吧,当然,对于此示例有效,但不适用于 _real_ 示例”。

不是 30 个示例会有所帮助,而是 1 个示例足够详细,以至于您无法将其简化为“好吧,当然,对于此示例有效,但对于真实示例却无效”。

我已经说过几次了,但是这种判断钩子的方式是不公平的。
Hooks 并不是要让以前不可能的事情成为可能。 它是关于提供一致的 API 来解决此类问题。

请求一个应用程序来展示一些不能以不同方式简化的东西,就是通过它爬树的能力来判断一条鱼。

我们不只是试图判断钩子,我们还试图评估不同的解决方案,看看是否有任何解决方案可以满足每个人的需求。

(我很好奇您在这里评估不同提案的方式是什么,如果不是通过在每个提案中实际编写应用程序并进行比较。您会提出什么评估指标?)

判断此问题的解决方案的正确方法不是应用程序(因为 API 的每个单独使用都将像这里的示例一样被忽略)。

我们应该判断所提出的解决方案是:

  • 生成的代码客观上是否比默认语法更好?

    • 它避免错误吗?

    • 是否更具可读性?

    • 写起来容易吗?

  • 生成的代码的可重用性如何?
  • 这个API能解决多少问题?

    • 我们是否会因某些特定问题而失去一些好处?

在这个网格上评估时, Property / addDispose提案可能有一个很好的“结果代码更好吗?” 得分,但在可重用性和灵活性方面的评估都很差。

如果没有实际看到每个提案的实际使用情况,我不知道如何回答这些问题。

为什么?

我不需要使用Property来制作应用程序,我知道这个提议将难以生成真正可重用的代码并解决许多问题。

我们可以采用任何现有的 *Builder 并尝试使用建议的解决方案重新实现它们。
我们也可以尝试重新实现这个线程中列出的钩子,或者 React 社区中制作的一些钩子(网上有许多钩子的编译)。

我不需要使用Property来制作应用程序,我知道这个提议将难以生成真正可重用的代码并解决许多问题。

不幸的是,我没有在这里分享你的直觉(正如我认为 Property (https://github.com/flutter/flutter/issues/51752#issuecomment-667737471) 工作得很好,直到你说它必须处理值来自具有相同 API 的小部件和本地状态,直到您提出它我才意识到这是一个约束)。 如果我提供的 Property 版本也可以解决该问题,那肯定没问题,还是会出现一些它没有涵盖的新问题? 没有目标,我们都同意就是目标,我不知道我们在为什么设计解决方案。

我们可以采用任何现有的 *Builder 并尝试使用建议的解决方案重新实现它们。

显然不是_任何_。 例如,你在 OP 中给出了一个,当我提出我的第一个 Property 提案(https://github.com/flutter/flutter/issues/51752#issuecomment-664787791)时,你指出了它没有说明的问题最初的建造者。

我们也可以尝试重新实现这个线程中列出的钩子,或者 React 社区中制作的一些钩子(网上有许多钩子的编译)。

我不介意我们从哪里开始。 如果你有一个特别好的例子,你认为它说明了问题并且它使用了钩子,那么很好,让我们将它添加到@TimWhiting的存储库中。 重点是以多种不同的方式实现同​​一件事,我不介意这些想法来自哪里。

不是 30 个示例会有所帮助,而是 1 个示例足够详细,以至于您无法将其简化为“好吧,当然,对于此示例有效,但不适用于 _real_ 示例”。

没有比使用 AnimatorController(或您能想到的任何其他可重用的有状态组件)而没有不可读的构建器或一堆容易出错的生命周期样板的简单愿望,再复杂不过了。

还没有提出以通用方式解决所要求的可读性和健壮性优势的解决方案。

我坚持认为 _any_ builder 会这样做,因为这个问题可以重命名为“我们需要 Builder 的语法糖”并导致相同的讨论。

所有其他参数(例如创建和处理AnimationController )都是基于它们也可以提取到构建器中:

Widget build(context) {
  return AnimationControllerBuilder(
    duration: Duration(seconds: 2),
    builder: (context, animationController) {

    }
  );
}

最后,我认为最好的例子是尝试完整地重新实现 StreamBuilder,并在不同的场景中对其进行测试:

  • 流来自Widget
  • // 来自一个继承的Widget
  • 来自当地州

并针对“流可以随时间变化”测试每个案例,因此:

  • 带有新流的 didUpdateWidget
  • InheritedWidget 更新
  • 我们调用了 setState

这目前无法用PropertyonDispose

@esDotDev您能列举出“要求的可读性和健壮性优势”吗? 如果有人提出一个提案来处理具有可读性和健壮性优势的 AnimationController,那么我们就到此为止了?

@rrousselGit我不是在提倡财产,正如您之前所说,它不能解决您的问题。 但是,如果有人要创建一个解决方案来完成 StreamBuilder 所做的一切,但没有缩进,那会是这样吗? 你会高兴吗?

但是,如果有人要创建一个解决方案来完成 StreamBuilder 所做的一切,但没有缩进,那会是这样吗? 你会高兴吗?

最有可能,是的

当然,我们需要将此解决方案与其他解决方案进行比较。 但这将达到可接受的水平。

@esDotDev您能列举出“要求的可读性和健壮性优势”吗?

健壮性在于它可以围绕依赖项和生命周期完全封装样板。 IE。 我不应该每次都告诉 fetchUser,它可能应该在 id 更改时重建,它知道在内部这样做。 不必告诉 Animation 在每次处理它的父 Widget 时处理自己,等等(我不完全理解 Property 是否可以做到这一点)。 这可以防止开发人员在整个代码库中为死记硬背的任务犯错误(目前 imo 使用 Builders 的主要好处之一)

可读性是我们可以用一行非缩进的代码获得有状态的事物,并且事物的变量被提升并且清晰可见。

@esDotDev如果有人提出了一个提案来处理具有可读性和健壮性优势的 AnimationController,那么我们就到此为止了?

如果你的意思是特别的 AnimationController 没有。 如果您指的是任何类似 AnimationController/FocusController/TextEditingController 的对象,那么是的。

有一个类似函数的 API 返回一个值,该值具有一个不明确的定义的生命周期是

我认为这是一个关键的误解。 钩子的生命周期是明确的,因为它们被定义为子状态。 它们_总是_在“使用”它们的国家的生命周期内存在。 实际上再清楚不过了。 语法可能很奇怪和陌生,但它肯定不缺乏清晰度。

类似于 TweenAnimationBuilder() 的生命周期也很清楚。 当它的父母离开时它就会消失。 就像子小部件一样,这些是子状态。 它们是完全独立的状态“组件”,我们可以轻松组装和重用,并且我们明确不管理它们的生命周期,因为我们希望它自然地绑定到它声明的状态,一个特性而不是一个错误。

@esDotDev

等等

你可以说得更详细点吗? (这就是为什么我建议提出一个涵盖所有基础的演示应用程序。我仍然认为这是做到这一点的最佳方式。)除了仅调用初始化程序之外,是否还有其他重要的功能,除非配置已更改并自动处理处理宿主元素时分配的资源?

类似 TextEditingController 的对象

你可以说得更详细点吗? 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 的约束构建者相同,没有人要求更多。 构建器可以绑定到widget.whatever,构建器可以完全管理widget 树上下文中所需的任何内部状态。 这就是 hook 所能做的,以及任何人要求的微状态或任何你想称它的东西。

你可以说得更详细点吗? TextEditingController 在某些方面比 AnimationController 更复杂吗?

不,但它可能在 init/dispose 中做不同的事情,或者它会绑定到不同的属性,我想封装那个特定的样板。

@esDotDev所以您想要与构建器相同的东西,但没有缩进,并且在一行上(大概是构建器回调本身)? 我刚刚发布的示例(https://github.com/flutter/flutter/issues/51752#issuecomment-671004483)今天对构建者也这样做了,所以大概还有其他限制吗?

(FWIW,我不认为构建器或类似构建器的东西,但在一条线上,是一个很好的解决方案,因为它们要求每种数据类型都为其创建自己的构建器;没有什么好方法可以即时构建一个.)

(FWIW,我不认为构建器或类似构建器的东西,但在一条线上,是一个很好的解决方案,因为它们要求每种数据类型都为其创建自己的构建器;没有什么好方法可以即时构建一个.)

我不明白这是什么意思。 你能改写一下吗? 🙏

您必须为动画创建一个 AnimationBuilder,为流创建一个 StreamBuilder,等等。 当你创建你的 StatefulWidget 时,不要只拥有一个 Builder 并说“这是你如何获得一个新的,这是你如何处理它,这是你如何获取数据”等等。

不过,我猜这违反了其他一些约束。 这就是为什么我更喜欢在我们尝试解决问题之前先对问题进行完整描述。

我认为这显然违反了对可读代码的任何要求,这也是这里的最终目标,否则我们都会使用一百万个特定类型的构建器,永远嵌套它们,然后收工。

我认为请求的内容类似于(请耐心等待,我不会,经常使用 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 vs RestorableString vs 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),即保证在调用 build 方法期间不会发生任何初始化代码。

@rrousselGit

我不认为这是一个问题。 我们已经有了 RestorableInt vs RestorableString vs RestorableDouble

我们有 AnimationBuilder 和 StreamBuilder 等等,是的。 在这两种情况下都是不幸的。

通用构建器

这类似于我为 Property 提出的建议,但如果我理解您的担忧,您认为这太冗长了。

早些时候,您说过如果有人要创建一个解决方案来完成 StreamBuilder 所做的一切,但没有缩进,您可能会很高兴。 你还没有评论我这样做的尝试(https://github.com/flutter/flutter/issues/51752#issuecomment-671004483)。 你对这个解决方案满意吗?

@esDotDev

我认为请求的内容是这样的:

这将违反其他人列出的一些非常合理的约束(例如@Rudiksz),即保证在调用 build 方法期间不会发生任何初始化代码。

这段代码是否在构建中并不重要。 重要的部分是

  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 与 Widget/State/life-cycles 无关,所以这个 pass_ ✅
  • 这个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),即保证在调用 build 方法期间不会发生任何初始化代码。

我们已经通过使用 *Builders 隐式地做到了这一点。 我们只需要一个语法糖来取消它们的缩进。 我认为这很像 async/await 和 futures。

@esDotDev您所描述的听起来与我之前提出的 Property 非常相似(参见例如 https://github.com/flutter/flutter/issues/51752#issuecomment-664787791 或 https://github.com/flutter/flutter/问题/51752#issuecomment-667737471)。 有没有什么东西可以阻止这种解决方案被创建为一个包? 也就是说,核心框架需要对其进行哪些更改才能允许您使用这种功能?

@rrousselGit和 Shawn 一样,我会问你同样的问题。 如果当前 StreamBuilder 功能与满足您需求的功能之间的唯一区别是不同的语法,那么您对核心语法的要求是什么才能使您能够使用这样的功能? 仅仅创建您喜欢的语法并使用它是不够的吗?

我对你的网格的问题是,如果我将它应用于目前的解决方案,我会得到这个,我认为这与你得到的非常不同:

有状态小部件

  • 生成的代码客观上是否比默认语法更好?

    • 它避免错误吗?

      _它和默认语法一样,不是特别容易出错。_ 🔷

    • 是否更具可读性?

      _是一样的,所以同样可读,可读性还可以。_🔷

    • 写起来容易吗?

      _是一样的,所以写起来同样容易,相当容易。_🔷

  • 生成的代码的可重用性如何?
    _重用的代码很少。_ ✅
  • 这个API能解决多少问题?
    _这是基线。_ 🔷
  • 我们是否会因某些特定问题而失去一些好处?
    _好像不是这样。_ ✅

属性变化

  • 生成的代码客观上是否比默认语法更好?

    • 它避免错误吗?

      _它把代码移到了不同​​的地方,但并没有特别减少错误的数量。_ 🔷

    • 是否更具可读性?

      _它把初始化代码和清理代码和其他生命周期代码放在同一个地方,所以不太清楚。_ ❌

    • 写起来容易吗?

      _它混合了初始化代码和清理代码和其他生命周期代码,所以更难写。_ ❌

  • 生成的代码的可重用性如何?
    _与 StatefulWidget 一样可重用,只是在不同的地方。_ ✅
  • 这个API能解决多少问题?
    _这是 StatefulWidget 的语法糖,所以没什么区别。_ 🔷
  • 我们是否会因某些特定问题而失去一些好处?
    _性能和内存使用会受到轻微影响。_ ❌

建造者的变化

  • 生成的代码客观上是否比默认语法更好?

    • 它避免错误吗?

      _它基本上是 StatefulWidget 解决方案,但被排除在外; 错误应该差不多。_🔷

    • 是否更具可读性?

      _Build 方法更复杂,其余的逻辑移动到不同的小部件,所以大致相同。_ 🔷

    • 写起来容易吗?

      _第一次写起来更难(创建构建器小部件),之后稍微容易一些,所以大致相同。_ 🔷

  • 生成的代码的可重用性如何?
    _与 StatefulWidget 一样可重用,只是在不同的地方。_ ✅
  • 这个API能解决多少问题?
    _这是 StatefulWidget 的语法糖,所以基本上没有区别。 在某些方面它实际上更好,例如,它减少了处理依赖项更改时必须运行的代码量。_ ✅
  • 我们是否会因某些特定问题而失去一些好处?
    _好像不是这样。_ ✅

类似钩子的解决方案

  • 生成的代码客观上是否比默认语法更好?

    • 它避免错误吗?

      _鼓励不良模式(例如,在构建方法中进行构造),如果不小心与条件一起使用,则可能会导致错误。_ ❌

    • 是否更具可读性?

      _增加了理解构建方法必须知道的概念数量。_ ❌

    • 写起来容易吗?

      _开发者要学会写钩子,这是一个新概念,太难了。_ ❌

  • 生成的代码的可重用性如何?
    _与 StatefulWidget 一样可重用,只是在不同的地方。_ ✅
  • 这个API能解决多少问题?
    _这是 StatefulWidget 的语法糖,所以没什么区别。_ 🔷
  • 我们是否会因某些特定问题而失去一些好处?
    _性能和内存使用受到影响。_ ❌

我不明白为什么我们总是回到冗长。
我多次明确表示这不是问题,问题是可重用性、可读性和灵活性。

抱歉,我记错了谁说 Property 太冗长了。 您是对的,您担心的只是之前没有列出的一个新用例没有处理,尽管我认为扩展 Property 来处理该用例也是微不足道的(我没有没试过,最好等到我们有一个清晰的演示应用程序,这样我们就可以一劳永逸地解决问题,而不是随着需求的调整而反复迭代)。

@szotp

  1. 此功能 IMO 可以扩展到所有构建器小部件,例如包括 LayoutBuilder。

LayoutBuilder 是一个与大多数构建器 FWIW 非常不同的小部件。 到目前为止讨论的所有提案都不适用于类似 LayoutBuilder 的问题,并且在您的评论之前描述的要求都不包括 LayoutBuilder。 如果我们还应该使用这个新功能来处理 LayoutBuilder,这很重要; 我建议与@TimWhiting合作,以确保我们将基于提案的示例应用程序包含布局构建器作为示例。

  1. 需要有一种方法来禁用监听,以便您可以创建 10x 控制器并将它们传递给叶子进行重建,或者 flutter 需要以某种方式知道树的哪一部分使用了构建器获得的值。

我不确定这到底是什么意思。 据我所知,您今天可以使用侦听器和构建器来完成此操作(例如,我在之前引用的应用程序中使用 ValueListenableBuilder 来完成此操作)。

这将违反其他人列出的一些非常合理的约束(例如@Rudiksz),即保证在调用 build 方法期间不会发生任何初始化代码。

我们已经通过使用 *Builders 隐式地做到了这一点。

我不认为那是准确的。 这取决于构建器,但有些人非常努力地将 initState、didChangeDependencies、didUpdateWidget 和构建逻辑分开,以便根据已更改的内容运行每个构建只需要最少量的代码。 例如, ValueListenableBuilder 仅在首次创建时注册侦听器,其构建器可以重新运行,而无需重新运行构建器的父级或 initState。 这不是 Hooks 可以做到的。

@esDotDev你所描述的听起来与我之前提出的 Property 非常相似(参见例如#51752 (comment)#51752 (comment) )。

如果我理解正确,我们可以让UserProperty自动处理 UserId 的 DidDependancyChange 或AnimationProperty ,或我们需要处理该类型的 init/update/dispose 的任何其他属性? 那么这对我来说似乎很好。 可以快速创建最常见的用例。

唯一让我失望的是这里的未来建设者。 但我认为这仅仅是因为你选择的例子?

例如,我可以创建这个吗?

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是多么微不足道,我对喜欢这种风格的人的建议是创建自己的(如果有帮助,可以从我的代码开始)并在他们认为合适的情况下直接在他们的应用程序中使用它,调整它以满足他们的需求。 如果很多人也喜欢它,它也可以制作成一个包,尽管对于一些微不足道的东西,我不清楚这与将它复制到自己的代码中并根据需要进行调整相比有多大好处。

唯一让我失望的是这里的未来建设者。 但我认为这仅仅是因为你选择的例子?

我试图解决@rrousselGit给出的一个例子。 原则上,它可以适用于任何事情。

例如,我可以创建这个吗?

您希望将 AnimationController 构造函数移动到一个将被调用的闭包中,而不是每次都调用它,因为initProperties在热重载期间被调用以获取新的闭包,但通常您不想创建热重载期间的新控制器(例如,它会重置动画)。 但是,是的,除此之外似乎还不错。 你甚至可以制作一个AnimationControllerProperty ,它接受AnimationController构造函数参数并用它们做正确的事情(例如,如果它发生变化,则更新热重载的持续时间)。

Imo 如果开发人员可以使用动画师,使用适当的 didUpdate 和 dispose 和 debugFillProperties 以及整个作品,而不必三思而后行,那就太好了(就像我们现在使用 TweenAnimationBuilder 时所做的那样,这是我们推荐的主要原因我们所有的开发人员都默认使用它而不是手动管理动画师)。

我担心开发人员没有考虑到这一点,如果您不考虑何时分配和处置事物,则更有可能最终分配很多您并不总是需要的东西,或者运行不需要的逻辑不需要运行,或做其他会导致代码效率降低的事情。 这就是我不愿意将其作为默认推荐样式的原因之一。

您甚至可以创建一个 AnimationControllerProperty,它接受 AnimationController 构造函数参数并使用它们做正确的事情(例如,如果更改了,则更新热重载的持续时间)。

谢谢@Hixie ,这真的很酷,我认为很好地解决了这个问题。

我并不是建议开发人员永远不要考虑这些事情,但我认为这些事情 99% 的用例几乎总是与它们所使用的 StatefulWidget 相关联,并且做除此之外的任何事情已经将您带入了中级开发人员的领域。

同样,我看不出这与在原始 AnimatorController 上推荐 TweenAnimationBuilder 有什么根本不同。 基本上的想法是,如果您希望状态完全包含/管理在另一个状态中(这通常是您想要的),那么这样做会更简单,更健壮。

在这一点上,我们应该组织一次电话会议,并与不同的利益相关方一起讨论。
因为这个讨论没有进展,因为我们一遍又一遍地回答同样的问题。

我不明白经过这么长时间的讨论,提出了这么多论点,我们仍然可以争辩说,与 StatefulWidget 相比,Builders 没有避免错误,或者钩子并不比原始 StatefulWidgets 更可重用。

考虑到所有主要的声明式框架(React、Vue、Swift UI、Jetpack Compose)都有一种或另一种方法来本地解决这个问题,这尤其令人沮丧。
似乎只有 Flutter 拒绝考虑这个问题。

@esDotDev恕我直言,使用 AnimationBuilder 或 TweenAnimationBuilder 或 ValueListenableBuilder 的主要原因是它们仅在值更改时重建

@rrousselGit

似乎只有 Flutter 拒绝考虑这个问题。

这个周末我花了几个小时自己的时间,更不用说在那之前谷歌的很多时间了,考虑这个问题,描述可能的解决方案,并试图准确地找出我们想要解决的问题。 请不要将不了解问题与拒绝考虑问题混淆。 特别是当我已经描述了可以做的最好的事情来推动这一进程(创建一个演示应用程序,其中包含“太冗长或太难”的所有状态逻辑,引用问题标题,重用),此错误的其他人已将其视为一项任务,而您拒绝参与。

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)使用构建器几乎没有意义,并且在所有情况下,手动滚动样板基本上不是 DRY。

这当然是针对特定情况的,因为性能问题通常如此。 我认为如果人们喜欢这种风格,使用 Hooks 或 Property 之类的东西是有意义的。 这在今天是可能的,并且似乎不需要框架中的任何额外内容(正如 Property 所示,它确实不需要太多代码)。

不,但这有点像要求社区构建 TweenAnimationBuilder 和 ValueListenableBuilder 而不是为它们提供 StatefulWidget 来构建。

并不是您要问,而是这种架构的主要好处之一是它自然适用于可以轻松共享的微小组件。 如果你把一个小的基础部分放在适当的位置......

与 Property 相比,StatefulWidget 是一大堆代码,而且非常重要(与 Property 不同,Property 主要是胶水代码)。 也就是说,如果 Property 是可以广泛重用的东西(而不是根据其确切需求为每个应用程序或团队制作定制版本),那么我会鼓励提倡使用它的人制作一个包并将其上传到pub 。 事实上,这同样适用于 Hooks。 如果它是社区喜欢的东西,那么它就会被大量使用,就像 Provider 一样。 不清楚为什么我们需要在框架本身中放置类似的东西。

我猜是因为这本质上是可扩展的。 Provider 不是,它只是一个简单的工具。 这是被扩展的东西,就像 StatefulWidget 一样,但用于 StatefulComponents。 它相对琐碎的事实不应该被反对吗?

关于“喜欢这种风格的人”的注释。 如果您可以保存 3 个覆盖和 15-30 行,那么在大多数情况下这将是可读性的胜利。 客观地说imo。 它还客观地消除了 2 整类错误(忘记处理事物,忘记更新 deps)。

非常感谢精彩的讨论,很高兴看到它的发展方向,我一定会把它留在这里:)

很抱歉,这个帖子让我对重新陷入颤动感到失望,这是我在完成另一个项目时的计划。 我也感到沮丧,因为

考虑到所有主要的声明式框架(React、Vue、Swift UI、Jetpack Compose)都有一种或另一种方法来本地解决这个问题,这尤其令人沮丧。

我同意@rrousselGit 的观点,因为我认为我们不应该花时间构建 flutter 示例应用程序,因为这个问题已经在这个线程中一遍又一遍地清楚地描述了。 我看不出它不会得到相同的响应。 因为它将与这里介绍的相同。 我对这个线程的看法是,从 Flutter 框架的角度来看,对于 Flutter 开发人员来说,最好在多个小部件中重复相同的生命周期代码,而不是只编写一次并完成它。
此外,如果我们正在寻找解决方案,我们无法在 flutter 中编写应用程序,因为我们需要解决方案来编写应用程序。 因为至少在这次谈话中颤动的人已经很清楚他们不喜欢钩子。 而 Flutter 只是没有其他解决方案来解决 OP 中描述的问题。 到底应该怎么写。

感觉(至少对我而言)这没有被认真对待,对不起@Hixie ,我的意思是它没有被认真对待: We understand the problem and want to solve it 。 像其他声明式框架显然似乎已经完成了。
在另一个但类似的注意事项中,如下所示:

写起来容易吗?
开发者要学会写钩子,这是一个新概念,更难

让我伤心。 为什么要改进或改变任何东西? 无论如何,你总是可以提出这个论点。 即使一旦学会了新的解决方案,就会变得更加容易和愉快。 你可以用很多东西替换那个语句中的钩子。 30 年前,我的母亲本可以使用这种关于微波炉的句子。 例如,如果您将句子中的“hooks”替换为“flutter”或“dart”,则效果相同。

写起来容易吗?
它是一样的,所以它同样容易编写,这相当容易

我不认为@rrousselGitis it easier to write? (布尔答案问题)的意思是,如果相同,则答案不是false / undefined

我不知道我们将如何能够到达某个地方,因为我们甚至不同意存在问题,只是很多人认为这是一个问题。 例如:

这当然是人们提出的。 这不是我的本能体验。 在使用 Flutter 编写自己的应用程序时,我并不觉得这是个问题。 但这并不意味着这对某些人来说不是真正的问题。

尽管许多人多次提出了很多论点,为什么需要在核心中解决 OP 的解决方案。
例如,它需要成为核心,以使第三方能够像今天使用和创建小部件一样轻松自然地使用它。 以及其他多种原因。 咒语似乎是,只需放入一个包中即可。 但是已经有包作为钩子之一。 如果这就是已经解决的问题,那么为什么不关闭线程。

我真的希望你接受@rrousselGit的提议并组织一次电话会议,也许就这个问题进行实时讨论会更容易,而不是一直来回写东西。 如果来自其他框架的任何人解决了 OP 中描述的问题,如果他们中的一个人真的很友善,也许他们也可以参与一段时间的电话会议,并分享他们关于出现的事情的 5 美分。 可以随时问。

无论如何,我现在取消订阅,因为我在关注这个线程时有点难过,因为我看不到它在任何地方发生。 但我确实希望这个线程能够达到同意存在问题的状态,以便它可以专注于 OP 的可能解决方案。 由于如果您不了解人们面临的问题,提出解决方案似乎有点徒劳,正如@Hixie可能同意的那样,我的意思是,因为有问题的人会告诉您为什么解决方案之后不起作用。

我真的希望你在结束这个线程时好运,或者只是断然地说 flutter 不应该解决这个问题的核心,尽管人们想要它。 或者通过寻找解决方案。 😘

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 逻辑,但替换了 Widgets 层,因此如果小部件太多了,有人可以创建一个替代框架来替代它。 当然还有打包系统,因此可以在不丢失任何现有框架代码的情况下添加功能。

无论如何,我在这里离题的重点只是这不是全有或全无。 即使从长远来看,我们最终不会采用让您满意的解决方案,但这并不意味着您不能继续从您喜欢的产品部分中受益。


我敦促对这个问题感兴趣的人与@TimWhiting合作创建一个应用程序,该应用程序展示您为什么想要重用代码以及当您不能重用代码时它的样子(https://github.com/TimWhiting/local_widget_state_approaches)。 这将直接帮助我们提出如何解决这个问题的建议,以解决_所有_在此处发表评论的人(包括喜欢 Hooks 的人和不喜欢 Hooks 的人)的需求。

真的不难理解为什么“_一种使构建器扁平化并且不关心构建器如何实现的语法糖。_”被开发人员希望作为一流的功能。 我们已经一遍又一遍地概述了替代方法的问题。

简而言之,构建器解决了可重用性问题,但难以阅读和编写。 “问题”只是我们想要更容易阅读的类似构建器的功能。

如果您从根本上不同意 3 个嵌套构建器难以阅读,或者构建器通常并没有真正用于代码重用目的,那么没有应用程序可以更清楚地表明这一点。 更重要的是听到我们中的许多人实际上真的很喜欢减少嵌套,并且真的不喜欢在我们的应用程序中重复代码,因此我们被困在 2 个非理想的选项之间。

这个周末我花了几个小时自己的时间,更不用说在那之前谷歌的很多时间了,考虑这个问题,描述可能的解决方案,并试图准确地找出我们想要解决的问题

我很感激

请不要将不了解问题与拒绝考虑问题混淆

我对缺乏理解没有问题,但目前的情况似乎没有希望。
我们仍在辩论讨论开始时提出的观点。

从我的角度来看,我觉得我花了几个小时写详细的评论来展示不同的问题并回答问题,但我的评论被驳回了,同样的问题又被问到了。

例如,当前语法缺乏可读性是讨论的中心。
我对可读性问题进行了几次分析以支持这一点:

这些分析有很多👍,其他人似乎也同意

然而根据你最近的回答,没有可读性问题: https :

你还建议:

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谢谢,这对理解您的观点

而且我非常喜欢 Flutter 的分层架构。 我还想继续使用小部件。 所以也许答案只是提高 Dart & Flutter 的可扩展性,对我来说就是:

使代码生成更加无缝 - 在 Dart 中实现 SwiftUI 魔法是可能的,但通常所需的设置太大且太慢。

如果使用代码生成就像导入包和打一些注释一样简单,那么遇到讨论问题的人就会这样做并且不再抱怨。 其余的将继续直接使用旧的 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 和流更频繁地变化,现在我们正在重建 ValueListenableBuilder 和 StreamBuilder,即使它们通常不会被触发; 现在也必须输入初始化器逻辑并跳过每个构建。)

你已经说过冗长不是问题,所以更简洁并不是为什么它更具可读性,我想(虽然我也对此感到困惑,因为你确实在问题标题和问题的原始描述)。 您提到想要减少缩进,但是您将使用构建器而不缩进的版本描述为不可读,因此大概不是原始版本中的缩进是问题所在。

您说构建器是可重用性的顶峰,并且您只想要一种替代语法,但是您所建议的建议与构建器不同(它们不创建小部件或元素),因此这并不是您要特别关注的构建器方面正在找。

你有一个你喜欢的解决方案(Hooks),据我所知它很好用,但你想在框架中改变一些东西,以便人们使用 Hooks? 我也不明白,因为如果人们不喜欢 Hooks 将其用作包,它可能也不是框架的一个好的解决方案(一般来说,我们更倾向于使用包,甚至是特性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
  • 我们有多少建设者?

使用构建器:

小部件构建(上下文){
 返回值ListenableBuilder(
 valueListenable: someValueListenable,
 构建器:(上下文,foo,_){
 返回流构建器(
 流:一些流,
 建设者:(上下文,巴兹){
 返回 TweenAnimationBuilder(
 吐温:吐温(...),
 构建器:(上下文,条){
 返回容器();
 },
 );
 },
 );
 },
 );
 }

使用线性语法:

小部件构建(上下文){
 最终 foo = 关键字 ValueListenableBuilder(valueListenable: someValueListenable);
 最终栏 = 关键字 StreamBuilder(stream: someStream);
 final baz = 关键字 TweenAnimationBuilder(tween: Tween(...));

返回图像(); }


就我而言,我很难通过嵌套代码找到答案。
另一方面,用线性树找到答案是瞬间的

您提到希望减少缩进,但您将使用构建器的版本描述为不可读,因此大概不是原始版本中的缩进是问题所在。

StreamBuilder 拆分成多个变量是一个严肃的建议吗?
根据我的理解,这是一个提出论点的讽刺建议。 不是吗? 你真的认为这种模式会导致更易读的代码,即使是在大型小部件上吗?

忽略示例不起作用的事实,我不介意分解它以解释为什么它不可读。 那会有价值吗?

```飞镖
小部件构建(上下文){
返回
ValueListenableBuilder(valueListenable: someValueListenable, builder: (context, value, _) =>
StreamBuilder(stream: someStream, builder: (context, value2) =>
TweenAnimationBuilder(tween: Tween(...), builder: (context, value3) =>
Text('$value $value2 $value3'),
)));
}

那看起来更好。
但这是假设人们不使用 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'),
              )));
}

这与原始代码几乎没有什么不同。

您说构建器是可重用性的顶峰,并且您只想要一种替代语法,但是您所建议的建议与构建器不同(它们不创建小部件或元素),因此这并不是您要特别关注的构建器方面正在找。

这是一个实现细节。
有或没有元素没有特别的理由。
事实上,拥有一个 Element 可能很有趣,这样我们就可以包含LayoutBuilder和潜在的GestureDetector

我认为这是低优先级。 但是在 React 社区中,在不同的钩子库中,我看到了:

  • useIsHovered – 返回一个布尔值,告诉小部件是否悬停
  • useSize –(可能应该是 Flutter 中的 useContraints)它给出了相关 UI 的大小。

(这也可能效率较低,具体取决于这些对象的独立程度;例如,如果动画总是比值 listenable 和流更频繁地变化,现在我们正在重建 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 团队在设计 hooks 时得出的结论。
但这可能不适用于 Flutter。

为什么这是不可读的?

由于嵌套 - 嵌套使得快速浏览并知道哪些部分可以忽略以及哪些对于理解正在发生的事情至关重要。 代码本质上有点“顺序”,但嵌套隐藏了这一点。 嵌套也很难使用它——想象一下你想重新排序两件事——或者在两件事之间注入一个新东西——在真正的顺序代码中是微不足道的,但是当你需要使用嵌套时就很难了。

这与 async/await 糖 vs 使用原始 Future API 非常相似,下面基于 dame continuation 的概念(甚至支持和反对的论点都非常相似) - 是的 Future API 可以直接使用并且不隐藏任何东西,但可读性和可维护性肯定不好 - async/await 是那里的赢家。

我的建议是在搜索特定事物时分析您的眼睛在看哪里,以及到达那里需要多少步骤。

我已经用超过 10 种不同的语言进行了 25 年的编程,这很容易成为评估代码可读性的最糟糕的方法。 源代码的可读性很棘手,但更多的是关于它表达编程概念和逻辑的程度,而不是“我的眼睛在看哪里”或它使用了多少行代码。

或者更确切地说,在我看来,你们过于关注可读性而不太关注可维护性

您的示例可读性较差,因为代码的 __intent__ 不太明显,并且将不同的关注点隐藏在同一位置使其更难维护。


final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);

甚至会有什么价值? 一个小部件? 字符串变量? 我的意思是它在一个内部使用
return Text('$value $value2 $value3');

基本上你想要的是通过在小部件 B 的构建方法中引用变量 A,它应该导致 B 在 A 的值发生变化时重建? 这就是 mobx 所做的,它完全使用了适量的魔法/样板。


final value2 = keyword StreamBuilder(stream: someStream);

这应该返回什么? 一个小部件? 一条流? 字符串值?

同样,它看起来像一个字符串值。 所以,你希望能够在构建方法简单地引用流,原因是小工具来重建每当流发出的值,获得发射值每当创建窗口小部件创建流/更新/物/更新/ 毁了? 在一行代码中? 在build方法里面?

是的,使用 mobx,你可以让你的构建方法看起来和你的“更具可读性”的例子完全一样(除非你引用了 observables)。 您仍然需要编写完成所有工作的实际代码,就像使用钩子一样。 实际代码大约有 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);

可以说,第一种写法清楚地表明 Promise 是在幕后创建的,以及链是如何形成的。 我想知道您是否同意这一点,或者您是否认为 Promises 与 Builders 有着根本的不同,因为它们应该有一个语法。

一个名为“TweenAnimationBuilder”的类返回一个字符串?! 我什至没有接近为什么这是一个可怕的想法。

你可以对 Promises/Futures 提出同样的论点,并说await掩盖了它返回一个 Promise 的事实。

我应该注意到,通过语法“解包”事物的想法并不新鲜。 是的,在主流语言中它是通过 async/await 实现的,但是,例如,F# 具有计算表达式,类似于某些核心 FP 语言中的do符号。 在那里,它具有更强大的功能,并且被推广到可以与满足某些规则的任何包装器一起使用。 我并不是建议将 Monads 添加到 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语句、三个构建器语句、三个 lambda 标头、三个上下文和最后三个值。 最终,我们关心的是这三个值——其余的都是样板,让我们到达那里。 我实际上发现这是这个代码块中最具挑战性的部分。 发生了很多事情,而我真正关心的部分(构建器返回的值)很小,以至于我将大部分精力都花在了样板上,而不是专注于我真正需要的部分(再次,价值观)。


在第二个代码块中,大量减少了样板文件,因此我可以专注于我关心的部分 - 再次是值。

3. 第一个代码块将build方法最重要的部分隐藏在嵌套的最深处

我认识到这个build方法的所有部分都很重要,但我发现当我阅读这种风格的声明式 UI 代码时,我通常寻找的是显示给用户,在本例中是嵌入在最深嵌套构建器中的Text小部件。 这个Text小部件不是位于前面和中间,而是隐藏在多层缩进、语法和意图中。 如果您在这些层之一中抛出ColumnRow ,它会变得更加嵌套,此时您甚至无法追踪到缩进最多的部分.


在第二个代码块中,返回的实际可渲染Widget位于函数的底部,这很明显。 此外,我发现当你有类似 OP 建议的语法时,你可以指望视觉Widget总是在函数的底部,这使得代码更可预测和易于阅读。

关于嵌套,表达 _tree_ 的嵌套和表达 _sequence_ 的嵌套是有区别的

在正常的View -> Text嵌套等情况下,嵌套很重要,因为它表示屏幕上的父子关系。 对于像 Context 这样的特性(不确定 Flutter 是否有),它代表了上下文的范围。 所以嵌套本身在这些情况下具有重要的语义意义,不能被忽视。 您不能只是交换父母和孩子的位置并期望结果相同。

但是,通过 Builder 的嵌套(在 React 中也称为“Render Props”)或 Promise 的嵌套,嵌套应该传达一系列转换/增强。 树本身并不重要——例如,当嵌套独立的ABuilder -> BBuilder -> CBuilder ,它们的父子关系不会传达额外的含义。

只要所有三个值都在下面的范围内可用,它们的树结构就不是真正相关的。 它在概念上是“扁平的”,嵌套只是语法的产物。 当然,它们可能会使用彼此的值(在这种情况下它们的顺序很重要),但顺序函数调用也是如此,并且无需任何嵌套即可完成。

这就是async/await引人注目的原因。 它删除了描述低级机制的附加信息(Promise 的父子关系),而是让您专注于高级意图(描述序列)。

树是比列表更灵活的结构。 但是当每个父母只有一个孩子时,它就会变成一棵病态树——本质上是一个列表。 Async/Await 和 Hooks 认识到我们在不传达信息的东西上浪费了语法,并将其删除。

这实际上很有趣,因为我之前说过“这与样板无关”,现在看来我在自相矛盾。 我认为这里有两件事。

就其本身而言,构建器(或至少是 React的渲染道具)

所以对我来说似乎没有解决的部分是减少嵌套的读者成本。 该参数与async / await参数完全相同。

由于以下原因,它的可读性较差:

  • 过度使用空格不能很好地扩展。 我们的显示器上只有这么多行,强制滚动会降低可读性并增加认知负荷。 想象一下,我们已经有一个 60 行的小部件树,而你只是为构建者强加了 15 行,这并不理想。
  • 它浪费了赫兹空间,我们仅限于导致额外的环绕,这进一步浪费了行空间。
  • 它将叶节点(即内容)从左侧推到更远的位置,并推入树中,在那里一目了然难以发现
  • 一目了然地识别“关键参与者”或“非样板文件”要困难得多。 在我的推理开始之前,我必须“找到重要的东西”。

另一种看待这个问题的方法是简单地突出显示非样板代码,以及它是组合在一起让你的眼睛轻松享受,还是分散在任何地方让你的眼睛不得不四处浏览:

通过突出显示,这是很容易推理的。 没有它,我需要阅读整个冗长的内容,然后才能弄清楚谁在使用什么以及在哪里:
image

现在与此相比,突出显示基本上是多余的,因为我的眼睛无处可去:
image

值得注意的是,可读性与可理解性可能存在分歧。 @Hixie可能将时间花在单一的类文件中,他必须不断地阅读和理解大量的树,而典型的应用程序开发人员更多的是构建数百个较小的类,当您管理许多小类时,grok-ability 是关键. 当你放慢速度阅读代码时,并不是说代码可读性强,而是我能一眼就知道这是做什么的,所以我可以跳进去调整或修复一些东西。

作为参考,React 中 Context 的等价物是 InheritedWidgets/Provider

它们之间唯一的区别是,在 React 中,在 hooks 之前我们_不得不_使用 Builder 模式来使用 Context/Inheritedwidget

而 Flutter 有一种方法可以通过简单的函数调用来绑定重建。
所以不需要钩子来使用 InheritedWidgets 来压扁树——这可以解决 Builders 的问题

这可能是讨论更加困难的原因之一,因为我们较少需要 Builders。

但值得一提的是,引入类似钩子的解决方案可以同时解决https://github.com/flutter/flutter/issues/30062
https://github.com/flutter/flutter/issues/12992

似乎@Hixie更习惯于阅读深层嵌套的树,因为 Flutter 基本上都是树,在我看来比其他语言要多得多。 作为 Flutter 本身的主要开发者之一,他当然会有更多的经验。 Flutter 本质上可以被认为是一个从左到右的框架,具有深度嵌套,很像 HTML,我认为@Hixie已经创建了 HTML5 规范。 也就是说,代码块的最右边是主要逻辑和返回值所在的位置。

然而,大多数开发人员不是,或者来自更多自上而下的语言,其中逻辑再次从上到下读取,而不是在嵌套树中; 它位于代码块的最底部。 因此,对于他而言,对于许多其他开发人员而言,可读性不一定如此,这可能就是您在这里看到关于可读性的意见分歧的原因。

另一种看待它的方式是,我的大脑需要多少代码来进行视觉编辑。 对我来说,这准确地代表了我的大脑在解析从树返回的内容之前必须做的“繁重工作”:
image

简而言之,构建器版本具有 4 倍高的垂直足迹,同时几乎不添加任何附加信息或上下文,并且以更加稀疏/分散的方式打包代码。 在我看来,这是一个开放和封闭的案例,仅出于这个原因,客观上可读性较差,甚至没有考虑围绕缩进和排列花括号的额外认知负荷,我们都在颤动中处理过这些问题。

把我的眼睛想象成一个饥饿的 cpu,哪个更适合处理? :)

在正常的View -> Text嵌套等情况下,嵌套很重要,因为它代表了屏幕上的_父子关系_。 对于像 Context 这样的特性(不确定 Flutter 是否有),它代表了上下文的范围。 所以嵌套本身在这些情况下具有重要的语义意义,不能被忽视。 您不能只是交换父母和孩子的位置并期望结果相同。

完全同意,我之前提到过这一点。 从语义上讲,在可视化显示树中创建额外的上下文层是零意义的,因为我正在使用具有状态的额外非可视化控制器。 使用 5 个动画师,现在您的小部件有 5 层深? 仅仅在那个高层次上,当前的方法有点味道。

有两个问题在我这里跳来跳去。

  1. 我怀疑在使用一些昂贵的资源时应该有多困难/明确存在一些分歧。 Flutter 的理念是它们应该更加困难/明确,以便开发人员认真考虑何时以及如何使用它们。 流、动画、布局构建器等代表了非平凡的成本,如果它们太简单,则可能会被低效使用。

  2. 构建是同步的,但您作为应用程序开发人员处理的最有趣的事情是异步的。 当然,我们不能使构建异步。 我们创建了这些便利,如 Stream/Animation/FutureBuilder,但它们并不总是能很好地满足开发人员的需求。 这可能说明我们在框架中很少使用 Stream 或 FutureBuilder。

我不认为解决方案是告诉开发人员在使用异步操作时总是只编写自定义渲染对象。 但是在我在这个 bug 中看到的示例中,异步和同步工作混合在一起,我们不能只是等待。 Build 必须在每次调用时产生一些东西。

fwiw,React 团队将重用可读性问题作为第一个动机:
Hooks 动机:很难在组件之间重用有状态逻辑
React不提供将可重用行为“附加”到组件的方法……您可能熟悉模式……试图解决这个问题。 但是这些模式要求您在使用组件时重构它们,这可能很麻烦并且使代码更难以遵循

这与 Flutter 目前无法为我们提供原生“组合状态”的方式非常相似。 这也反映了我们构建者时发生的情况,即修改我们的布局树并使其使用起来更麻烦,并且“更难遵循”,树说。

@dnfield如果每次都必须调用build ,也许我们可以使钩子不在build方法中,以便构建始终同步,即将它们放在initState的类中dispose是。 这样做有问题吗,那些编写钩子的人?

你可以对 Promises/Futures 提出同样的论点,并说await掩盖了它返回一个 Promise 的事实。

不,你没有。 Await 实际上只是一个功能的语法糖。 如果您使用冗长的 Futures 或声明性语法,代码的 __intent__ 是相同的。

这里的要求是将处理完全不同问题的源代码转移到同一把伞下,将各种不同的行为隐藏在一个关键字后面,并声称它以某种方式减少了认知负担。

这是完全错误的,因为现在每次使用该关键字时,我都需要考虑结果是否会执行任何异步操作、触发不必要的重建、初始化长寿命对象、执行网络调用、从磁盘读取文件或简单地返回静态值. 所有这些都是非常不同的情况,我必须熟悉我使用的钩子的味道。

我从讨论中了解到,这里的大多数开发人员不喜欢打扰这些类型的细节,并且想要轻松开发,只需能够使用这些“钩子”而不必担心实现细节。
在不了解其全部含义的情况下随意使用这种所谓的“钩子”会导致低效和糟糕的代码,并且会导致人们对自己开枪 - 因此它甚至不能解决“保护初学者开发人员”的问题。

如果您的用例很简单,那么是的,您可以随意使用钩子。 您可以随心所欲地使用和嵌套构建器,但是随着您的应用程序变得复杂以至于您发现自己难以重用代码,我认为必须更加关注您自己的代码和架构是有必要的。 如果我正在为潜在的数百万用户构建一个应用程序,我会非常犹豫使用“魔术”来抽象出我的重要细节。 现在我发现 Flutter 的 API 对于非常简单的用例来说非常简单,并且仍然灵活,允许任何人以非常有效的方式实现任何类型的复杂逻辑。

@Rudiksz再次

无论如何,一旦人们看到多个钩子以某种方式阻塞了他们的应用程序,他们仍然可以编写高效的代码; 当他们分析或什至只是运行应用程序时,他们会看到它,就像您使用当前样式一样。

@Rudiksz再次

哦,天哪,同样的论点也适用于抱怨框架问题的人。 没有人强迫他们不使用 hooks 包。

我将在这里非常直率。
这个问题真正与钩子、statefulwidget 以及谁使用什么无关,而是关于恢复数十年的最佳实践,以便某些人能够少写 5 行代码。

你的论点真的行不通。 创建这个问题的原因是 flutter_hooks 包并没有做所有可能在框架中拥有某些东西的事情,而当前的模型由于已经在框架中原生地存在。 争论的焦点是将 flutter_hooks 的功能本地移动到框架中。 你的论点假设我可以用当前模型做任何事情,我也可以用 hooks 包做,这是不正确的,在本次讨论中似乎来自其他人。 如果它们是真的,那么它会起作用,这也意味着钩子本身就在框架中,因此,再次,由于钩子和非钩子是等效的,您可以使用当前模型以及基于钩子的模型模型,这就是我所争论的。

我不确定您的最佳实践来自哪里,因为我知道保持代码易于阅读是最佳实践,而过度嵌套是一种反模式。 您具体指的是哪些最佳实践?

fwiw,React 团队将重用可读性问题作为第一个动机:
Hooks 动机:很难在组件之间重用有状态逻辑
React不提供将可重用行为“附加”到组件的方法……您可能熟悉模式……试图解决这个问题。 但是这些模式要求您在使用组件时重构它们,这可能很麻烦并且使代码更难以遵循

我听到每个人都对 Flutter 比 React 更神奇的地方赞不绝口。 也许是因为它不像 React 那样做所有事情? 你不能同时拥有它,你不能说 Flutter 比 React 领先几英里,还要求它做的一切都和 React 完全一样。

无论 Flutter 决定对给定问题使用什么解决方案,都应该根据其自身的优点而定。 我不熟悉 React,但显然我错过了一些非常了不起的技术。 :/

我认为没有人认为 Flutter 应该像 React 那样做所有事情。

但事实是,Flutter 的小部件层很大程度上受到了 React 的启发。 这在官方文档中有说明。
因此,与 React 组件相比,Widget 具有相同的优点和相同的问题。

这也意味着在处理这些问题上,React 比 Flutter 更有经验。
它面对他们的时间更长,并且更了解他们。

所以 Flutter 问题的解决方案与 React 问题的解决方案相似也就不足为奇了。

@Rudiksz Flutter 的用户 API 与 React 的基于类的模型非常相似,即使内部 API 可能不同(我不知道它们是否确实不同,我并没有真正遇到内部 API)。 我确实鼓励您尝试使用钩子进行 React 以了解它是如何的,正如我之前所说,似乎存在一种意见分歧,几乎完全基于那些在其他框架中使用过和未使用过类似钩子的结构的人。

鉴于它们的相似性,如上所述,问题的解决方案看起来相似也就不足为奇了。

拜托,让我们尽量不要互相争斗。

唯一会导致我们打架的事情就是扼杀这个讨论并且找不到解决方案。

我听到每个人都对 Flutter 比 React 更神奇的地方赞不绝口。 也许是因为它不像 React 那样做所有事情? 你不能同时拥有它,你不能说 Flutter 比 React 领先几英里,还要求它做的一切都和 React 完全一样。

指出 React 团队在提出钩子时也有类似的动机,证实了我们在这里表达的担忧。 它当然证实了在这种基于组件的框架中重用和组合常见的有状态逻辑存在问题,并且也在某种程度上证实了关于可读性、嵌套和在你的观点中“混乱”的一般问题的讨论。

对任何事情都不狂热,我什至从未在 React 中工作过,而且我喜欢 Flutter。 我可以很容易地看到这里的问题。

@Rudiksz在我们将其付诸实践之前,我们无法确定它在实践中是否具有性能。 现在决定不是很容易。

@Hixie这是一个常见的颤振用户可能具有的旅程示例,用于实现一个小部件,用于显示来自 userId 的用户昵称,包括HookWidgetStatefulWidget

__挂钩小部件__

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);
  }
}

__有状态的小部件__

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()逻辑移动到父小部件并保存结果。
  • 使用缓存管理器。

第一个解决方案是可以接受的,但有两个问题。
1 - 它呈现UserNickname无用,因为它现在只是一个 Text 小部件,如果你想在其他地方使用它,你必须重复你在父小部件中所做的事情(它有ListView ) . 显示昵称的逻辑属于UserNickname但我们必须单独移动它。
2 - 我们可以在许多其他子树中使用fetchNicknames()并且最好为所有应用程序缓存它,而不仅仅是应用程序的一部分。

所以想象一下我们选择缓存管理器并提供一个CacheManager类和InheritedWidgetsProvider

添加缓存支持后:

__挂钩小部件__

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);
  }
}

__有状态的小部件__

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);
  }
}

我们有一个套接字服务器,可以在昵称更改时通知客户端。

__挂钩小部件__

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);
  }
}

__有状态的小部件__

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);
  }
}

到目前为止,这两种实现都是可以接受的,并且很好。 IMO statful 中的样板完全没有问题。 当我们需要像UserInfo这样具有用户昵称和头像的小部件时,问题就出现了。 我们也不能使用UserNickname小部件,因为我们需要用“欢迎 [用户名]”这样的句子来显示。

__挂钩小部件__

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 widget__,我们不能只使用我们编写的逻辑。 我们必须将逻辑移到一个类中(例如您建议的Property ),并且我们仍然需要在新小部件中再次编写具有属性类的小部件胶水。

如果您看到前 3 个示例中的更改,我们根本没有更改小部件本身,因为唯一需要更改的是状态逻辑,而唯一更改的地方是状态逻辑。
这给了我们一个干净的(有意见的)、可组合的和完全可重用的状态逻辑,我们可以在任何地方使用它。

恕我直言,唯一的问题是调用useUserNickname很可怕,因为单个函数可以做这么多。
但在我多年的反应和使用flutter_hooks的经验中,在 rn 的 2 个应用程序中(大量使用钩子)证明没有良好的状态管理(我也尝试过 MobX 和其他状态管理解决方案,但是小部件中的胶水总是在那里)更可怕。 我不需要为前端应用程序中的每个屏幕编写 5 页文档,我可能需要在第一个版本发布后的几个月内添加一些小功能,以便了解我的应用程序页面的工作原理。 应用调用服务器太多? 简单的任务我去相关的钩子并改变它,整个应用程序修复,因为整个应用程序使用那个钩子。 我们可以在应用程序中使用类似的东西,而无需使用具有良好抽象的钩子,但我要说的是钩子是那种很好的抽象。

我很确定@gaearon比我能说得更好。 (如果他同意我的看法)

看上面的例子,上面的方法(有状态和钩子小部件)没有一个比另一个更高效。 但关键是其中之一鼓励人们编写高性能代码。

当有太多更新(例如动画)时,也可以只更新我们需要更新的子树,如StreamBuilder

1 - 简单地创建一个新的小部件,它对于HookWidgetStatefulWidget/StatelessWidget都是完全可行的选项
2 - 在flutter_hooks包中使用类似于HookWidgetBuilder ,因为父和子小部件数据非常紧密耦合。

旁注:我非常感谢@Hixie@rrousselGit讨论这个话题并在这个问题上投入了这么多精力。 我真的很期待这些会谈的结果。

基于@Hixie的起点,我想出了一些我认为非常酷/优雅的东西。 还没有准备好分享,但它允许我创建一些相当不错的代码示例,我认为比较apples:apples会更容易,而不是看起来如此陌生的 hooks。

所以,假设我们有一个带有这个签名的 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();
}

如果我们使用 vanilla animator 控制器来实现状态,我们会得到类似的结果:

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. vanilla 选项有多个可以创建错误的点。 忘记处理或忘记处理 didChange,或在 didChange 中犯了错误,并且您的代码库中有错误。 当使用多种类型的控制器时,情况会变得更糟。 然后你有单个函数来拆除所有不同类型的对象,这些对象不会像这样命名为 nice 和顺序。 这会变得混乱,很容易出错或错过条目。
  3. vanilla 选项没有提供重用常见模式或逻辑的方法,比如 playOnInit,所以我必须复制这个逻辑,或者在想要使用 Animator 的单个类中创建一些自定义函数。
  4. 这里没有必要理解 SingleTickerProviderMixin,它是“神奇的”,几个月来一直混淆了 Ticker 对我来说是什么(事后看来,我应该只读这门课,但每个教程都说:添加这个神奇的 mixin)。 在这里,您可以直接查看 StatefulAnimationProperty 的源代码,并了解动画控制器如何直接并在上下文中使用 Ticker 提供程序。

您确实必须了解 StatefulPropertyManager 的作用,但至关重要的是,这一次学习并应用于任何类型的对象,SingleTickerProviderMixin 主要特定于使用 Animator Controllers,并且每个控制器可能都有自己的混合以简化使用,这会变得混乱。 仅仅拥有知道所有这些东西的离散“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);
  }

[编辑] 好像是为了表达我自己的观点,我的香草示例有一个错误。 我忘记将正确的持续时间传递给 didUpdateWidget 中的每个动画师。 如果在代码审查中没有人注意到,我们需要多长时间才能在野外发现该错误? 没有人在阅读时发现它吗? 保留它,因为它是现实世界中发生的事情的完美例子。

这是一个鸟瞰图,样板标记为红色:
image

如果它是纯粹的样板文件,这不会那么糟糕,如果它丢失,编译器就会对你大喊大叫。 但这一切都是可选的! 当省略时,会产生错误。 所以这实际上是非常糟糕的做法,根本不是 DRY。 这是构建器的用武之地,但它们仅适用于简单的用例。

我认为对此非常有趣的是,一百行代码和一个简单的 State 的 mixin 如何使一堆现有的类变得多余。 例如,现在几乎不需要使用 TickerProviderMixins。 TweenAnimationBuilder 几乎不需要被使用,除非你真的_想要_创建一个子上下文。 许多传统的痛点,例如管理焦点控制器和 textInput 控制器,都得到了极大的缓解。 使用 Streams 变得更具吸引力且不那么笨拙。 在整个代码库中,一般可以减少对 Builder 的使用,这将导致更容易理解的树。

还可以_极其_轻松地创建自己的自定义状态对象,例如前面列出的 FetchUser 示例,目前基本上需要一个构建器。

我认为这将是在下一次 Flutter 开发人员调查中提出的一个非常有趣的问题。

这将是一个好的开始。 将这个问题分成不同的部分/问题,看看这是否是 Flutter 开发人员希望解决的真正问题。

一旦清楚,这次谈话将更加流畅和丰富

每条评论下的表情符号反应清楚地表明社区是否认为这是一个问题。 阅读了 250 多条针对此问题的长评论的开发人员的意见意味着很多恕我直言。

@esDotDev这与我一直在

我正在努力解决的主要问题是如何以有效的方式做到这一点。 例如, 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,

是的,我不知道这是否可行,但是 build 方法会像常规构建器一样采用子级,除了构建器的其他属性由属性设置。
如果您需要来自构建器的上下文,则 build 方法接受提供上下文的构建器参数。

在幕后,该方法可能只是创建一个具有指定属性的普通构建器,并将子参数传递给普通构建器并返回它。

假设你有这个代码:

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. 处理交流和自动收报机的清理
  5. 在刻度上重建视图

目前,开发人员手动和重复地处理(或不处理)1、3、4,以及处理 2 和 5 的半魔法 SingleTickerProviderMixin(我们将“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 绑定到它定义的状态,所以 ExpensiveParent 总是在这里重建。 然后我认为子级的缓存也是有问题的,因为在 Builder 示例中,它知道只在构建父状态时重建子级,但是在这种方法中,属性不知道何时使其缓存无效(但也许这可以解决吗?)

但是,当您想引入新的上下文时,这是构建器的完美用例。 我认为只有 StatefulProperties(纯状态)和 StatefulWidgets(状态和布局的混合)的上下文是非常优雅的。

每当您有意创建子上下文时,根据定义,您都会在树的下方进行,这有助于解决构建器的主要缺点之一(在整个树中强制嵌套)

@escamoteur (和写评论的@sahandevs )是的,我之前在研究这个。 我认为这肯定有助于展示人们想要删除的那种逻辑。 不过,我认为该示例本身有点可疑,因为我希望大部分逻辑(例如缓存周围的一切)都在应用程序状态业务逻辑中,而远不及小部件。 我也看不出有什么好方法可以在不破坏热重载的情况下使语法像该评论中建议的那样简短(例如,如果您更改正在使用的钩子数量,则不清楚如何在重载期间保持它们的状态) )。

也就是说,我认为上面@esDotDev@TimWhiting展示的工作非常有趣,可以解决这些问题。 它不像 Hooks 那样简短,但更可靠。 我认为将这样的东西打包是非常有意义的,如果它运行良好,它甚至可以成为 Flutter 的最爱。 我不确定它作为核心框架特性是否有意义,因为一旦你考虑到构建属性的复杂性和性能影响,以及不同的人会如何喜欢不同的风格,改进就不是那么大了。 归根结底,不同的人使用不同的风格应该没问题,但我们不希望核心框架有多种风格,这只是对新开发人员的误导。

还有一种观点认为,学习 Flutter 的正确方法是首先了解小部件,然后学习将它们抽象出来的工具(Hooks 或其他),而不是直接跳到抽象语法上。 否则,您将错过系统工作方式的一个关键组件,这可能会导致您在编写高性能代码方面误入歧途。

我也看不出有什么好方法可以在不破坏热重载的情况下使语法像该评论中建议的那样简短(例如,如果您更改正在使用的钩子数量,则不清楚如何在重载期间保持它们的状态) )。

钩子可以毫无问题地与热重载一起工作。
具有不匹配 runtimeType 的第一个钩子会导致所有后续钩子被销毁。

这支持添加、删除和重新排序。

我认为有一种观点认为完全抽象比当前存在的部分更可取。

如果我想了解 Animator 在属性上下文中是如何工作的,我要么完全忽略它,要么跳进去,它就在那里,自成一体且连贯。

如果我想了解 AnimatorController 在 StatefulWidget 的上下文中是如何工作的,我需要(被迫)了解基本的生命周期钩子,但不必了解底层的滴答机制是如何工作的。 这在某种意义上是两全其美的。 没有足够的魔力让它“正常工作”,但足以让新用户迷惑并迫使他们盲目地相信一些 mixin(这对大多数人来说本身就是一个新概念)和一个神奇的 vsync 属性。

我不确定代码库中的其他示例,但这适用于为 StatefulWidget 提供了一些 helper mixin 的任何情况,但仍有一些其他引导程序必须始终执行。 开发人员将学习引导程序(无聊的部分)并忽略 Mixin(有趣/复杂的部分)

也就是说,我认为上面@esDotDev@TimWhiting展示的工作非常有趣,可以解决这些问题。 它不像 Hooks 那样简短,但更可靠

这如何更可靠?

我们仍然无法有条件地或在其生命周期之外创建/更新属性,因为我们可能会进入错误状态。 例如,当条件为假时,有条件地调用属性不会处置该属性。
每次重建时,所有房产仍会重新评估。

但它会导致多个问题,例如强制用户在 NNBD 之后到处使用!或可能允许用户在更新之前访问属性。

例如,如果有人读取didUpdateWidget的属性怎么办?

  • initProperties在生命周期之前执行? 但这意味着我们可能必须在每次构建时多次更新属性。
  • 在 didUpdateWidget 之后是否执行了initProperties ? 然后在 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));

    ...
  }
}

condition切换为 false 时,它​​将处理value2

将构建器包提取到函数中:

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。 这里需要的领域知识是_deep_。 我有脚本知识来实现​​这样一个简单的缓存系统,我什至没有接近领域知识来了解可能发生的每个边缘情况,或者我们可能进入的不良状态。 除了 Remi,我认为 Flutter 团队之外的世界上大概有 4 个开发人员知道这些东西! (明显夸大)。

支持无状态小部件的问题是一个很好的问题。 一方面我明白了,StatefulWidgets 非常冗长。 另一方面,这里我们真正谈论的是纯粹的冗长。 必须定义 2 个类不会发生任何错误,您无法将其搞砸,编译器不允许您这样做,我不想在 StatelessWidget 中做任何有趣的事情。 所以我不认为这是一个主要问题......当然会有很好的,但它是最后的 5% imo,而不是被卡住的东西。

另一方面……来自带有关键字支持的 remi 语法非常漂亮,而且非常灵活/强大。 如果它免费为您提供 StatelessWidget 支持,那只是额外的 🔥

支持 StatelessWidget 是一件大事 IMO。 可选,但仍然很酷。

虽然我同意这并不重要,但人们已经在争论使用函数而不是 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');
}

这是functional_widget支持的东西——它是一个代码生成器,你可以在其中编写一个函数并为你生成一个类——它也支持HookWidget

不同之处在于,在 Dart 中支持高阶函数将消除代码生成以支持此类语法的需要。

我在猜测@Hixie更可靠的意思是什么,它不会受到钩子所具有的操作顺序/条件问题的影响,因为这对于架构 POV 来说非常“不可靠”(尽管我意识到这是一个简单的规则)学而不违,一学即成)。

但是您的提案也没有使用关键字。 我认为 new 关键字的情况非常好:

  • 比嫁接到 State 更灵活和可组合
  • 更简洁的语法
  • 适用于无状态,这是一个非常好的选择

我不喜欢它的是,我们担心在一些简单的对象上多次/构建设置属性的成本,但随后提倡一种基本上会创建一百万级上下文和一堆布局成本的解决方案。 我误会了吗?

另一个缺点是这种魔法的想法。 但是,如果您要做一些神奇的事情,我认为新关键字是一种有效的方式来完成它,因为它可以轻松突出显示并向社区发出呼吁,并解释它是什么以及它是如何工作的。 基本上所有人都在谈论明年在 Flutter 中的所有话题,我相信我们会看到大量很酷的插件从中产生。

我在猜测@Hixie更可靠的意思是什么,它不会受到钩子所具有的操作顺序/条件问题的影响,因为这对于架构 POV 来说非常“不可靠”(尽管我意识到这是一个简单的规则)学而不违,一学即成)。

但是钩子也不会遇到这样的问题,因为它们是静态可分析的,因此当它们被滥用时我们可能会出现编译错误。

这不是问题

同样,如果自定义错误是不可行的,那么正如我之前提到的,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,它们是非常单一的用途)。

关键是,在构建这样的东西时,构建器的工作效果不佳,因为它们一开始就让您陷入可读性(和可写性)漏洞中,因此它们非常适合简单的用例,它们不会“组合”好。 Compose 是 2 个或多个事物的组合。

不要关注我如何使用它们。 在一个真实的例子中,...

...然后回到第一个。 你为什么不举一个真实的例子?

您需要使用复杂动画的真实示例吗?
https://github.com/gskinnerTeam/flutter_vignettes

显示一些任意复杂的动画只会混淆示例。 可以说,在一些小部件中使用多个动画师(或您可以想象的任何其他有状态对象)有很多用例

当然。 这里的例子是在一些小部件中利用 3 个动画来做“X”。 示例中特意简化了 X,以突出样板的数量。

小部件“核心”将是一百行或其他东西

在另一篇文章中,您发布了一个带有模糊“核心”的样板示例,但现在您告诉我们核心将是数百行? 那么实际上,与核心相比,样板文件会微不足道吗? 你不能同时拥有它。
你不断地改变你的论点。

不要关注我如何使用它们。 在一个真实的例子中,...

...然后回到第一个。 你为什么不举一个真实的例子?

可能是因为在玩各种想法时需要花费大量时间来创建一个真实的示例。 目的是让读者想象如何在真实情况下使用它,而不是提到有其他方法可以解决它。 当然可以使用动画容器,但如果不能呢? 如果只用一个动画容器来制作它太复杂了怎么办。

现在,作者没有使用真正真实的例子,可以证明是好的还是坏的,我对此没有意见,我只是评论这个线程中提出改善问题的趋势,这些问题没有彻底解决手头的问题。 这似乎是 hooks 支持者和反对者之间混淆的主要来源,因为每个人似乎在某种程度上都在谈论另一个,所以我支持 Hixie 的提议,即创建一些真正的应用程序,这样反对者就不能说“真实”的例子是没有显示,支持者不能说人们应该仅仅想象一个真实世界的场景。

我想我说的是有 100 行的类是愚蠢的,其中一半是样板。 这正是我在这里描述的。 核心,无论多大,都不应该被一堆噪音混淆,当使用多个构建器时肯定会出现这种情况。

原因是大型代码库的扫描能力、可读性和维护性。 这不是线条的写作,尽管由于倾向于进入大括号地狱,在建设者中写作是生产力的失败者。

在另一篇文章中,您发布了一个带有模糊“核心”的样板示例,但现在您告诉我们核心将是数百行? 那么实际上,与核心相比,样板文件会微不足道吗? 你不能同时拥有它。
你不断地改变你的论点。

同样,这个问题与样板无关,而是关于可读性和可重用性。
如果我们有 100 行也没关系。
重要的是这些行的可读性/可维护性/可重用性。

即使争论是关于样板的,我作为用户为什么要在任何情况下容忍这样的样板,给定一种足够等效的方式来表达同样的事情? 编程就是创建抽象和自动化劳动,我真的不认为在各种类和文件中一遍又一遍地重做同样的事情有什么意义。

您需要使用复杂动画的真实示例吗?
https://github.com/gskinnerTeam/flutter_vignettes

当然,你不能指望我深入研究你的整个项目。 我应该查看哪个文件?

显示一些任意复杂的动画只会混淆示例。

恰恰相反。 显示一些任意复杂的动画,可以不受任何现有的解决方案来解决将例子,那就是Hixie不断问什么,我相信。

只需扫描 gif 并开始想象如何构建其中的一些东西。 该存储库实际上是 17 个独立的应用程序。 你也不能指望我给你写一些随意的动画只是为了向你证明复杂的动画是可以存在的。 从 Flash 开始,我一直在构建它们 20 年,每一个都与上一个不同。 无论如何,它并不是特定于动画的,它们只是最简单最熟悉的 API 来说明一个更大的观点。

就像你知道的那样,当你使用动画师时,你每次都需要做 6 件事,但它也需要生命周期钩子?? 好的,现在将其扩展到任何每次都必须执行 6 个步骤的事情……并且您需要在 2 个地方使用它。 或者您需要一次使用其中的 3 个。 这显然是一个问题,我不知道我还能补充什么来解释它。

编程就是创建抽象和自动化劳动,我真的不认为在各种类和文件中一遍又一遍地重做同样的事情有什么意义。

__全部__? 那么性能、可维护性不相关吗?

“自动化”一项劳动和“做”一项劳动是一样的。

伙计们,如果您没有时间或不想创建真实的示例,那也没关系,但是请注意,如果您对创建示例来解释问题不感兴趣,那么您也不应该期望人们会被迫解决问题(这比创建示例来显示问题要多得多)。 这里没有人需要为任何人做任何事情,这是一个开源项目,我们都在努力互相帮助。

@TimWhiting你介意在你的https://github.com/TimWhiting/local_widget_state_approaches 存储库中放一个许可证文件吗? 有些人在没有适用的许可证(BSD、MIT 或类似的理想情况下)的情况下无法做出贡献。

为什么我,用户,在任何情况下都应该容忍这样的样板,给定一种足够等效的方式来表达同样的事情?

是的,可维护性和性能当然很重要。 我的意思是,当有等效的解决方案时,我们应该选择样板更少、更易于阅读、更可重用等的解决方案。 这并不是说钩子就是答案,因为我没有测量它们的性能,但根据我的经验,它们更易于维护。 如果将类似钩子的构造放入 Flutter 核心,我仍然不确定您关于它如何影响您的工作的论点。

只需扫描 gif 并开始想象如何构建其中的一些东西。

我扫描了 gif。 我不会使用构建器小部件。
许多动画非常复杂,如果我知道您使用更高级别的构建器实现它们,我可能不会使用您的包。

无论如何,这个讨论似乎因为更多的个人分歧而失控。 我们应该专注于手头的主要任务。 如果反对者如我之前所说,找到不能真正解决所提出问题的改进,我不确定钩子的支持者如何展示较小的例子。 我认为我们现在应该为@TimWhiting的存储库做出贡献。

展示一些任何现有解决方案都无法解决的任意复杂动画就是一个例子,这就是 Hixie 一直在问的,我相信。

展示今天不可能的事情的例子超出了这个问题的范围。
这个问题是关于改进已经可行的语法,而不是解除一些今天不可能的事情。

任何要求提供今天不可能的东西都是题外话。

@TimWhiting你介意在你的https://github.com/TimWhiting/local_widget_state_approaches 存储库中放一个许可证文件吗? 有些人在没有适用的许可证(BSD、MIT 或类似的理想情况下)的情况下无法做出贡献。

完毕。 抱歉,我没有太多时间来处理这些示例,但我可能会在本周的某个时候进行处理。

展示一些任何现有解决方案都无法解决的任意复杂动画就是一个例子,这就是 Hixie 一直在问的,我相信。

展示今天不可能的事情的例子超出了这个问题的范围。
这个问题是关于改进已经可行的语法,而不是解除一些今天不可能的事情。

任何要求提供今天不可能的东西都是题外话。

让我重新表述我说过的话。

展示一些涉及动画、状态等的任意复杂难以编写并且可以在不影响性能的情况下显着改进,这就是示例,这就是 Hixie 一直在问的,我相信。

我理解推动更少的样板、更多的可重用性和更多的魔力。 我也喜欢编写更少的代码,而且做更多工作的语言/框架非常有吸引力。
到目前为止,这里介绍的示例/解决方案组合都不会显着改进代码。 也就是说,如果我们关心的不仅仅是我们必须编写多少行代码。

伙计们,如果您没有时间或不想创建真实的示例,那也没关系,但是请注意,如果您对创建示例来解释问题不感兴趣,那么您也不应该期望人们会被迫解决问题(这比创建示例来显示问题要多得多)。 这里没有人需要为任何人做任何事情,这是一个开源项目,我们都在努力互相帮助。

@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 多个类文件的项目中广泛传播,您就可以完美地了解我们试图避免的可读性和可维护性问题。

@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 个动画,并且相当地装载了样板和 cruft。

重要的是要注意,我可以完成任何需要硬弹回起始位置的动画(消除所有隐式小部件),或在 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,这是一个 10line 的命中率。

如果我们使用诸如“关键字”之类的东西,我们会将样板行减少到基本上为 0%。 该类的整个重点可以放在(独特的)业务逻辑和可视化树元素上。 一般来说,我们使 StatefulWIdget 的使用更加罕见,绝大多数视图的冗长程度减少了 10% 或 20%,并且更加集中。

另外,值得注意的是,上面的面板场景是真实世界的,在现实世界中显然这种方法真的不好,所以我们没有使用它,你也不会在代码库中看到它。 我们也不会使用嵌套构建器,因为它们看起来很粗糙,所以你也不会看到。

我们构建了一个专用的 SlidingPanel 小部件,它接受一个 IsOpen 属性,并自行打开和关闭。 这通常是这些用例中的每一个的解决方案,在这些用例中,您需要一些特定的行为,将有状态的逻辑向下移动到一些非常特定的小部件中,然后使用它。 您基本上编写了自己的 ImplicitlyAnimatedWidget。

这通常可以正常工作,但仍然需要时间和精力,实际上它存在的唯一原因是因为使用动画太难了(它的存在导致重用有状态组件通常很难)。 例如,在 Unity 或 AIR 中,我不会创建一个专门用于在单个轴上移动面板的专用类,它只是打开或关闭一行代码,专用小部件将无事可做。 在 Flutter 中,我们必须创建一个专用小部件,因为它实际上是封装 AnimatorController 的引导和拆卸的唯一合理方法(除非我们想用 TAB 嵌套、嵌套、嵌套)

我的主要观点是这种类型的事情是为什么很难找到现实世界的例子。 作为开发人员,我们不能让这些东西在我们的代码库中存在太多,所以我们用不太理想但有效的解决方法来解决它们。 然后,当您查看代码库时,您只会看到这些变通方法生效,一切似乎都很好,但这可能不是团队想要的,可能他们花了 20% 的时间到达那里,这可能是总共调试的痛苦,这些都不是显而易见的代码。

为了完整起见,这里是由构建器制作的相同用例。 行数大大减少,没有机会出现错误,不需要学习外国概念 RE TickerProviderMixin ......但这种嵌套令人悲伤,各种变量散布在整个树中的方式(动态结束值,value1,value2 等) ) 使业务逻辑比它需要的更难阅读。

```飞镖
类 _SlidingPanelViewState 扩展状态{
bool isLeftMenuOpen = true;
bool isRightMenuOpen = true;
bool isBtmMenuOpen = true;

@覆盖
小部件构建(BuildContext 上下文){
返回 TweenAnimationBuilder(
补间:补间(开始:0,结束:isLeftMenuOpen?1:0),
持续时间:widget.slideDuration,
建造者:(_,leftAnimValue,__){
返回 TweenAnimationBuilder(
补间:补间(开始:0,结束:isRightMenuOpen?1:0),
持续时间:widget.slideDuration,
建造者:(_,rightAnimValue,__){
返回 TweenAnimationBuilder(
补间:补间(开始:0,结束:isBtmMenuOpen?1:0),
持续时间:widget.slideDuration,
建设者:(_,btmAnimValue,__){
双 leftPanelSize = 320;
double leftPanelPos = -leftPanelSize * (1 - leftAnimValue);
双 rightPanelSize = 230;
double rightPanelPos = -rightPanelSize * (1 - rightAnimValue);
双底面板尺寸 = 80;
double bottomPanelPos = -bottomPanelSize * (1 - btmAnimValue);
返回堆栈(
孩子们: [
//背景
容器(颜色:Colors.white),
//主要内容区
定位(
顶部:0,
左:leftPanelPos + leftPanelSize,
底部:bottomPanelPos + bottomPanelSize,
右:rightPanelPos + rightPanelSize,
孩子:ChannelInfoView(),
),
//左面板
定位(
顶部:0,
左:leftPanelPos,
底部:bottomPanelPos + bottomPanelSize,
宽度:leftPanelSize,
孩子:ChannelMenu(),
),
//底部面板
定位(
左:0,
右:0,
底部:bottomPanelPos,
高度:bottomPanelSize,
孩子:通知栏(),
),
//右侧面板
定位(
顶部:0,
右:rightPanelPos,
底部:bottomPanelPos + bottomPanelSize,
宽度:rightPanelSize,
孩子:设置菜单(),
),
// 纽扣
排(
孩子们: [
Button("left", () => setState(() => isLeftMenuOpen = !isLeftMenuOpen)),
Button("btm", () => setState(() => isBtmMenuOpen = !isBtmMenuOpen)),
Button("right", () => setState(() => isRightMenuOpen = !isRightMenuOpen)),
],
)
],
);
},
);
},
);
},
);
}
}

最后一个很有趣......我最初打算建议构建器应该围绕定位小部件而不是堆栈(我仍然建议左右面板)但后来我意识到底部的影响所有三个,我意识到构建器只给你一个child参数实际上是不够的,因为你真的想保持ContainerRow不变建立。 我想您可以在第一个构建器上方创建它们。

我的主要观点是这种类型的事情是为什么很难找到现实世界的例子。 作为开发人员,我们不能让这些东西在我们的代码库中存在太多,所以我们用不太理想但有效的解决方法来解决它们。 然后,当您查看代码库时,您只会看到这些变通方法生效,一切似乎都很好,但这可能不是团队想要的,可能他们花了 20% 的时间到达那里,这可能是总共调试的痛苦,这些都不是显而易见的代码。

郑重声明,这并非意外。 这在很大程度上是设计使然。 让这些小部件成为它们自己的小部件可以提高性能。 我们非常希望这是人们使用 Flutter 的方式。 这就是我上面提到的“Flutter 设计理念的很大一部分是引导人们走向正确的选择”时所指的。

我最初打算建议构建器应该围绕 Positioned 小部件而不是 Stack (我仍然建议左右面板)

为了简洁起见,我实际上省略了它,但通常我可能会在这里需要一个 Content 容器,它会使用所有 3 个菜单中的大小来定义它自己的位置。 这意味着所有 3 个都需要在树的顶部,如果有任何重建,我需要重建整个视图,不能绕过它。 这基本上是您经典的桌面式脚手架。

当然,我们可以开始拆树,这总是一个选项,我们经常将它用于较大的小部件,但我从来没有见过它实际上一目了然地提高了可理解性,读者需要在那里做一个额外的认知步骤观点。 当你把树拆开的那一刻,我突然开始跟踪变量赋值的面包屑痕迹,以找出这里传递的内容以及整个事物如何组合在一起变得封闭。 根据我的经验,将树呈现为可消化的树总是更容易推理。

可能是专用 RenderObject 的一个很好的用例。

或者 3 个易于管理的 AnimatorObjects,它们可以将自己挂钩到小部件生命周期中:D

让这些小部件成为它们自己的小部件可以提高性能。

由于我们在这里变得具体:在这种情况下,因为这是一个脚手架,每个子视图都已经是它自己的小部件,这个家伙负责布置和翻译孩子。 孩子将是 BottomMenu()、ChannelMenu()、SettingsView()、MainContent() 等

在这种情况下,我们用另一层独立的小部件包装了一堆独立的小部件,只是为了管理移动它们的样板。 我不相信这是一场表演胜利? 在这种情况下,我们被推到框架_认为_我们想做的事情,而不是我们真正想做的事情,即以更简洁和连贯的方式编写同等性能的视图。

[编辑] 我将更新示例以添加此上下文

我建议使用专用的 RenderObject 的原因是它会使布局更好。 我同意动画方面将在有状态的小部件中,它只会将三个 0..1 双精度值 (value1, value2, value3) 传递给渲染对象,而不是 Stack 数学。 但这主要是本次讨论的一个小插曲; 在您执行此操作时,您仍然希望进行 Hooks 或该有状态小部件中的类似功能提供的简化。

在更相关的说明中,我在为@TimWhiting的项目创建演示时https :
我很好奇 Hooks 版本会是什么样子。 我不确定我能找到一种使它更简单的好方法,尤其是保持性能特征(或改进它们;那里有一条评论显示了它目前不是最佳的地方)。

(我也很好奇这是否是一种如果我们找到一种方法来简化它,我们就会在这里完成的事情,或者如果它缺少我们在完成之前需要解决的关键问题。)

恢复的东西对这个例子很重要吗? 因为它,我很难跟随,甚至不知道 RestorationMixin 做什么。 我想这是……我需要一点时间来理解这一点。 我相信 Remi 会在 4 秒内完成钩子版本:)

目前不支持使用HookWidget而不是StatefulHookWidget的恢复 API。

理想情况下,我们应该能够改变

final value = useState(42);

进入:

final value = useRestorableInt(42);

但它需要一些思考,因为当前的恢复 API 并没有真正考虑到钩子。

作为旁注,React hooks 带有一个“关键”功能,通常像这样使用:

int userId;

Future<User> user = useMemo(() => fetchUser(id), [id]);

这段代码的意思是“缓存回调的结果,并在数组中的任何内容发生变化时重新评估回调”

Flutter_hooks 对此进行了一对一的重新实现(因为它只是一个端口),但我认为这不是我们想要为 Flutter 优化代码做的事情。

我们可能想要:

int userId;

Future<User> user = useMemo1(id, (id) => fetchUser(id));

这会做同样的事情,但通过避免列表分配和使用功能撕裂来消除内存压力

在这个阶段这并不重要,但值得一提的是我们是否计划使用flutter_hooks作为示例。

@Hixie我将您的动画示例移植到 hooks

这是一个有趣的例子,值得思考!
这是一个很好的例子,默认情况下,“活动”和“持续时间”的实现无处不在(并且彼此依赖)。
有很多“if(active)”/“controller.repeat”调用

而使用钩子,所有逻辑都以声明方式处理并集中在一个地方,没有重复。

该示例还展示了如何使用钩子轻松缓存对象——这解决了 ExpensiveWidget 重建过于频繁的问题。
我们获得了 const 构造函数的好处,但它适用于动态参数。

我们还获得了更好的热重载。 我们可以更改背景颜色的 Timer.periodic 持续时间,并立即查看更改的效果。

@rrousselGit你有链接吗? 我在@TimWhiting的存储库中没有看到任何新

目前不支持使用HookWidget而不是StatefulHookWidget的恢复 API。

无论我们提出什么解决方案,我们都需要确保它不需要知道每一个最后的 mixin。 如果有人想将我们的解决方案与其他一些引入 mixin 的包结合使用,就像 TickerProviderStateMixin 或 RestorationMixin 一样,他们应该能够这样做。

https://github.com/TimWhiting/local_widget_state_approaches/pull/3

同意,但我并不担心。 例如,useAnimationController 不需要用户关心 SingleTickerProvider。

AutomaritKeepAlive 可以从相同的处理中受益。
我在想的一件事是有一个“useKeepAlive(bool)”钩子

这避免了 mixin 和“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您认为尝试制作一个代表关键字支持外观的版本是否有意义? 它与仅具有专用“使用”关键字的钩子基本相似,还是变得更容易遵循? 很难在一个平衡的基础上比较 hook 方法,因为它有很多外国概念,如 useEffect、useMemo 等,我认为这使它看起来比它更神奇?

另一件需要注意的事情是,如果你有多个小部件,所有这些小部件都需要共享这种“颜色步进”逻辑,但以完全不同的方式使用生成的颜色。 使用钩子风格的方法,我们只需捆绑任何可以重用的逻辑,然后我们就简单地使用它。 没有我们被迫进入的建筑角落,它真正是不可知的和灵活的。

在有状态的方法中,我们被迫进入

  • 复制和粘贴逻辑(非常不可维护)
  • 使用构建器(不是超级可读,尤其是在使用嵌套时)
  • 混合(组合不好,不同的混合很容易在它们的共享状态下发生冲突)

我认为的关键是,在后者中,您会立即遇到架构问题,我应该将它放在树中的什么位置,如何最好地封装它,它应该是构建器,还是自定义小部件? 对于前者,唯一的决定是在哪个文件中保存这一块重用的逻辑,对您的树根本没有影响。 当您想一起使用这些逻辑封装中的一些,在层次结构中上下移动它们或移动到同级小部件等时,这在架构上非常好

我绝不是专家级开发人员。 但是这种重用逻辑并将其写在一个地方的新风格真的很方便。

我很长时间没有使用 Vue.js,但他们甚至有自己的 API(受 Hooks 启发)用于他们的下一个版本,可能值得一看。

而 CMIIW,我认为反应钩子的规则(不要使用条件)不适用于 Composition API。 因此,您不需要使用 linter 来执行规则。

动机部分再次强烈地加强了此处的 OP:

动机
随着功能随着时间的推移而增长,复杂组件的代码变得更难推理。 这种情况尤其发生在开发人员阅读不是他们自己编写的代码时。
[有一个] 缺乏用于在多个组件之间提取和重用逻辑的干净且免费的机制。

关键词是“干净”和“免费”。 Mixin 是免费的,但它们并不干净。 构建器是干净的,但它们在可读性和小部件架构方面不是免费的,因为它们更难在树上移动并根据层次结构进行推理。

我还认为在可读性讨论中要注意这一点很重要:“_尤其是当开发人员正在阅读他们没有编写的代码时_”。 当然_你的_嵌套构建器可能很容易阅读,你知道那里有什么并且可以轻松地跳过它,它正在阅读其他人的代码,就像你在任何更大的项目中所做的那样,或者你自己的代码几周/几个月前,当它变得相当烦人/难以解析和重构这些东西。

其他一些特别相关的部分。

为什么仅仅拥有组件是不够的:

创建...组件允许我们将界面的可重复部分及其功能提取到可重用的代码段中。 仅此一项就可以使我们的应用程序在可维护性和灵活性方面走得更远。 然而,我们的集体经验证明,仅凭这一点可能还不够,尤其是当您的应用程序变得非常大时 - 考虑数百个组件。 在处理如此大的应用程序时,共享和重用代码变得尤为重要。

关于为什么减少逻辑碎片和更强烈地封装事物是一个胜利:

碎片化导致难以理解和维护复杂的组件。 选项的分离掩盖了潜在的逻辑问题。 此外,在处理单个逻辑问题时,我们必须不断“跳转”相关代码的选项块。 如果我们可以搭配与相同逻辑关注点相关的代码,那就更好了。

我很好奇,除了我提交的建议之外,是否还有其他关于我们正在努力改进的规范示例的建议。
如果这是人们想要使用的起点,那就太好了。 但是,我想说它现在最大的问题是冗长; 在那个例子中没有多少代码可以重用。 所以我不清楚它是否能很好地代表 OP 中描述的问题。

我一直在思考如何表达我个人会在解决方案中寻找的特征,这让我意识到我在当前的 Hooks 提案中看到的一个大问题,这也是我不会这样做的原因之一想要将其合并到 Flutter 框架中:Locality and Encapsulation,或者更确切地说,缺少它们。 Hooks 的设计使用全局状态(例如静态来跟踪当前正在构建的小部件)。 恕我直言,这是我们应该避免的设计特征。 通常,我们尝试使 API 成为自包含的,因此如果您使用一个参数调用一个函数,您应该确信它无法对该参数之外的值执行任何操作(这就是我们传递 BuildContext 的原因,而不是相当于useContext )。 我并不是说这是每个人都必然想要的特征; 当然,如果这对他们来说不是问题,人们可以使用 Hooks。 只是我想避免在 Flutter 中做更多的事情。 每次我们拥有全局状态(例如在绑定中)时,我们最终都会后悔。

Hooks 可能是上下文中的方法(我认为它们是早期版本),但老实说,我认为合并它们目前没有太大价值。 Merge 需要有一些优势来分离包,比如提高性能或挂钩特定的调试工具。 否则,它们只会增加混乱,例如,您将有 3 种官方的收听方式:AnimatedBuilder、StatefulWidget 和 useListenable。

所以对我来说,要走的路是改进代码生成——我提出了一些改变: https :

如果这些建议真的得到实施,那些想要在他们的应用程序中使用类似 SwiftUI 的神奇解决方案的人就可以制作一个包,而不会打扰其他人。

在这个阶段讨论钩子的有效性有点题外话,因为据我所知,我们在这个问题上仍然没有达成共识。

如本期多次所述,还有许多其他解决方案,其中多个是语言特性。
Hooks 只是来自另一种技术的现有解决方案的一个端口,以最基本的形式实现它的成本相对较低。

这个特性可以走一条与 hooks 完全不同的路径,比如 SwiftUI 或 Jetpack Compose 所做的; “命名混合”提案,或 Builders 提案的语法糖。

我坚持认为这个问题的核心是要求简化像 StreamBuilder 这样的模式:

  • StreamBuilder 由于嵌套而具有较差的可读性/可写性
  • Mixin 和函数不能替代 StreamBuilder
  • 到处复制粘贴StreamBuilder在所有StatefulWidgets的实现是不合理的

到目前为止,所有评论都提到了 StreamBuilder 的替代方案,无论是针对不同的行为(创建一次性对象,发出 HTTP 请求,...)还是提出不同的语法。

我不知道还有什么要说的,所以我不知道我们可以如何进一步取得进展。
您在@Hixie 的声明中不理解/不同意的是什么?

@rrousselGit你能创建一个演示应用程序来显示这个吗? 我试图创建一个演示应用程序,显示我所理解的问题,但显然我没有做对。 (我不确定“像 StreamBuilder 这样的模式”和我在演示应用程序中所做的有什么区别。)

  • StreamBuilder 由于嵌套而具有较差的可读性/可写性

你已经说过冗长不是问题。 嵌套只是冗长的另一个方面。 如果嵌套确实是一个问题,我们应该处理 Padding、Expanded、Flexible、Center、SizedBox 和所有其他无缘无故添加嵌套的小部件。 但是通过拆分整体小部件可以轻松解决嵌套问题。

到处复制粘贴StreamBuilder在所有StatefulWidgets的实现是不合理的

您的意思是复制粘贴创建和处理 StatefulWidgets 需要创建和处理的流的代码行吗? 是的,这是绝对合理的。

如果您有 10 个或上帝禁止数百个需要创建/处理自己的流的 _不同_ 自定义 StatefulWidgets - 钩子术语中的“使用”-,那么您需要担心的问题比“逻辑”重用或嵌套更大。我会担心关于为什么我的应用程序必须首先创建这么多不同的流。

公平地说,我认为有人认为他们的应用程序中的特定模式不合理是可以的。 (这并不一定意味着框架必须本身支持它,但至少允许一个包来解决它会很好。)如果有人不想使用stream.listenStreamBuilder(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 钩子会检查它是否已在每个动画刻度上更新。 比如,“使用”那个可听的小部件是否更新了? 时长有变化吗? 实例被替换了吗?
memoized hook,我什至不知道这是怎么回事。 它可能是为了缓存一个对象,但在每次构建时,小部件都会检查对象是否更改? 什么? 为什么? 当然,因为它在有状态的小部件中使用,而树上的其他一些小部件可能会更改该值,因此我们需要轮询更改。 这是字面轮询行为,与“反应式”编程完全相反。

更糟糕的是,“新”和“旧”钩子都具有相同的实例类型,都具有相同的值,但函数会遍历这些值以检查它们是否发生了变化。 在_每个动画滴答上_。

这是我得到的输出,无穷无尽。

/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

所有这些工作都是在每个动画滴答上完成的。 如果我添加另一个钩子,如“最终颜色 = useAnimation(animationColor);",为了使颜色也有动画效果,现在小部件检查 _two_ 次是否已更新。

我坐在这里看着文本动画来回移动,应用程序状态或任何小部件或小部件树都没有变化,而且钩子仍然不断检查树/小部件是否更新。 让每个“使用”这些特定钩子的小部件都执行这种轮询行为是不好的。

在构建方法中处理状态对象的初始化/更新/处置逻辑只是糟糕的设计。 在可重用性、热重载或认知负载方面获得的任何改进都不能证明对性能的影响是合理的。
再次,在我看来。 Hooks 是一个包,任何人都可以使用它们,如果他们认为收益证明开销是合理的。

此外,如果我们开始尝试抽象构建过程中的所有内容,我认为任何数量的语言功能、编译器魔法或抽象都无法阻止此类不必要的检查。 所以我们还有其他选择,比如扩展 StatefulWidget。 已经可以做到的事情,被无数次驳回。

@Hixie你还没有回答这个问题。 在上面列出的项目符号列表中,您不理解/同意的是什么?

如果不知道您要我演示什么,我就无法举例。

@Rudiksz当然,我们实际考虑的任何解决方案都需要进行分析和基准测试,以确保它不会让事情变得更糟。 我构建提交给@TimWhiting的演示应用程序的部分方式旨在准确涵盖容易搞砸的模式类型。 (而且,如前所述,它确实有改进的余地,请参阅代码中的 TODO。)

@rrousselGit我真的不想深入探讨这个问题,因为它太主观了,但既然你问:

  • 首先,我一般会避免使用 Streams。 恕我直言,ValueListenable 是一种更好的模式,适用于或多或少相同的用例。
  • 我不认为 StreamBuilder 是特别难读或写的; 但是,正如@satvikpendem之前评论的那样,我的历史是在 HTML 中深度嵌套的树,而且我已经关注 Flutter 树 4 年了(我之前说过 Flutter 的核心能力是如何有效地行走巨树),所以我可能比大多数人有更高的容忍度,我的意见在这里并不重要。
  • 至于 mixin 和函数是否可能是 StreamBuilder 的替代方案,我认为 Hooks 很好地证明了你绝对可以使用函数来监听流,而且 mixin 可以清楚地做任何类在这里可以做的事情,所以我不明白为什么他们会也不是解决办法。
  • 最后,关于复制粘贴实现,这是一个主观问题。 我没有亲自复制和粘贴 initState/didUpdateWidget/dispose/build 中的逻辑,我每次都重新编写它,看起来基本没问题。 当它“失控”时,我会将其分解为像 StreamBuilder 这样的小部件。 所以我的意见在这里可能不相关。

作为一般规则,我是否遇到您所看到的问题并不重要。 不管我的意见如何,你的经验都是有效的。 我很高兴为您遇到的问题寻找解决方案,即使我没有感觉到这些问题。 正如我们在本次讨论中所看到的,我自己没有经历过这个问题的唯一影响是我很难很好地理解这个问题。

我希望你展示的是认为不可接受的编码模式,在一个足够重要的演示中,当有人创建一个解决方案时,你不会因为在不同的情况下它可能难以使用而驳回它(即包括所有相关情况,例如,确保包括“更新”部分或您认为重要处理的任何其他部分),或者说该解决方案在一种情况下有效但不适用于一般情况(例如采取您之前的反馈,确保参数来自多个地方并在多种情况下更新,这样很明显,像上面的 Property 这样的解决方案不会排除其他常见代码)。

问题是,您正在询问对我来说“显而易见”的领域的示例。

我不介意举一些例子,但我不知道你在期待什么,因为我不明白你不明白的东西。

我已经把我该说的都说了。
在不理解你不理解的东西的情况下,我唯一能做的就是重复我自己。

我可以运行这里的一些片段,但这相当于重复我自己。
如果代码片段没有用,我不明白为什么能够运行它会改变任何事情。

至于 mixin 和函数是否可能是 StreamBuilder 的替代方案,我认为 Hooks 很好地证明了你绝对可以使用函数来监听流,而且 mixin 可以清楚地做任何类在这里可以做的事情,所以我不明白为什么他们会也不是解决办法。

钩子不应被视为函数。
它们是一种类似于 Iterable/Stream 的新语言结构

函数不能做钩子做的事情——没有状态或导致小部件重建的能力。

OP 中演示了 mixin 的问题。 TL;DR:变量上的名称冲突,不可能多次重用相同的 mixin。

@rrousselGit好吧,既然你不介意举一些例子,而且由于我要求的例子很明显,让我们从这些明显的例子中的一些开始,然后从那里开始迭代。

我没有说这些例子很明显,但问题是。
我的意思是,我不能创造新的例子。 我要说的一切都已经在这个线程中了:

我想不出任何可以添加到这些示例中的内容。

但是 FWIW 我正在使用 Riverpod 开发一个开源天气应用程序。 完成后我会把它链接到这里。


我在 twitter 上做了一个民意调查,询问了一些关于 Builders 与这里讨论的问题相关的问题:

https://twitter.com/remi_rousselet/status/1295453683640078336

民意调查仍在进行中,但以下是目前的数字:

Screenshot 2020-08-18 at 07 01 44

200 人中有 86% 希望有一种不涉及嵌套的方式来编写构建器,这一事实不言而喻。

需要明确的是,我从来没有建议我们不应该解决这个问题。 如果我认为我们不应该解决它,该问题将被关闭。

我想我会尝试制作一个使用您链接到的片段的示例。

我可以帮助您根据链接的片段制作示例,但我需要知道为什么这些片段不够好
否则,我唯一能做的就是编译这些片段,但我怀疑这就是您想要的。

例如,这里有一个关于众多 ValueListenableBuilder+TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73的要点

例如,这里有一个关于众多 ValueListenableBuilder+TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73的要点

FWIW,这个特定示例可以更轻松地在 mobx 中实现。
它实际上比你的钩子实现更短。

Mobx 的 observable 是类固醇上的 ValueNotifiers,它的 Observer 小部件是 Flutter 的 ValueListenableBuilder 的演变——它可以监听多个 ValueNotifier。
作为 ValueNotifier/ValueListenableBuilder 组合的直接替代品,意味着您仍然编写惯用的 Flutter 代码,这实际上是一个重要因素。

由于它仍然使用 Flutter 内置的 Tween 构建器,因此无需学习/实现新的小部件/钩子(换句话说,它不需要新功能)并且它没有钩子的性能影响。

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 我们只重建一个 Element,而使用 Builders 重建 2-4 个 Builders。
所以 Hooks 可能会更快。

“总计”字段的行为在您的代码段中被破坏。 它与示例不符,其中两个计数器可以一起动画,但在不同的时间和不同的曲线完成动画。

显然甚至没有尝试运行该示例。 它的行为与您的代码完全一样。

至于您的最后一个要点,TickerProvider 已损坏,因为它不支持 TickerMode。

我不知道你说这个是什么意思。 我重构了 _your_ 示例,它不使用 TickerMode。 您再次更改了要求。

至于性能,使用 Hooks 我们只重建一个 Element,而使用 Builders 重建 2-4 个 Builders。 所以 Hooks 可能会更快。

不就是不。 您的钩子小部件不断轮询每个构建的更改。 基于 valuelistenables 的构建器是“反应性的”。

同样,我不确定这个例子给 ValueListenableBuilder 变量添加了什么

而 Mobx 可能是题外话。 我们不是在讨论如何实现环境状态/ValueListenable vs Stores vs Streams,而是如何处理本地状态/嵌套构建器——Mobx 没有以任何方式解决

你一定在开玩笑。 我拿了你的例子并“处理”了嵌套的 ValueListenableBuilders * 和tween builders?! 你特别提出的一个问题。
但是这里的这句话,描述了你对这次讨论的整体态度。 如果它不是钩子,那就是“题外”,但你说你不在乎它是否会被用作解决方案的钩子。
给我休息一下。

您显然甚至没有尝试运行该示例。 它的行为与您的代码完全一样。

它没有。 第一个计数器动画超过 5 秒,第二个计数器动画超过 2 秒——两者也使用不同的曲线。

使用我给出的两个片段,您可以同时增加两个计数器,并且在动画的每一帧中,“总数”都是正确的。 即使第二个计数器停止动画而第一个计数器仍在动画中

另一方面,您的实现不考虑这种情况,因为它将 2 个 TweenAnimationBuilders 合并为一个。
要修复它,我们必须编写:

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


不就是不。 您的钩子小部件不断轮询每个构建的更改。 基于 valuelistenables 的构建器是“反应性的”。

您忽略了Element所做的事情,这恰好与钩子所做的事情相同:比较 runtimeType 和键,并决定是创建新元素还是更新现有元素

我以你的例子和嵌套的 ValueListenableBuilders * 和 tween 构建器“处理”

假设总计数问题已解决,删除了哪些嵌套?

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 / TickerModevsync需要使用SingleTickerProviderClientStateMixin ,否则不支持静音逻辑,这会导致性能问题。
我在我的一篇文章中对此进行了解释: https :

使用这种方法,我们必须在最初需要 TweenAnimationBuilder 的每个位置重新实现 Tween 逻辑。 这会导致显着的重复,尤其是考虑到逻辑不是那么简单

使用我给出的两个片段,您可以同时增加两个计数器,并且在动画的每一帧中,“总数”都是正确的。 即使第二个计数器停止动画而第一个计数器仍在动画中

另一方面,您的实现不考虑这种情况,因为它将 2 个 TweenAnimationBuilders 合并为一个。

是的,这是我愿意做出的权衡。 我可以很容易地想象这样一种情况,动画只是对发生的变化的视觉反馈,而准确性并不重要。 这一切都取决于要求。

我怀疑你会反对,因此第二个版本解决了这个确切的问题,同时使代码更清晰。

如果您指的是您的要点,那么正如我之前提到的,这种方法会破坏 TickerProvider/TickerMode。 vsync 需要使用 SingleTickerProviderClientStateMixin 获取,否则不支持 muting 逻辑,这可能会导致性能问题。

因此,您在小部件中创建了 tickerprovider 并将其传递给 Counters。 我也没有处理动画控制器。 这些细节实现起来非常简单,我不觉得它们会在示例中添加任何内容。 但在这里我们对他们挑剔。

我实现了 Counter() 类来做你的例子所做的事情,仅此而已。

使用这种方法,我们必须在最初需要 TweenAnimationBuilder 的每个位置重新实现 Tween 逻辑。 这会导致显着的重复,尤其是考虑到逻辑不是那么简单

什么? 为什么? 请解释一下,为什么我不能创建多个 Counter 类的实例并在各种小部件中使用它们?

@Rudiksz我不确定您的解决方案是否真的解决了提出的问题。 你说

我实现了 Counter() 类来做你的例子所做的事情,仅此而已。

但是

是的,这是我愿意做出的权衡。 我可以很容易地想象这样一种情况,动画只是对发生的变化的视觉反馈,而准确性并不重要。 这一切都取决于要求。

因此,您在小部件中创建了 tickerprovider 并将其传递给 Counters。 我也没有处理动画控制器。 这些细节实现起来非常简单,我不觉得它们会在示例中添加任何内容。 但在这里我们对他们挑剔。

您提供的代码仅在表面上与@rrousselGit的钩子版本等效,但实际上并不等效,因为您省略了钩子版本包含的部分。 在那种情况下,它们并没有真正的可比性,对吧? 如果您想将您的解决方案与建议的解决方案进行比较,最好使其完全符合要求,而不是谈论为什么不包括它们。 这就是制作@TimWhiting存储库的原因。 如果您认为您已满足所有要求,则可以在此处提交您的解决方案。

您显然甚至没有尝试运行该示例。 它的行为与您的代码完全一样。

不就是不。

你一定在开玩笑。 我以你的例子和嵌套的 ValueListenableBuilders * 和 tween 构建器“处理”

请不要进行此类指责,它们只会使此线程变得尖刻,而不能解决根本问题。 你可以发表你的观点,而不是指责、贬低或激怒对方,这是我在这个帖子中看到的评论的效果,我没有看到其他人对你有这样的行为。 我也不确定 emoji-react 对你自己的帖子有什么影响。

@rrousselGit

我可以帮助您根据链接的片段制作示例,但我需要知道为什么这些片段不够好
否则,我唯一能做的就是编译这些片段,但我怀疑这就是您想要的。

我要求一个更复杂的应用程序而不仅仅是一个片段的原因是,当我发布一个处理你的一个片段的例子时,你说这还不够好,因为它也没有处理其他一些情况' 不在您的代码段中(例如,没有处理 didUpdateWidget,或者没有处理并排放置其中两个代码段,或者您希望处理的其他完全合理的事情)。 我希望有一个更精细的应用程序,我们可以得到足够精细的解决方案,一旦我们有了解决方案,就不会出现一些需要处理的新问题的“陷阱”时刻。 显然,我们仍然可能会错过一些需要处理的东西,但我们的想法是尽量减少机会。

关于最近不太受欢迎的帖子,请大家,让我们专注于在@TimWhiting的 repo 上创建示例,而不是用小示例来回争吵。 正如我们已经讨论过的,小例子永远不会详细到足以证明替代方案确实有效。

如果您认为您已满足所有要求,则可以在此处提交您的解决方案。

事实发生后,要求发生了变化。 我提供了两种解决方案。 一个做出妥协(一个非常合理的),一个实现所提供示例的确切行为

您提供的代码仅在表面上与@rrousselGit的钩子版本等效,但实际上并不等效,

我没有实现“hooks 解决方案”,我实现了 ValueListenableBuilder 示例,特别关注“嵌套问题”。 它不会做的每一件事情做的钩子,我只是展示了如何在委屈的符号列表一个项目可以使用替代解决方案可以简化。

如果允许您将外部包带入讨论中,那么我也可以。

可重用性:看看下面 repo 中的例子
https://github.com/Rudiksz/cbl_example

笔记:

  • 它旨在展示如何在小部件外部封装“逻辑”并拥有精益的小部件,几乎是 html 外观
  • 它并没有涵盖钩子涵盖的所有内容。 这不是重点。 我以为我们正在讨论 Flutter 框架的替代方案,而不是 hooks 包。
  • 它没有涵盖每个营销团队可以提出的_每个_用例
  • 但是,Counter 对象本身非常灵活,它可以独立使用(参见 AppBar 标题),作为复杂小部件的一部分,需要计算不同计数器的总数,使其具有反应性或响应用户输入。
  • 由消费小部件来自定义他们想要使用的计数器的数量、初始值、持续时间、动画类型。
  • 动画处理方式的细节可能有缺点。 如果 Flutter 团队说是的,在小部件之外使用动画控制器和补间,不知何故打破了框架,我会重新考虑。 肯定有需要改进的地方。 将我使用的自定义 tickerprovider 替换为由消费小部件的 mixin 创建的一个是微不足道的。 我没做过。
  • 这只是“Builder”模式的另一种替代解决方案。 如果您说您需要或想要使用 Builders,那么这些都不适用。
  • 尽管如此,事实是有一些方法可以简化代码,而无需额外的功能。 如果您不喜欢它,请不要购买。 我不提倡将其中的任何内容纳入框架。

编辑:此示例有一个错误,如果您在更改动画时启动“增量”,则计数器将重置并从当前值开始递增。 我没有故意修复它,因为我不知道您对这些“计数器”可能有的确切要求。 同样,更改增量/减量方法以解决此问题也很简单。

就这些。

@Hixie我是否应该将您的评论解释为我的示例(https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/hooks/animated_counter.dart)不够好?

另外,我们可以打个 Zoom/Google 电话会议吗?

我也不确定 emoji-react 对你自己的帖子有什么影响。

没有。 它与任何事情都完全无关。 你为什么提出来?

@rrousselGit只有你知道它是否足够好。 如果我们找到一种方法来重构该示例,使其干净、简短且没有重复的代码,您会满意吗? 或者,您认为我们应该支持那些我们需要处理以满足此错误的示例没有处理的事情?

只有你知道它是否足够好

我不能做这个判断。 首先,我不相信我们可以使用一组有限的应用程序来捕捉问题。

我不介意像你希望的那样工作,但我需要指导,因为我不明白这将如何让我们进步。

我认为我们提供了大量代码片段,从我们的角度显示了问题。 我真的不认为通过更多的代码示例会变得更加清晰,如果显示的那些没有这样做。

例如,如果看到多个难以阅读的嵌套构建器,或者 50 行有很多错误机会的纯样板文件,并没有足够强烈地证明问题,那就无处可去。

在这里提供mixin和functions是一个解决方案,这很奇怪,当整个ask是封装状态时,即是可重用的。 函数不能保持状态。 Mixin 没有被封装。 这个建议忽略了所提供的所有例子和基本原理的全部要点,仍然显示出对这个问题的深刻误解。

对我来说,我认为我们已经在地球上领先了 2 分,而且我认为两者都不能认真争论。

  1. 嵌套构建器本质上难以阅读
  2. 除了嵌套构建器之外,_没有办法_封装和共享具有小部件生命周期挂钩的状态。

正如多次所述,甚至在 Remi 的民意调查中,简单地说:我们想要 builder 的功能,而没有 builder 的冗长和嵌套闭包。 这不完全概括吗? 我完全不明白为什么这里需要进一步的代码示例。

Remi 的民意调查显示,大约 80% 的 Flutter 开发人员希望在可能的情况下避免在他们的代码中使用嵌套构建器的某种能力。 这确实不言自明。 当社区情绪如此清晰时,您无需在此线程中从我们这里获取它。

从我的角度来看,这些问题很清楚,当您查看专门用段落描述此处基本原理的竞争框架时,这些问题会变得更加清晰。 Vue、React、Flutter 它们都是堂兄弟,它们都是从 React 派生的,它们都面临着重用状态的问题,这必须与小部件生命周期相关联。 他们都详细描述了为什么他们实施了这样的事情。 一切都在那里。 这都是相关的。

@rrousselGit你能举一个有很多多个钩子的例子吗? 例如,我正在制作一个可能有几十个 AnimationControllers 的动画。 使用常规 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已经提供了足够的示例。

我不想谈论钩子的可组合性质如何使重构上面的代码更容易、可重用和更干净,但如果你愿意,我也可以发布重构的版本。

正如多次所述,甚至在 Remi 的民意调查中,简单地说:我们想要 builder 的功能,而没有 builder 的冗长和嵌套闭包。 这不完全概括吗? 我完全不明白为什么这里需要进一步的代码示例。

我从字面上提供了一些示例,说明如何使用 Remi 提供的示例减少冗长并避免构建器的嵌套。
我把他的代码粘贴到我的应用程序中,运行它并重新编写它。 就功能性而言,最终结果几乎相同——我可以从运行代码中收集到尽可能多的信息,因为代码没有附带要求。 当然,我们可以讨论边缘情况和潜在问题,但它被称为离题。

对于简单的用例,我使用 Builders,对于复杂的用例,我不使用 Builders。 这里的论点是,如果不使用构建器,就没有简单的方法来编写简洁、可重用的代码。 隐含地,这也意味着 Builders 是必备的,也是开发 Flutter 应用程序的唯一方法。 这显然是错误的。

我刚刚展示了一个有效的概念验证代码来演示它。 它没有使用构建器或钩子,也没有涵盖这个特定 github 问题似乎想要解决的“无限问题集”的 100%。 它被称为题外话。
旁注,它也非常有效,即使没有任何基准,我猜甚至可以击败 Builder 小部件。 如果证明是错误的,我很高兴改变主意,如果我发现 Mobx 成为性能瓶颈,我会立即放弃 Mobx 并切换到普通构建器。

Hixie 在 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 和 dispose 已经实例化,我只是不确定它是如何在钩子中工作的。

@satvikpendem考虑像类上的属性一样的钩子。 您是在循环中定义它们还是手动将它们一一命名?

在你的例子中这样定义

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是如何与你的状态逻辑的命名部分相关的吗? (比如当应用处于加载状态时,isLoading 的 useState 连接到)

在您的第二个代码段中,您在循环中调用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");

我明白了,所以对于钩子,看起来我们需要创建一个单独的钩子来处理具有一系列项目的情况,例如多个 AnimationControllers。

这是我最初拥有的似乎不起作用的内容:

  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

我认为我的声明不适用于AnimationControlleruseAnimationController

因为虽然您可能有多个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]);

  • 如果AnimationController是动态的,您也可以使用useState

(当股票代码更改时,它也会重新同步)

@rrousselGit你能举一个有很多多个钩子的例子吗? 例如,我正在制作一个可能有几十个 AnimationControllers 的动画。 使用常规 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 。 我想这是一个微不足道的例子,但我在任何地方都找不到这种用例的解决方案。

Hooks 的做法不同。

我们不再创建控制器列表,而是将控制器逻辑向下移动到项目:

Widget build(context) {
  return ListView(
    children: [
      for (var i = 0; i < 50; i++)
        HookBuilder(
          builder: (context) {
            final controller = useAnimationController();
          },
        ),
    ],
  );
}

我们仍然制作了 50 个动画控制器,但它们归不同的小部件所有。

也许你可以分享一个你为什么需要它的例子,我们可以尝试转换为钩子并将其添加到蒂姆的回购中?

Hixie 在 Google 工作,他必须对你有耐心和礼貌,不能因为你缺乏参与而责备你。 他能做的最好的事情就是推动更多的例子。

@Hixie ,如果您有这种感觉,请说出来(在这里或私下与我联系)。

我从字面上提供了一些示例,说明如何使用 Remi 提供的示例减少冗长并避免构建器的嵌套。

谢谢,但我不清楚你如何从这段代码中提取一个共同的模式,将这个逻辑应用于不同的用例。

在 OP 中,我提到目前,我们有 3 个选择:

  • 使用构建器并嵌套代码
  • 不要对代码进行任何因式分解,这不会扩展到更复杂的状态逻辑(我认为StreamBuilder和它的AsyncSnapshot是一个复杂的状态逻辑)。
  • 尝试使用 mixins/oop/... 来构建一些架构,但最终得到的解决方案太针对问题了,任何与 _tiny_ 不同的用例都需要重写。

在我看来,您使用了第三种选择(与PropertyaddDispose提案的早期迭代属于同一类别)。

我之前做了一个评估网格来判断模式:

你能在这个上运行你的变体吗? 特别是关于实现StreamBuilder所有功能的第二条评论,如果多次使用,代码不会重复。

我目前对此错误的计划是:

  1. https://github.com/flutter/flutter/issues/51752#issuecomment -675285066 中的示例为例,并使用纯 Flutter 创建一个应用程序,将这些不同的用例一起展示。
  2. 尝试设计一个解决方案,为那些满足此处讨论的各种关键要求并符合我们设计原则的示例启用代码重用。

如果有人想帮助其中任何一个,我绝对很乐意得到帮助。 我不太可能很快做到这一点,因为我首先致力于 NNBD 过渡。

@rrousselGit当然,我正在制作一个应用程序,其中许多小部件可以在屏幕上移动(让我们称它们Box es),并且它们应该能够相互独立移动(因此至少需要有每个Box一个 AnimationController )。 这是我制作的一个版本,仅使用一个 AnimationController 在多个小部件之间共享,但将来我可能会独立为每个小部件设置动画,例如执行复杂的转换,例如实现CupertinoPicker ,并使用其自定义滚轮效果.

当您单击 FloatingActionButton 时,Stack 中有三个框会上下移动。

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 和父小部件不在同一个类中,我将如何为每个Box在 AnimationController 中传递的第一种情况制作 AnimationControllers 列表? 根据您上面使用 HookBuilder 的回答,这似乎不需要,但是如果我按照您所说的将状态向下移动到子 Widget,并选择通过useAnimationController使每个Box拥有自己的 AnimationController ,我遇到了另一个问题:我如何将创建的 AnimationController 公开给父类,以便它为每个子类协调和运行独立的动画?

在 Vue 中,您可以通过emit模式将事件发送回父级,因此在 Flutter 中,我是否需要一些更高的状态管理解决方案,例如 Riverpod 或 Rx,其中父级更新全局状态,子级监听全局状态? 似乎我不应该,至少对于这样的简单示例。 谢谢你解开我的困惑。

@satvikpendem对不起,我不清楚。 你能展示在没有钩子的情况下你会如何做,而不是你用钩子阻塞的问题吗?

我想清楚地了解您正在尝试做什么,而不是您陷入困境的地方

但作为一个快速的猜测,我认为您正在寻找间隔曲线,并且有一个动画控制器。

@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, initStatedispose都可以有循环,但在使用钩子时则不然,所以我只是想知道如何解决这个问题。 同样,我不想将Box类放在父小部件中,因为我不想将它们都紧密封装起来; 例如,我应该能够保持父逻辑不变,但将Box换成Box2

谢谢!
我已将您的示例推送到@TimWhiting的存储库,并带有等效的钩子

TL;DR,使用钩子(或构建器),我们以声明方式而不是命令方式进行思考。 因此,与其在一个小部件上拥有一系列控制器,然后强制地驱动它们——将控制器移动到项目并实现隐式动画。

谢谢@rrousselGit! 在开始使用钩子后,我在这种类型的实现上挣扎了一段时间,但我现在明白它是如何工作的。 我刚刚为每个动画控制器具有不同目标的版本打开了一个 PR,因为这可能更有助于理解为什么钩子是有用的,正如我上面所说的:

我们可以想象这样一种场景,我给它们每一个都不同的曲线,而不是它们每个都具有相同的曲线,或者我制作 100 个盒子,每个盒子的持续时间比前一个长或短,或者我让偶数上升奇数下降,依此类推。

我一直在尝试制作声明性版本,但我想我不明白的是didUpdateWidget/Hook生命周期方法,所以我不知道当子道具从父道具更改时如何驱动动画,但你的代码清除了它。

今天在我的代码库中遇到了一个真实的例子,所以我想我不妨分享一下。

因此,在这种情况下,我正在使用 Firestore,并且我想对每个 StreamBuilder 执行一些样板,因此我制作了自己的自定义构建器。 我还需要使用 ValueListenable这允许用户重新排序列表。 出于与 Firestore 相关的货币成本原因,这需要一个非常具体的实现(每个项目不能存储自己的订单,而是列表必须将其保存为串联 id 的字段),这是因为 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 只有一种连接方法,即 Streams,并且任何时候我想要这种类似套接字的行为,我别无选择,只能使用流。 这就是生活。

这确实失去了关于粒度重建的优化,但这不会产生任何不同的 IRL,因为所有视觉元素都位于最底部的叶节点,所有包装器都是纯状态。

它仍然有所作为。 节点是否可视并不影响重建是否需要花费一些东西。

我可能会将该示例分解为不同的小部件(IDE 具有一键式重构工具以使其变得非常简单)。 _buildItemList可能应该是一个小部件,根应该是FamilyStreamBuilder

我们并没有真正失去粒度重建。
事实上,钩子通过允许使用useMemoized轻松缓存小部件实例来改进这一方面。

Tim 的 repo 上有一些例子可以做到这一点。

我可能会将该示例分解为不同的小部件(IDE 具有一键式重构工具以使其变得非常简单)。 _buildItemList可能应该是一个小部件,根应该是FamilyStreamBuilder

事情是,我真的不想这样做,因为在这个视图中我根本没有性能问题。 因此,在 100% 的时间里,我会更喜欢代码局部性和一致性,而不是像这样的微优化。 此视图仅在用户启动操作(~平均每 10 秒一次)或后端数据更改并且他们盯着打开的列表时(几乎从未发生)重建。 它也恰好是一个简单的视图,主要是一个列表,并且该列表在内部进行了大量自己的优化。 我意识到 build() 技术上可以随时触发,但实际上任何随机重建都非常罕见。

imo 如果所有这些逻辑都集中在一个小部件中,那么处理和调试这个视图会容易得多,主要是为了让我将来回到它时的生活更轻松:)

另一件要注意的事情是嵌套基本上“迫使我退出”构建方法,因为我无法开始在孔中的 3 个闭包和 16 个空间内构建我的树。

是的,您可以说然后移动到单独的小部件是有意义的。 但是为什么不只停留在构建方法中呢? 如果我们可以将样板文件减少到它真正_需要_的样子,那么就不需要将内容拆分为 2 个文件的可读性和维护麻烦。 (假设性能不是问题,但通常不是)

请记住,在这个场景中,我已经创建了一个自定义小部件来处理我的 Stream Builder。 现在我需要再做一个来处理这些构建器的组合?? 似乎有点过头了。

因为在这个视图中我完全没有性能问题

哦,我不会为了性能将它重构为小部件,构建器应该已经解决了。 我会重构它以提高可读性和可重用性。 我并不是说这是做到这一点的“正确”方式,只是说我将如何构建代码。 无论如何,这既不在这里也不在那里。

我无法开始在洞中的 3 个封闭和 16 个空间内构建我的树

我的显示器可能比你宽... :-/

那么就不需要将东西拆分成 2 个文件的可读性和维护麻烦

我会将这些小部件放在同一个文件中,FWIW。

无论如何,这是一个很好的例子,我相信你宁愿使用一个具有不同语法的小部件而不是使用更多的小部件。

我的显示器可能比你宽... :-/

我有一个超宽的 :D,但 dartfmt 显然将我们限制在 80。所以失去 16 意义重大。 主要问题是我声明的结尾是},);});},),),);当事情变得一团糟时并不是很有趣。 每当我编辑这个层次结构时,我都必须非常小心,像swap with parent这样的常见 IDE 助手会停止工作。

我会将这些小部件放在同一个文件中,FWIW。

100%,但我仍然发现在单个文件中垂直跳跃更难维护。 当然不可避免,但我们会尽可能减少并“保持一致”。

但至关重要的是,即使我确实将主列表重构为它自己的小部件(我同意,这比嵌套构建方法更具可读性),如果没有嵌套在父小部件中,它仍然更具可读性。 我可以进来,一目了然地理解所有逻辑,清楚地看到 _MyListView() 小部件,然后直接跳进去,确信我了解周围的上下文。 我还可以相对轻松地添加/删除其他依赖项,因此它可以很好地扩展。

dartfmt 显然将我们所有人限制在 80

我的意思是,这是我通常不使用 dartfmt 的原因之一,当我使用时,我将其设置为 120 或 180 个字符......

你在这里的经历是完全有效的。

实际上我也是,一整天都是 120 :) 但是 pub.dev 会主动降低未格式化为 80 的插件的费率,我的印象是,当我们更改此值时,我(我们)是少数。

好吧,这太荒谬了,我们应该解决这个问题。

pub.dev 不会降低不尊重 dartfmt 的插件。 它只在分数页面显示评论,但分数不受影响
但可以说,dartfmt 的问题不仅仅是行长。

太大的行长会导致在多行中更易读的内容在一行中,例如:

object
  ..method()
  ..method2();

这可能会变成:

object..method()..method2();

我看这个?
image
有问题的包: https :

有趣 - 以前肯定不是那样的,因为提供程序有一段时间没有使用 dartfmt。
我站着纠正。

是的,这绝对是新行为,当我去年春天最初发布时,我确保我勾选了所有框,并且不需要 dartfmt。

在所有这些讨论之后,我希望我们看到对颤振中类似钩子的解决方案的原生支持。 useHookuse Hook或任何 flutter 团队认为他们的功能不像 React 的东西😁🤷‍♂️

我们以final controller = useAnimationController(duration: Duration(milliseconds: 800));的方式使用钩子
使用从 kotlin/swift 复制的 Darts 新程序功能 _Extension_ 到漂亮的语法不是更好吗?

类似: final controller = AnimationController.use(duration: Duration(milliseconds: 800));
使用这种方法,当 flutter/dart 团队决定添加use Hook而不是当前可用的语法useHook ,我认为该扩展函数的Annotation使其读取用作
final controller = use AnimationController(duration: Duration(milliseconds: 800));

constnew一样使用use关键字也是可以理解/有意义的:
new Something
const Something
use Something

作为该建议的奖励,我认为最后甚至构造函数/生成器函数也可以使用/受益于提议的Annotation 。 然后带有一些自定义的 dart 编译器将其转换为支持use关键字。

如此美丽和颤振/飞镖特定功能😉

我是否正确假设https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful中的示例现在代表了人们想要解决的问题?

我不确定其他人的感受,但我认为问题在那里有所体现(这意味着我无法确定,因为有人可能会指出一些没有体现的内容)。

我在该存储库中尝试了中间解决方案。 它像钩子一样可组合,但不依赖于函数调用的顺序或不允许循环等。它直接使用 StatefulWidgets。 它涉及一个 mixin,以及由键唯一标识的有状态属性。 我并不是要将此作为最终解决方案来推广,而是将其作为两种方法之间的中间地带。

我将其称为生命周期混合方法,它与此处讨论的 LateProperty 方法非常接近,但主要区别在于它实现了更多生命周期,并且可以轻松组合。 (在生命周期部分,除了 initState 和 dispose 之外,我没有使用过小部件生命周期,所以我可能在那里完全搞砸了)。

我喜欢这种方法,因为:

  1. 它的运行时间损失很小。
  2. 在构建路径中没有创建或管理状态的逻辑/函数(构建可以是纯的 - 仅获取状态)。
  3. 通过构建器优化重建时,生命周期管理更加清晰。 (但您不会牺牲少量状态的可重用性和可组合性)。
  4. 由于您可以重用状态位的创建,因此库可以由应该以特定方式创建和处理的公共状态位组成,因此您自己的代码中的样板文件更少。

我不喜欢这种方法(与钩子相比),原因如下:

  1. 我不知道它是否涵盖了钩子可以做的所有事情。
  2. 必须使用密钥来唯一标识属性。 (因此,在组合构建某些状态的逻辑片段时,您必须附加到键以唯一标识状态的每个部分 - 使键成为必需的位置参数会有所帮助,但我喜欢语言级别的解决方案来访问变量的唯一 ID)。
  3. 它大量使用扩展来创建可重用的函数来创建常见的状态位。 并且扩展不能由 IDE 自动导入。
  4. 如果您混合不同小部件的生命周期/在小部件之间访问它们而没有明确地正确管理它们,您可能会搞砸自己。
  5. builder 语法有点奇怪,所以创建的状态在构建函数的范围内,但让构建函数保持纯净。
  6. 我尚未实现所有示例,因此可能存在我无法涵盖的用例。

简单的反例。
动画计数器示例

框架
可重用状态组合逻辑的常见位

我不确定我有多少时间,研究生学习总是让我很忙,但我希望得到一些反馈。 @rrousselGit这与钩子有多接近,你能看到可重用性或可组合性方面的一些明显漏洞吗?

我不是要宣传我的解决方案,而是鼓励在中间立场上进行积极的讨论。 如果我们就缺少什么或这个解决方案给我们带来了什么达成一致,我认为我们将取得良好的进展。

@TimWhiting我对这种方法的主要问题是缺乏稳健性。 这里的一个重要驱动因素是以简洁的形式需要建设者的可靠性。 神奇的 id 和生命周期冲突的能力,都为错误的发生创造了新的载体,我会继续向我的团队推荐他们使用构建器,尽管阅读起来很讨厌,但至少我们知道它们是 100 % 无错误。

关于示例,我仍然认为完美的示例是简单地使用 AnimationController,并将持续时间值绑定到小部件。 保持简单和熟悉。 没有必要比这更深奥了,它是可重用样板的完美小用例,它需要生命周期挂钩,并且所有解决方案都可以通过它们简洁地使用多个动画的能力轻松判断。

其他一切都只是相同使用“有状态控制器”用例的变体。 我想在 initState 中执行 X,在处置状态中执行 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我同意,键和构建器语法是

我是否正确假设https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful中的示例现在代表了人们想要解决的问题?

老实说,我不确定。
我会说_是_。 但这实际上取决于您将如何解释这些示例。

在这个帖子中,我们有相互不了解的历史,所以我不能保证这种情况不会再发生。

这就是为什么我不喜欢使用代码示例并建议提取一组规则的部分原因。
示例是主观的,有多种解决方案,其中一些可能无法解决更广泛的问题。

我想知道@rrousselGit是否可以在这里提供一些见解,或者是否有关于当前最常使用的钩子的任何数据。 我猜它是 80% 的流和动画,但实际上知道人们最常使用什么会很好。

我认为这是非常同质的。

尽管如果有的话, useStream和 Animations 可能是最少使用的:

  • useStream 通常具有更好的等效项,具体取决于您的架构。 可以使用context.watch , useBloc , useProvider , ...
  • 很少有人花时间制作动画。 这很少是优先事项,而TweenAnimationBuilder其他隐式动画小部件涵盖了大部分需求。
    如果我在 flutter_hooks 中添加我的useImplicitlyAnimatedInt钩子,可能会改变。

@esDotDev刚刚删除了生命周期混合方法中对键/ID 的需求。 它在构建器语法中仍然有点尴尬。 但这可能最终也会有所帮助。 我遇到的一个问题是类型系统。 它试图以某些不起作用的方式投射事物。 但它可能只需要一些仔细的转换或类型系统掌握。 至于混合生命周期,我认为可以通过在该小部件的生命周期无法访问您尝试访问的特定状态时抛出一些合理的异常来改进。 或者在生命周期构建器中的 lint,您应该只访问构建器的生命周期。

谢谢 Remi,这让我感到惊讶,我认为人们会非常频繁地使用 Animation 来驱动核心中的大量 Transition 小部件,但我想大多数人只是使用各种 Implicit,因为它们很好读并且没有嵌套.

尽管 AnimatorController 可以很好地与一组隐式和显式小部件一起使用,但我仍然认为它是“需要维护状态,并与小部件参数和生命周期相关联”的一个很好的例子。 并且作为要解决的问题的一个完美的小例子(事实是在 Flutter 中完全解决了,尽管有十几个小部件),我们都可以讨论并专注于架构而不是内容。

例如,考虑一下,如果var anim = AnimationController.use(context, duration: widget.duration ?? _duration);是一等公民,那么实际上这些隐式或显式动画都不需要存在。 它使它们变得多余,因为它们都是为了管理核心问题而创建的:在小部件的上下文中轻松组合有状态的事物 (AnimationController)。 TAB 变得非常接近毫无意义,因为你可以用AnimatedBuilder + AnimatorController.use()做同样的事情。

如果您查看围绕动画出现的大量小部件,它确实说明了对一般用例的需求。 正是因为重用核心设置/拆卸逻辑非常麻烦/容易出错,我们有 15 个以上的小部件都处理非常具体的事情,但它们中的大多数都在重复相同的动画样板,只有少数在许多情况下是独特的代码行。

它表明,是的,我们也可以做这件事来重用我们自己的有状态逻辑:为每一个使用排列制作一个小部件。 但多么麻烦和维护头痛! 有一个简单的方法来组合小的有状态对象,使用生命周期钩子就更好了,如果我们想要制作专用的渲染小部件,或者一个可重用的构建器,我们可以轻松地将它们放在上面。

对于它的价值,我在我的应用程序中大量使用useAnimation类的东西,而不是普通的动画小部件。 这是因为我使用的是 SpringAnimation,例如 AnimatedContainer 之类的小部件不能很好地支持它; 它们都假设一个基于时间的动画,使用curveduration而不是基于模拟的动画,后者将接受Simulation参数。

我对useAnimation进行了抽象,但使用弹簧,所以我将其称为useSpringAnimation 。 我使用这个钩子的包装小部件类似于AnimatedContainer但它更容易制作,因为我可以像你所说的@esDotDev一样重用所有动画代码,因为大部分逻辑是相同的。 我什至可以再次使用useSpringAnimation制作我自己的所有动画小部件版本,但我的项目不一定需要这样做。 这再次展示了钩子提供的生命周期逻辑重用的强大功能。

例如,考虑如何, if var anim = AnimationController.use(context, duration: widget.duration ?? _duration); 作为一等公民,实际上这些隐式或显式动画都不需要存在。 它使它们变得多余,因为它们都是为了管理核心问题而创建的:在小部件的上下文中轻松组合有状态的事物 (AnimationController)。 TAB 变得几乎毫无意义,因为你可以用 AnimatedBuilder + AnimatorController.use() 做同样的事情。

阅读我上面的评论,这似乎基本上就是我对 spring 动画钩子所做的。 我封装了逻辑,然后简单地使用了 AnimatedBuilder。 为了使它们隐含,以便当我像在 AnimatedContainer 上一样更改道具时,它会进行动画处理,我只是添加了didUpdateWidget (在flutter_hooks称为didUpdateHook flutter_hooks )方法到将动画从旧值运行到新值。

我是否正确假设https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful中的示例现在代表了人们想要解决的问题?

老实说,我不确定。
我会说_是_。 但这实际上取决于您将如何解释这些示例。

在这个帖子中,我们有相互不了解的历史,所以我不能保证这种情况不会再发生。

这就是为什么我不喜欢使用代码示例并建议提取一组规则的部分原因。
示例是主观的,有多种解决方案,其中一些可能无法解决更广泛的问题。

我还想说,我们应该在这个问题中包含所有讨论过的代码示例,我认为@rrousselGit在上面的某个地方有一个列表。 我可以创建一个 PR,将它们添加到 local_state 存储库中,但它们并非都是完整的代码示例,因此它们实际上可能无法全部编译和运行。 但它们至少显示了潜在的问题。

我可以做一个 PR 将它们添加到 local_state 存储库中

那将非常有用。

我想指出这个线程没有定义重用或重用是什么样子。 我认为我们应该痛苦地具体定义,以免谈话失去重点。

我们只展示了与 Flutter 相关的重用_不是_。

已经有很多使用示例,钩子清楚地提供了一个小部件状态重用的完整示例。 我不确定混乱来自哪里,因为它表面上看起来很简单。

重用可以简单地定义为:_构建器小部件可以做的任何事情。_

请求是一些可以存在于任何小部件中的有状态对象,即:

  • 封装它自己的状态
  • 可以根据 initState/dispose 调用自行设置/拆卸
  • 可以在小部件中的依赖项更改时做出反应

并且以一种简洁、易于准备、无样板的方式这样做,例如:
AnimationController anim = AnimationController.stateful(duration: widget.duration);
如果这适用于无状态和有状态小部件。 如果它在 widget.something 改变时重建,如果它可以运行它自己的 init() 和 dispose(),那么你基本上就有一个赢家,我相信每个人都会欣赏它。

我正在努力解决的主要问题是如何以有效的方式做到这一点。 例如, ValueListenableBuilder 采用可用于显着提高性能的子参数。 我没有看到用 Property 方法来做到这一点的方法。

我很确定这不是问题。 我们将按照XTransition小部件现在的工作方式来执行此操作。 如果我有一些复杂的状态,并且我希望它有一些昂贵的孩子,我会为它制作一个小的包装小部件。 就像我们可能做的那样:
FadeTransition(opacity: anim, child: someChild)

通过将“事物”传递到 Widget 以重新渲染它,我们可以轻松地对任何我们想要渲染的事物执行此操作。
MyThingRenderer(value: thing, child: someChild)

  • 这不像构建器那样_需要_嵌套,但它可以选择支持它(.child 可以是构建 fxn)
  • 它保留了无需包装小部件即可直接使用的能力
  • 我们总是可以创建一个构建器并在构建器中使用这个语法来保持它的简洁。 它还为围绕同一个核心对象构建的多种类型的构建器打开了大门,无需到处复制粘贴代码。

同意@esDotDev。 正如我之前提到的,另一个标题是“构建器的语法糖”。

我正在努力解决的主要问题是如何以有效的方式做到这一点。 例如, ValueListenableBuilder 采用可用于显着提高性能的子参数。 我没有看到用 Property 方法来做到这一点的方法。

我很确定这不是问题。 我们将按照XTransition小部件现在的工作方式来执行此操作。 如果我有一些复杂的状态,并且我希望它有一些昂贵的孩子,我会为它制作一个小的包装小部件。 就像我们可能做的那样:

没有必要这样做。
这个特性的一个好处是,我们可以有一个状态逻辑,“如果它的参数没有改变,就缓存小部件实例”。

使用钩子,在 React 中就是useMemo

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

使用此代码, myWidget将_only_在value更改时重建。 即使调用useMemo的小部件由于其他原因而重建。

这类似于小部件的 const 构造函数,但允许动态参数。

在 Tim 的 repo 中有一个例子。

请求是一些可以存在于任何小部件中的有状态对象,即:

  • 封装它自己的状态
  • 可以根据 initState/dispose 调用自行设置/拆卸
  • 可以在小部件中的依赖项更改时做出反应

我想我很难看出为什么通过这些参数, StatefulWidget并没有比它做得更好。 这就是为什么我问了一个问题,即我们在解决方案中真正追求的是什么。 作为使用flutter_hooks我发现使用它们比StatefulWidget更有趣,但这只是为了避免冗长——不是因为我认为是钩子。 实际上,与Widget相比,我发现使用钩子很难推理 UI 更新。

  • 可以在小部件中的依赖项更改时做出反应

您的意思是在小部件内部创建/获取的依赖项? 还是远低于树中小部件的依赖项?

我并不否认 Flutter 中存在一个导致冗长/混乱的问题,我只是犹豫要不要依赖每个人实际上都对“重用”具有相同的心理模型。 非常感谢您的解释; 当人们有不同的模型时,他们会创建不同的解决方案。

因为使用 SW 来执行此操作对于特定用例很好,但不适用于跨多个 SW 抽象用例的可重用逻辑。 以动画的设置/拆卸为例。 这不是软件本身,而是我们想要在它们之间使用的东西。 如果没有对共享封装状态的一流支持,您最终必须创建一个构建器,即 TweenAnimationBuilder,或者创建大量特定的小部件,即 AnimatedContainer 等。如果您可以将这些逻辑捆绑起来并重新使用,那真的会更优雅它以任何你想要的方式在树内。

就 Widget 依赖而言,我的意思是如果widget.foo发生变化,则有状态的事物有机会进行它需要进行的任何更新。 在stateful AnimationController 的情况下,它会检查持续时间是否改变,如果改变,更新它的内部 AnimatorController 实例。 这使动画的每个实现者不必处理属性更改。

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

使用此代码, myWidget将_only_在value更改时重建。 即使调用useMemo的小部件由于其他原因而重建。

啊我明白了,Memoized 本身返回一个 Widget,然后你传入 [value] 作为重建触发器,整洁!

AnimatedOpacity 的关键既不是父级重建,也不是子级重建。 事实上,当您使用 AnimatedOpacity 触发动画时,在您触发动画的第一帧之后实际上没有任何重建。 我们完全跳过构建阶段并在渲染对象中完成所有工作(在渲染树中,它只是重新绘制,而不是重新布局,实际上它使用了一个图层,因此即使绘制也非常少)。 它对性能和电池使用量产生重大影响。 如果我们要将其构建到核心框架中,那么我们在这里提出的任何解决方案都需要能够保持这种性能。

不幸的是,我没有时间将这个问题中的示例整理到本地状态存储库中,我的错。 我可能无法在短期内获得它,所以如果其他人想要拿起它,我会很好。

关于在 build/render 方法中定义钩子的性能(我认为本期前面有人提到过),我正在阅读 React 文档并看到这个 FAQ,可能会有用。 基本上它会询问钩子是否因为在每次渲染中创建函数而变慢,他们说不,原因有几个,其中之一是能够使用像useMemouseCallback这样的钩子来记忆函数

https://reactjs.org/docs/hooks-faq.html#are -hooks-slow-because-of-creating-functions-in-render

基本上它会询问钩子是否因为在每次渲染中创建函数而变慢,他们说不,原因有几个,其中之一是能够使用像useMemouseCallback这样的钩子来记忆函数

担心的不是创建闭包的成本,它们确实相对便宜。 完全运行任何代码和根本不运行任何代码之间的区别是 Flutter 在当今最佳情况下表现出的性能的关键。 我们花费了大量精力来制定算法来完全避免运行某些代码路径(例如,对于 AnimatedOpacity 完全跳过构建阶段,或者我们避免遍历树以执行更新而是仅针对受影响的节点的方式)。

我同意。 我不太了解 Flutter 的内部结构,也不太了解钩子的内部结构,但你是对的,钩子需要(如果他们还没有)弄清楚它们应该何时运行,何时不运行,并且性能不能倒退。

完全运行任何代码和根本不运行任何代码之间的区别是 Flutter 在当今最佳情况下表现出的性能的关键

正如前面几次提到的,钩子改进了这一点。
Tim 的 repo 上的动画示例证明了这一点。 由于useMemo ,hooks 变体的重建频率低于 StatefulWidget 变体

由于在此线程的某个地方正在讨论此问题的解决方案,因此我也将其标记为提案。

我真的很想看到钩子和 react 一样被加入到颤振中。 我以与第一次使用 react 时相同的方式查看 flutter 中的 state。 由于使用钩子,我个人永远不会回去。

IMO 的可读性要好得多。 目前,您必须声明两个带有状态小部件的类,而不是您只需放入 usestate 的钩子。

它还可以让 React 开发人员在查看 Flutter 代码时通常不熟悉 Flutter。 显然,将 Flutter 与 React 进行比较是一条危险的道路,但我真的认为我的开发人员使用 hooks 的体验比没有它们时的体验要好。

顺便说一句,我并不讨厌 flutter,它实际上是我最喜欢的框架,但我认为这是提高可读性和开发体验的好机会。

我认为绝对有机会改进命名约定并使它们更像颤振。

UseMemoized 和 UseEffect 之类的东西听起来很陌生,听起来我们想要某种方式不必在构建 fxn 中运行 init() 代码。

目前用钩子初始化是这样的(我认为?):

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 ? 我不是语言设计师,所以我的话毫无价值。

我确实觉得 Flutter 中类似钩子的功能将极大地帮助 React 开发人员平滑学习曲线,当公司在 ReactNative 和 Flutter 之间做出决定时,这确实是目标受众。

@lemusthelroy 相呼应,Flutter 是迄今为止我最喜欢的框架,我很高兴看到它的发展方向。 但是我觉得函数式编程的概念可以极大地帮助框架朝着一个相对未开发的方向发展。 我认为有些人正在摒弃这个想法,目的是与 React 保持距离,这很不幸,但可以理解。

是的,我认为这枚硬币有两个方面。 一个新的关键字是一个大事件,所以知识传播会非常迅速,但另一方面肯定是它现在对_每个人_来说都是新的东西。 如果没有这可能也很酷! 只是不确定它是……至少没有那么优雅。

意见:社区倾向于将钩子命名为该问题的事实上的解决方案,其根源在于对函数的偏见。 函数比对象更容易组合,尤其是在静态类型语言中。 我认为对于许多开发人员来说,Widgets 的心智模型实际上就是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是一种类型系统级别的 hack,它有助于静态分析所产生的钩子是根据熟悉钩子的开发人员所知的钩子法则调用的。 作为那种东西,钩子类型可以被记录为很像一个函数的东西,但具有静态的内部可变状态。

我在写这篇文章时有点畏缩,因为从语言的角度来看,这太奇怪了。 再说一次,Dart 是为编写用户界面而生的语言。 如果这种怪事应该存在于任何地方,也许就是这个地方。 只是不是特别奇怪。

意见:社区倾向于将钩子命名为该问题的事实上的解决方案,其根源在于对函数的偏见。 函数比对象更容易组合,尤其是在静态类型语言中。 我认为对于许多开发人员来说,Widgets 的心智模型实际上就是构建方法。

我不确定你想说些什么。 我也与 get_it_mixin 一起使用的钩子方法只是使小部件树比使用 Builder 更易于阅读。

关于React 钩子的有趣文章

@nt4f04uNd您的所有观点之前都已解决,包括性能、为什么它需要成为核心功能、功能与类样式小部件,以及为什么钩子以外的东西似乎不起作用。 我建议您通读整个对话以了解各个要点。

我建议您通读整个对话以了解各个要点。

考虑到他们没有阅读整个线程,这可以说是公平的,但我不确定阅读线程的其余部分会让事情变得更清楚。 有些人的首要任务是保持 Widget 的原样,还有一些人想要完全做其他事情或使 Widget 更加模块化。

虽然这可能是真的,但这个问题表明存在一些小部件目前无法解决的问题,所以如果我们想解决这些问题,我们别无选择,只能做出新的东西。 这与具有Future和后来引入async/await语法的概念相同,后者使没有新语法根本不可能实现的事情成为可能。

然而,人们_are_ 建议我们将其作为框架的一部分。 React 不能向 Javascript 添加新语法,因为它不是唯一可用的框架(好吧,它可以通过 Babel 转换),但 Dart 是专门设计用于 Flutter(至少是 Dart 2,不是原始版本)所以我们有一个使钩子与底层语言一起工作的更多能力。 例如,React 需要用于 JSX 的 Babel,并且它必须使用 linter 来处理useEffect错误,而我们可以将其设置为编译时错误。 拥有一个包会让采用变得更加困难,因为你可以想象如果它是第三方包,React hooks 会(不)获得的吸引力。

如果除了目前的 Stateless 和 Stateful 小部件之外,还有第三种类型的小部件,即 HookWidget,那就没有问题了。 让社区决定使用哪一个。 Remi 已经有一个包,但它不可避免地有局限性。 我试过了,它大大减少了样板文件,但由于限制,我不得不不幸地放弃了它。 我必须为仅使用 init 方法创建有状态小部件。 如果它是具有语言支持的核心框架的一部分,则可能会有额外的巨大好处。 此外,HookWidget 可以使社区创建更优化和更高性能的应用程序。

我必须为仅使用 init 方法创建有状态小部件。

您实际上不必这样做,useEffect() 能够在构建中执行 initCall。 文档根本没有解释这一点,并且基本上假设您是一个已经知道钩子如何工作的 React 开发人员。

我正在使用这种方式,但我在包的限制方面遇到了其他一些问题,我不记得到底是什么问题。

此页面是否有帮助?
0 / 5 - 0 等级