Flutter: Reusing state logic is either too verbose or too difficult

Created on 2 Mar 2020  ·  420Comments  ·  Source: flutter/flutter

.

Related to the discussion around hooks #25280

TL;DR: It is difficult to reuse State logic. We either end up with a complex and deeply nested build method or have to copy-paste the logic across multiple widgets.

It is neither possible to reuse such logic through mixins nor functions.

Problem

Reusing a State logic across multiple StatefulWidget is very difficult, as soon as that logic relies on multiple life-cycles.

A typical example would be the logic of creating a TextEditingController (but also AnimationController, implicit animations, and many more). That logic consists of multiple steps:

  • defining a variable on State.
    dart TextEditingController controller;
  • creating the controller (usually inside initState), with potentially a default value:
    dart @override void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • disposed the controller when the State is disposed:
    dart @override void dispose() { controller.dispose(); super.dispose(); }
  • doing whatever we want with that variable inside build.
  • (optional) expose that property on debugFillProperties:
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

This, in itself, is not complex. The problem starts when we want to scale that approach.
A typical Flutter app may have dozens of text-fields, which means this logic is duplicated multiple times.

Copy-pasting this logic everywhere "works", but creates a weakness in our code:

  • it can be easy to forget to rewrite one of the steps (like forgetting to call dispose)
  • it adds a lot of noise in the code

The Mixin issue

The first attempt at factorizing this logic would be to use a mixin:

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

  @override
  void initState() {
    super.initState();
    _textEditingController = TextEditingController();
  }

  @override
  void dispose() {
    _textEditingController.dispose();
    super.dispose();
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty('textEditingController', textEditingController));
  }
}

Then used this way:

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

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

But this has different flaws:

  • A mixin can be used only once per class. If our StatefulWidget needs multiple TextEditingController, then we cannot use the mixin approach anymore.

  • The "state" declared by the mixin may conflict with another mixin or the State itself.
    More specifically, if two mixins declare a member using the same name, there will be a conflict.
    Worst-case scenario, if the conflicting members have the same type, this will silently fail.

This makes mixins both un-ideal and too dangerous to be a true solution.

Using the "builder" pattern

Another solution may be to use the same pattern as StreamBuilder & co.

We can make a TextEditingControllerBuilder widget, which manages that controller. Then our build method can use it freely.

Such a widget would be usually implemented this way:

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

  final Widget Function(BuildContext, TextEditingController) builder;

  @override
  _TextEditingControllerBuilderState createState() =>
      _TextEditingControllerBuilderState();
}

class _TextEditingControllerBuilderState
    extends State<TextEditingControllerBuilder> {
  TextEditingController textEditingController;

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(
        DiagnosticsProperty('textEditingController', textEditingController));
  }

  @override
  void dispose() {
    textEditingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, textEditingController);
  }
}

Then used as such:

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextEditingControllerBuilder(
      builder: (context, controller) {
        return TextField(
          controller: controller,
        );
      },
    );
  }
}

This solves the issues encountered with mixins. But it creates other issues.

  • The usage is very verbose. That's effectively 4 lines of code + two levels of indentation for a single variable declaration.
    This is even worse if we want to use it multiple times. While we can create a TextEditingControllerBuilder inside another once, this drastically decrease the code readability:

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

    That's a very indented code just to declare two variables.

  • This adds some overhead as we have an extra State and Element instance.

  • It is difficult to use the TextEditingController outside of build.
    If we want a State life-cycles to perform some operation on those controllers, then we will need a GlobalKey to access them. For example:

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

Most helpful comment

I'll add a few thoughts from the React perspective.
Pardon if they're not relevant but I wanted to briefly explain how we think about Hooks.

Hooks are definitely "hiding" things. Or, depending on how you look at it, encapsulate them. In particular, they encapsulate local state and effects (I think our "effects" are the same things as "disposables"). The "implicitness" is in that they automatically attach the lifetime to the Component inside of which they're called.

This implicitness is not inherent in the model. You could imagine an argument being explicitly threaded through all calls — from the Component itself throughout custom Hooks, all the way to each primitive Hook. But in practice, we found that to be noisy and not actually useful. So we made currently executing Component implicit global state. This is similar to how throw in a VM searches upwards for the closest catch block instead of you passing around errorHandlerFrame in code.

Okay, so it's functions with implicit hidden state inside them, that seems bad? But in React, so are Components in general. That's the whole point of Components. They're functions that have a lifetime associated with them (which corresponds to a position in the UI tree). The reason Components themselves are not a footgun with regards to state is that you don't just call them from random code. You call them from other Components. So their lifetime makes sense because you remain in the context of UI code.

However, not all problems are component-shaped. Components combine two abilities: state+effects, and a lifetime tied to tree position. But we've found that the first ability is useful on its own. Just like functions are useful in general because they let you encapsulate code, we were lacking a primitive that would let us encapsulate (and reuse) state+effects bundles without necessarily creating a new node in the tree. That's what Hooks are. Components = Hooks + returned UI.

As I mentioned, an arbitrary function hiding contextual state is scary. This is why we enforce a convention via a linter. Hooks have "color" — if you use a Hook, your function is also a Hook. And the linter enforces that only Components or other Hooks may use Hooks. This removes the problem of arbitrary functions hiding contextual UI state because now they're no more implicit than Components themselves.

Conceptually, we don't view Hook calls as plain function calls. Like useState() is more use State() if we had the syntax. It would be a language feature. You can model something like Hooks with Algebraic Effects in languages that have effect tracking. So in that sense, they would be regular functions, but the fact that they "use" State would be a part of their type signature. Then you can think of React itself as a "handler" for this effect. Anyway, this is very theoretical but I wanted to point at prior art in terms of the programming model.

In practical terms, there are a few things here. First, it's worth noting Hooks aren't an "extra" API to React. They're the React API for writing Components at this point. I think I'd agree that as an extra feature they wouldn't be very compelling. So I don't know if they really make sense for Flutter which has an arguably different overall paradigm.

As for what they allow, I think the key feature is the ability to encapsulate state+effectful logic, and then chain it together like you would with regular function composition. Because the primitives are designed to compose, you can take some Hook output like useState(), pass it as an input to a cusom useGesture(state), then pass that as an input to several custom useSpring(gesture) calls which give you staggered values, and so on. Each of those pieces is completely unaware of the others and may be written by different people but they compose well together because state and effects are encapsulated and get "attached" to the enclosing Component. Here's a small demo of something like this, and an article where I briefly recap what Hooks are.

I want to emphasize this is not about reducing the boilerplate but about the ability to dynamically compose pipelines of stateful encapsulated logic. Note that it is fully reactive — i.e. it doesn't run once, but it reacts to all changes in properties over time. One way to think of them is they're like plugins in an audio signal pipeline. While I totally get the wary-ness about "functions that have memories" in practice we haven't found that to be a problem because they're completely isolated. In fact, that isolation is their primary feature. It would fall apart otherwise. So any codependence has to be expressed explicitly by returning and passing values into the next thing in the chain. And the fact that any custom Hook can add or remove state or effects without breaking (or even affecting) its consumers is another important feature from the third-party library perspective.

I don't know if this was helpful at all, but hope it sheds some perspective on the programming model.
Happy to answer other questions.

All 420 comments

cc @dnfield @Hixie
As requested, that's the full details on what are the problems solved by hooks.

I'm concerned that any attempt to make this easier within the framework will actually hide complexity that users should be thinking about.

It seems like some of this could be made better for library authors if we strongly typed classes that need to be disposed with some kind of abstract class Disposable. In such a case you should be able to more easily write a simpler class like this if you were so inclined:

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

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

  @override
  void dispose() {
    for (final Disposable disposable in _disposables)
      disposable.dispose();
    super.dispose();
  }
}

Which gets rid of a few repeated lines of code. You could write a similar abstract class for debug properties, and even one that combines both. Your init state could end up looking something like:

@override
void initState() {
  super.initState();
  controller = TextEditingController(text: 'Hello world');
  addDisposable(controller);
  addProperty('controller', controller);
}

Are we just missing providing such typing information for disposable classes?

I'm concerned that any attempt to make this easier within the framework will actually hide complexity that users should be thinking about.

Widgets hides the complexity that users have to think about.
I'm not sure that's really a problem.

In the end it is up to users to factorize it however they want.


The problem is not just about disposables.

This forgets the update part of the problem. The stage logic could also rely on lifecycles like didChangeDependencies and didUpdateWidget.

Some concrete examples:

  • SingleTickerProviderStateMixin which has logic inside didChangeDependencies.
  • AutomaticKeepAliveClientMixin, which relies on super.build(context)

There are many examples in the framework where we want to reuse state logic:

  • StreamBuilder
  • TweenAnimationBuilder
    ...

These are nothing but a way to reuse state with an update mechanism.

But they suffer from the same issue as those mentioned on the "builder" part.

That causes many problems.
For example one of the most common issue on Stackoverflow is people trying to use StreamBuilder for side effects, like "push a route on change".

And ultimately their only solution is to "eject" StreamBuilder.
This involves:

  • converting the widget to stateful
  • manually listen to the stream in initState+didUpdateWidget+didChangeDependencies
  • cancel the previous subscription on didChangeDependencies/didUpdateWidget when the stream changes
  • cancel the subscription on dispose

That's _a lot of work_, and it's effectively not reusable.

Problem

Reusing a State logic across multiple StatefulWidget is very difficult, as soon as that logic relies on multiple life-cycles.

A typical example would be the logic of creating a TextEditingController (but also AnimationController, implicit animations, and many more). That logic consists of multiple steps:

  • defining a variable on State.
    dart TextEditingController controller;
  • creating the controller (usually inside initState), with potentially a default value:
    dart @override void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • disposed the controller when the State is disposed:
    dart @override void dispose() { controller.dispose(); super.dispose(); }
  • doing whatever we want with that variable inside build.
  • (optional) expose that property on debugFillProperties:
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

This, in itself, is not complex. The problem starts when we want to scale that approach.
A typical Flutter app may have dozens of text-fields, which means this logic is duplicated multiple times.

Copy-pasting this logic everywhere "works", but creates a weakness in our code:

  • it can be easy to forget to rewrite one of the steps (like forgetting to call dispose)
  • it adds a lot of noise in the code

I really have trouble understanding why this is a problem. I've written plenty of Flutter applications but it really doesn't seem like that much of an issue? Even in the worst case, it's four lines to declare a property, initialize it, dispose it, and report it to the debug data (and really it's usually fewer, because you can usually declare it on the same line you initialize it, apps generally don't need to worry about adding state to the debug properties, and many of these objects don't have state that needs disposing).

I agree that a mixin per property type doesn't work. I agree the builder pattern is no good (it literally uses the same number of lines as the worst case scenario described above).

With NNBD (specifically with late final so that initiializers can reference this) we'll be able to do something like 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>>[];

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

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

  @override
  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));
  }
}

You'd use it like this:

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

  final String title;

  @override
  _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;
    });
  }

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

Doesn't seem to really make things better. It's still four lines.

Widgets hides the complexity that users have to think about.

What do they hide?

The problem is not the number of lines, but what these lines are.

StreamBuilder may be about as many lines as stream.listen + setState + subscription.close.
But writing a StreamBuilder can be done without any reflection involved, so to say.
There is no mistake possible in the process. It's just "pass the stream, and build widgets out of it".

Whereas writing the code manually involves a lot more thoughts:

  • Can the stream change over time? If we forgot to handle that, we have a bug.
  • Did we forget to close the subscription? Another bug
  • What variable name do I use for the subscription? That name may not be available
  • What about testing? Do I have to duplicate the test? With StreamBuilder, there's no need to write unit tests for listening to the stream, that would be redundant. But if we write it manually all the time, it's entirely feasible to make a mistake
  • If we listen to two streams at once, we now have multiple variables with very similar names polluting our code, it may cause some confusion.

What do they hide?

  • FutureBuilder/StreamBuilder hides the listening mechanism and keeps track of what is the current Snapshot.
    The logic of switching between two Future is fairly complex too, considering it doesn't have a subscription.close().
  • AnimatedContainer hides the logic of making a tween between the previous and new values.
  • Listview hides the logic of "mount a widget as it appears"

apps generally don't need to worry about adding state to the debug properties

They don't, because they do not want to deal with the complexity of maintaining the debugFillProperties method.
But if we told developers "Would you like it is out of the box all of your parameters and state properties were available on Flutter's devtool?" I'm sure they would say yes

Many people have expressed to me their desire for a true equivalent to React's devtool. Flutter's devtool is not yet there.
In React, we can see all the state of a widget + its parameters, and edit it, without doing anything.

Similarly, people were quite surprised when I told them that when using provider + some other packages of mine, by default their entire application state is visible to them, without having to do anything (modulo this annoying devtool bug)

I have to admit that I'm not a big fan of FutureBuilder, it causes a lot of bugs because people don't think about when to trigger the Future. I think it would not be unreasonable for us to drop support for it. StreamBuilder is ok I guess but then I think Streams themselves are too complicated (as you mention in your comment above) so...

Why does someone have to think about the complexity of creating Tweens?

ListView doesn't really hide the logic of mounting a widget as it appears; it's a big part of the API.

The problem is not the number of lines, but what these lines are.

I really don't understand the concern here. The lines seem pretty much like simple boilerplate. Declare the thing, initialize the thing, dispose of the thing. If it's not the number of lines, then what's the problem?

I'll agree with you that FutureBuilder is problematic.

It's a bit off-topic, but I would suggest that in development, Flutter should trigger a fake hot-reload every few seconds. This would highlight misuses of FutureBuilder, keys, and many more.

Why does someone have to think about the complexity of creating Tweens?

ListView doesn't really hide the logic of mounting a widget as it appears; it's a big part of the API.

We agree on that. My point was that we cannot criticize something like hooks with "it hides logic", as what hooks do is strictly equivalent to what a TweenAnimationBuilder/AnimatedContainer/... do.
The logic is not hidden

In the end, I think animations are a good comparison. Animations have this concept of implicit vs explicit.
Implicit animations are loved because of their simplicity, composability and readability.
Explicit animations are more flexible, but more complex.

When we translate this concept to listening to streams, StreamBuilder is an _implicit listening_, whereas stream.listen is _explicit_.

More specifically, with StreamBuilder you _cannot_ forget to handle the scenario where the stream changes, or forget to close the subscription.
You can also combine multiple StreamBuilder together

stream.listen is slightly more advanced and more error-prone.

Builders are powerful to simplify the application.
But as we agreed on previously, the Builder pattern is not ideal. It's both verbose to write and to use.
This issue, and what hooks solve, is about an alternate syntax for *Builders

For example, flutter_hooks has a strict equivalent to FutureBuilder and StreamBuilder:

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

In the continuation, AnimatedContainer & alike could be represented by a useAnimatedSize / useAnimatedDecoractedBox / ... such that we have:

double opacity;

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

My point was that we cannot criticize something like hooks with "it hides logic",

That's not the argument. The argument is "it hides logic that developers should be thinking about".

Do you have an example of such logic that the developers should be thinking about?

Like, who owns the TextEditingController (who creates it, who disposes of it).

Like with this code?

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

The hook creates it and disposes of it.

I'm not sure what is unclear about this.

Yes, exactly. I have no idea what the lifecycle of the controller is with that code. Does it last until the end of the lexical scope? The lifetime of the State? Something else? Who owns it? If I pass it to someone else, can they take ownership? None of this is obvious in the code itself.

It looks like your argument is caused more by a lack of understanding on what hooks do rather than a real issue.
These questions have a clearly defined answer that is consistent with all hooks:

I have no idea what the lifecycle of the controller is with that code

Nor do you have to think about it. It is no longer the responsibility of the developer.

Does it last until the end of the lexical scope? The lifetime of the State

The lifetime of the State

Who owns it?

The hook owns the controller. It is part of the API of useTextEditingController that it owns the controller.
This applies to useFocusNode, useScrollController, useAnimationController, ...

In a way, these questions apply to StreamBuilder:

  • We don't have to think about the life-cycles of the StreamSubscription
  • The subscription lasts for the lifetime of the State
  • the StreamBuilder owns the StreamSubscription

In general, you can think of:

final value = useX(argument);

as a strict equivalent to:

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

  },
);

They have the same rules and the same behavior.

It is no longer the responsibility of the developer

I think fundamentally that's the disagreement here. Having a function-like API that returns a value that has a defined life-time that isn't clear is, IMHO, fundamentally very different than an API based on passing that value to a closure.

I have no problem with someone creating a package that uses this style, but it's a style contrary to the kind that I would want to include in the core flutter API.

@Hixie
I don't think what @rrousselGit was saying was that they are the same thing but just that they have "the same rules and the same behaviour" regarding life cycle? Correct?

They don't solve the same problems though.

Maybe I'm wrong here but last fall when trying out flutter I believe that if I would have needed three of those builders in one widget it would have been a lot of nesting. Compared to three hooks (three lines).
Also. Hooks are composable so if you need to share state logic composed of multiple hooks you could make a new hook that uses other hooks and some extra logic and just use the one new hook.

Such stuff like sharing state logic easily between widgets was a thing I was missing when trying out flutter fall of 2019.

There could of course be a lot of other possible solutions. Maybe it's already been solved and I just didn't find it in the docs.
But if not there are a lot that could be done to speed up development a great deal if a thing like hooks or another solution for the same problems where available as a first class citizen.

I'm definitely not suggesting using the builder approach, as the OP mentions, that has all kinds of problems. What I would suggest is just using initState/dispose. I don't really understand why that's a problem.

I'm curious how people feel about the code in https://github.com/flutter/flutter/issues/51752#issuecomment-664787791. I don't think it's any better than initState/dispose, but if people like hooks, do they like that too? Is hooks better? Worse?

@Hixie Hooks are nice to use because they compartmentalize the life cycle into a single function call. If I use a hook, say useAnimationController, I don't have to think about initState and dispose anymore. It removes the responsibility from the developer. I don't have to worry whether I disposed every single animation controller I created.

initState and dispose are fine for a single thing but imagine having to keep track of multiple and disparate types of state. Hooks compose based on the logical unit of abstraction instead of spreading them out in the life cycle of the class.

I think what you're asking is the equivalent of asking why have functions when we can manually take care of effects every time. I agree it is not exactly the same, but it broadly feels similar. It seems that you have not used hooks before so the problems don't seem too apparent to you, so I would encourage you to do a small or medium size project using hooks, with the flutter_hooks package perhaps, and see how it feels. I say this with all respect, as a user of Flutter I have run into these issues that hooks provide solutions to, as have others. I am unsure how to convince you that these problems truly exist for us, let us know if there's a better way.

I'll add a few thoughts from the React perspective.
Pardon if they're not relevant but I wanted to briefly explain how we think about Hooks.

Hooks are definitely "hiding" things. Or, depending on how you look at it, encapsulate them. In particular, they encapsulate local state and effects (I think our "effects" are the same things as "disposables"). The "implicitness" is in that they automatically attach the lifetime to the Component inside of which they're called.

This implicitness is not inherent in the model. You could imagine an argument being explicitly threaded through all calls — from the Component itself throughout custom Hooks, all the way to each primitive Hook. But in practice, we found that to be noisy and not actually useful. So we made currently executing Component implicit global state. This is similar to how throw in a VM searches upwards for the closest catch block instead of you passing around errorHandlerFrame in code.

Okay, so it's functions with implicit hidden state inside them, that seems bad? But in React, so are Components in general. That's the whole point of Components. They're functions that have a lifetime associated with them (which corresponds to a position in the UI tree). The reason Components themselves are not a footgun with regards to state is that you don't just call them from random code. You call them from other Components. So their lifetime makes sense because you remain in the context of UI code.

However, not all problems are component-shaped. Components combine two abilities: state+effects, and a lifetime tied to tree position. But we've found that the first ability is useful on its own. Just like functions are useful in general because they let you encapsulate code, we were lacking a primitive that would let us encapsulate (and reuse) state+effects bundles without necessarily creating a new node in the tree. That's what Hooks are. Components = Hooks + returned UI.

As I mentioned, an arbitrary function hiding contextual state is scary. This is why we enforce a convention via a linter. Hooks have "color" — if you use a Hook, your function is also a Hook. And the linter enforces that only Components or other Hooks may use Hooks. This removes the problem of arbitrary functions hiding contextual UI state because now they're no more implicit than Components themselves.

Conceptually, we don't view Hook calls as plain function calls. Like useState() is more use State() if we had the syntax. It would be a language feature. You can model something like Hooks with Algebraic Effects in languages that have effect tracking. So in that sense, they would be regular functions, but the fact that they "use" State would be a part of their type signature. Then you can think of React itself as a "handler" for this effect. Anyway, this is very theoretical but I wanted to point at prior art in terms of the programming model.

In practical terms, there are a few things here. First, it's worth noting Hooks aren't an "extra" API to React. They're the React API for writing Components at this point. I think I'd agree that as an extra feature they wouldn't be very compelling. So I don't know if they really make sense for Flutter which has an arguably different overall paradigm.

As for what they allow, I think the key feature is the ability to encapsulate state+effectful logic, and then chain it together like you would with regular function composition. Because the primitives are designed to compose, you can take some Hook output like useState(), pass it as an input to a cusom useGesture(state), then pass that as an input to several custom useSpring(gesture) calls which give you staggered values, and so on. Each of those pieces is completely unaware of the others and may be written by different people but they compose well together because state and effects are encapsulated and get "attached" to the enclosing Component. Here's a small demo of something like this, and an article where I briefly recap what Hooks are.

I want to emphasize this is not about reducing the boilerplate but about the ability to dynamically compose pipelines of stateful encapsulated logic. Note that it is fully reactive — i.e. it doesn't run once, but it reacts to all changes in properties over time. One way to think of them is they're like plugins in an audio signal pipeline. While I totally get the wary-ness about "functions that have memories" in practice we haven't found that to be a problem because they're completely isolated. In fact, that isolation is their primary feature. It would fall apart otherwise. So any codependence has to be expressed explicitly by returning and passing values into the next thing in the chain. And the fact that any custom Hook can add or remove state or effects without breaking (or even affecting) its consumers is another important feature from the third-party library perspective.

I don't know if this was helpful at all, but hope it sheds some perspective on the programming model.
Happy to answer other questions.

I'm definitely not suggesting using the builder approach, as the OP mentions, that has all kinds of problems. What I would suggest is just using initState/dispose. I don't really understand why that's a problem.

I'm curious how people feel about the code in #51752 (comment). I don't think it's any better than initState/dispose, but if people like hooks, do they like that too? Is hooks better? Worse?

The late keyword makes things better, but it still suffers from some issues:

Such Property may be useful for states that are self-contained or that do not depend on parameters that can change over time. But it may get difficult to use when in a different situation.
More precisely, it lacks the "update" part.

For example, with StreamBuilder the stream listened can change over time. But there is no easy solution to implement such thing here, as the object is initialized only once.

Similarly, hooks have an equivalent to Widget's Key – which can cause a piece of state to be destroyed and re-created when that key changes.

An example of that is useMemo, which is a hook that cache an instance of object.
Combined with keys, we can use useMemo to have implicit data fetching.
For example, our widget may receive a message ID – which we then use to fetch the message details. But that message ID may change over time, so we may need to re-fetch the details.

With useMemo, this may look like:

String messageId;

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

}

In this situation, even if the build method is called again 10 times, as long as messageId does not change, the data-fetching is not performed again.
But when the messageId changes, a new Future is created.


It's worth noting that I do not think flutter_hooks in its current state is refined for Dart. My implementation is more of a POC than a fully-fledged architecture.
But I do believe that we have an issue with code reusability of StatefulWidgets.

I didn't remember where, but I remember suggesting that hooks in the ideal world would be a custom function generator, next to async* & sync*, which may be similar to what Dan suggest with use State rather than useState

@gaearon

I want to emphasize this is not about reducing the boilerplate but about the ability to dynamically compose pipelines of stateful encapsulated logic.

That isn't the problem being discussed here. I recommend filing a separate bug to talk about the inability to do what you describe. (That sounds like a very different problem and honestly a more compelling one than the one described here.) This bug is specifically about how some of the logic is too verbose.

No he is right, it is my wording that may be confusing.
As I mentioned previously, this is not about the number of lines of code, but the lines of code themselves.

This is about factorizing state.

This bug is extremely clear about the problem being "Reusing state logic is too verbose/difficult" and being all about how there is too much code in a State when you have a property that needs to have code to declare it, in initState, in dispose, and in debugFillProperties. If the problem you care about is something different then I recommend filing a new bug that describes that problem.

I really, really strongly recommend forgetting about hooks (or any solution) until you fully understand the problem you want to solve. It's only by having a clear understanding of the problem that you will be able to articulate a convincing argument in favour of a new feature, because we must evaluate features against the problems that they solve.

I think you are misunderstanding what I said in that issue then.

The problem is by no mean boilerplate, but reusability.

Boilerplate is a consequence of an issue with reusability, not the cause

What this issue describes is:

We may want to reuse/compose state logic. But the options available are either mixins, Builders, or not reusing it – all of which have their own issues.

The issues of the existing options may be related to boilerplate, but the problem we are trying to solve isn't.
While reducing the boilerplate of Builders is one path (which is what hooks do), there may be a different one.

For example, something I wanted to suggest for a while was to add methods like:

context.onDidChangeDependencies(() {

});
context.onDispose(() {

});

But these have their own issues and do not fully solve the problem, so I didn't.

@rrousselGit, feel free to edit the original problem statement at the top here to better reflect the problem. Also feel free to create a design doc: https://flutter.dev/docs/resources/design-docs that we can iterate on together (again, as @Hixie suggests, focusing for now on the tightest possible exposition of the problem statement). I'd love you to feel as empowered as any other Flutter engineer -- you're part of the team, so let's iterate together!

I've looked through the issue again a few times. In all honesty, I do not understand where the misunderstanding is coming from, so I'm not sure what to improve.
The original comment repeatedly mentions the desire for reusability/factorization. The mentions about boilerplate are not "Flutter is verbose" but "Some logics are not reusable"

I don't think the design doc suggestion is fair. It takes a significant amount of time to write such a document, and I am doing this in my free time.
I am personally satisfied with hooks. I'm not authoring these issues in my interest, but to raise awareness about a problem that impacts a significant number of people.

A few weeks ago, I was hired to discuss architecture about an existing Flutter app. Their probably was exactly what is mentioned here:

  • They have some logic that needs to be reused in multiple widgets (handling loading states / marking "messages" as read when some widgets become visible / ...)
  • They tried to use mixins, which caused major architecture flaws.
  • They also tried to manually handle the "create/update/dispose" by rewriting that logic in multiple locations, but it caused bugs.
    In some places, they forgot to close subscriptions. In others, they didn't handle the scenario where their stream instance changes
  • marking "messages" as read when some widgets become visible

That's an interesting case because it's similar to issues I've had in one of my own apps, so I looked at how I'd implemented the code there and I really don't see much of the problems that this bug describes, which may be why I'm having trouble understanding the problem. This is the code in question:

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

Do you have examples of actual apps I could study to see the problem in action?

(BTW, in general I would strongly recommend not using Streams at all. I think they generally make things worse.)

(BTW, in general I would strongly recommend not using Streams at all. I think they generally make things worse.)

(I wholeheartedly agree. But the community currently has the opposite reaction. Maybe extracting ChangeNotifier/Listenable/ValueNotifier out of Flutter into an official package would help)

Do you have examples of actual apps I could study to see the problem in action?

Sadly no. I can only share the experience I had while helping others. I don't have an app at hand.

That's an interesting case because it's similar to issues I've had in one of my own apps, so I looked at how I'd implemented the code there and I really don't see much of the problems that this bug describes, which may be why I'm having trouble understanding the problem. This is the code in question:

In your implementation, the logic isn't tied to any life-cycle and placed inside _build_, so it kind of works around the problem.
It may make sense in that specific case. I'm not sure if that example was good.

A better example may be pull-to-refresh.

In a typical pull-to-refresh, we will want:

  • on the first build, handle loading/error states
  • on refresh:

    • if the screen was in error state, show the loading screen once again

    • if the refresh was performed during loading, cancel pending HTTP requests

    • if the screen showed some data:

    • keep showing the data while the new state is loading

    • if the refresh fails, keep showing the previously obtained data and show a snackbar with the error

    • if the user pops and re-enters the screen while the refresh is pending, show the loading screen

    • make sure that the RefreshIndicator says visible while the refresh is pending

And we'll want to implement such a feature for all resources and multiple screens. Furthermore, some screens may want to refresh multiple resources at once.

ChangeNotifier + provider + StatefulWidget will have quite a lot of difficulties factorizing this logic.

Whereas my latest experiments (which is immutability based & relies on flutter_hooks) supports the entire spectrum out of the box:

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

This logic is entirely self-contained. It can be reused with any resource inside any screens.

And if one screen wants to refresh multiple resources at once, we can do:

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

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

I would recommend putting all of that logic in the app state, outside of the widgets, and just having the app state reflect the current app state. Pull to refresh needs no state within the widget, it just has to tell the ambient state that a refresh is pending and then wait for its future to complete.

It isn't the responsibility of the ambient state to determine how to render an error vs loading vs data

Having this logic in the ambient state doesn't remove all logics from the UI
The UI still need to determine whether to show the error in full screen or in a snack-bar
It still need to force errors to be refreshed when the page is reloaded

And this is less reusable.
If the rendering logic is fully defined in the widgets rather than the ambient state, then it will work with _any_ Future and can even be included directly inside Flutter.

I don't really understand what you are advocating for in your last comment. My point is that you don't need changes to the framework to do something just as simple as the refresh indicator code above, as is demonstrated by the code I cited earlier.

If we have a lot of these types of interactions, not just for refresh indicators, but for animations, and others, it is better to encapsulate them where they are closest to being needed rather than putting them in the app state, because the app state doesn't need to know the specifics of every single interaction in the app if it's not needed in multiple places in the app.

I don't think we are agreeing on the complexity of the feature and its reusability.
Do you have an example which showcase that such feature is easy?

I linked to the source of one app I wrote above. It's certainly not perfect code, and I plan to rewrite bits of it for the next release, but I didn't experience the problems you describe in this issue.

But you are one of the tech leads of Flutter.
Even when faced with a problem, you would have enough skill to immediately come up with a solution.

Yet on the other side, a significant number of people do not understand what is wrong with the following code:

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

This fact is proven by how popular a Q/A I made on StackOverflow is.

The problem is not that it is impossible to abstract state logic in a reusable and robust way (otherwise there is no point in making this issue).
The problem is that it requires both time and experience to do so.

By providing an official solution, this reduces the likelihood that an application ends up unmaintainable – which increases the overall productivity and developer experience.
Not everyone could come up with your Property suggestion. If such a thing was built inside Flutter, it would be documented, get visibility, and ultimately help people that would have never thought about it to begin with.

The problem is that it really depends on what your app is, what your state looks like, and so on. If the question here is just "how do you manage app state" then the answer isn't anything like hooks, it's lots of documentation talking about different ways to do it and recommending different techniques for different situations... basically, this set of documents: https://flutter.dev/docs/development/data-and-backend/state-mgmt

There is ephemeral and app state, but there seems to be another use case as well: state that is only concerned with a single type of widget but that you nevertheless want to share between that type of widget.

For example, a ScrollController may invoke some type of animation, but it's not necessarily appropriate to put that into the global app state, because it's not data that needs to be used across all of the app. However, multiple ScrollControllers might have the same logic, and you want to share that life cycle logic between each of them. The state is still for only ScrollControllers, so not global app state, but copy-pasting the logic is prone to error.

Moreover, you may want to package this logic to make it more composable for your future projects, but also to others. If you look at the site useHooks, you'll see many pieces of logic that compartmentalize certain common actions. If you use useAuth you write it once and never have to worry about whether you missed an initState or dispose call, or whether the async function is has a then and catch. The function is written only once so the room for error basically disappears. Therefore, this kind of solution is not only more composable for multiple parts of the same app and between multiple apps, but it is also safer for the end programmer.

I have no objection to people using hooks. As far as I can tell, nothing is preventing that. (If something _is_ preventing that, then please file a bug about that.)

This bug isn't about hooks, it's about "Reusing state logic is too verbose/difficult", and I'm still struggling to understand why this requires any changes to Flutter. There's been many examples (including hooks) showing how it's possible to avoid the verbosity by structuring one's application one way or another, and there's lots of documentation about it already.

I see, so you're asking why, if there exists something like a hooks package which was built with no changes to Flutter already, there needs to be a first party solution for hooks? I suppose @rrousselGit can answer this better but the answer probably involves better support, more integrated support and more people using them.

I can agree with you that besides that, I am also confused why any fundamental changes need to be made to Flutter to support hooks, since ostensibly the flutter_hooks package exists already.

I'm still struggling to understand why this requires any changes to Flutter.

Saying that this problem is solved because the community made a package is like saying that Dart doesn't need data-classes + union types because I made Freezed.
Freezed may be quite liked by the community as a solution to both of these problems, but we still can do better.

The Flutter team has a lot more leverage than the community ever will. You have both the capability to modify the entire stack; people that are experts on each individual part; and a salary to sponsor the work needed.

This problem needs that.
Remember: One of the goals of the React team is for hooks to be part of the language, kind of like with JSX.

Even without language support, we still need work in analyzer; dartpad; flutter/devtools; and many hooks to simplify all the different things that Flutter does (such as for implicit animations, forms, and more).

That's a good argument, I agree, even though the general philosophy of Flutter to have a small core. For that reason, we've been increasingly adding new functionality as packages even when it comes from Google, c.f. characters and animations. That gives us greater flexibility to learn and change over time. We would do the same for this space, unless there's a compelling technical reason why a package was insufficient (and with extension methods, that's even less likely than ever).

Putting things into the core of Flutter is tricky. One challenge is, as you know well from first-hand experience, is that state is an area that is evolving as we all learn more about what works well in a reactive UI architecture. Two years ago, if we'd been forced to pick a winner, we might have selected BLoC, but then of course your provider package took over and is now our default recommendation.

I could comfortably conceive of Google-employed contributors supporting flutter_hooks or a similar hooks package that had traction (although we have plenty of other work that is competing for our attention, obviously). In particular, we should If you're looking for us to take it over from you, that's obviously a different question.

Interesting argument, @timsneath. The Rust community also does something similar, because once introduced into the core or standard library of a language or framework, it is very difficult to take it out. In Rust's case, it is impossible as they want to maintain backwards compatibility forever. Therefore, they wait until packages have arrived and competed with each other until only a few winners emerge, then they fold that into the language.

This could be a similar case with Flutter. There might be something better than hooks later on, just as React had to move from classes to hooks but still had to maintain classes, and people had to migrate. It might then be better to have competing state management solutions before being added to the core. And perhaps we the community should innovate on top of hooks or try finding even better solutions.

I understand that concern, but this isn't about a state management solution.

Such feature is closer to Inheritedwidget & StatefulWidget. It is a low level primitive, that could be as low as a language feature.

Hooks may be independent from the framework, but that's only through luck.
As I mentioned before, another path to this problem may be:

context.onDispose(() {

});

And similar event listeners.
But that is impossible to implement out of the framework.

I do not know what the team would come up with.
But we can't exclude the possibility that such solution would have to be directly next to Element

Do extensions help with that?

(Maybe we should talk about that in a different issue, though. It's sort of off-topic here. I really would prefer if we had one issue per problem that people are seeing, so we could discuss solutions in the right place. It's not clear how context.onDispose would help with verbosity.)

I strongly suspect there are some really good language proposals we could come up with related to this.

I think it'd be be helpful to talk about them more specifically than how they might enable a specific state management idiom. We could then more seriously consider what they would enable and what tradeoffs they might entail.

In particular, we'd be able to consider how and whether they could work in both the VM and JS runtimes

It's not clear how context.onDispose would help with verbosity.)

As I mentioned before, this issue is more about code-reusability than verbosity. But if we can reuse more code, this should implicitly reduce the verbosity.

The way context.onDispose is related to this issue is, with the current syntax we have:

AnimationController controller;

@override
void initState() {
  controller = AnimationController(...);
}

@override
void dispose() {
  controller.dispose();
}

The problem is:

  • this is tightly coupled to the class definition, so cannot be reused
  • as the widget grows, the relationship between the initialization and dispose becomes harder to read since there is hundreds of lines of code in the middle.

With a context.onDispose, we could do:

@override
void initState() {
  controller = AnimationController(...);
  context.onDispose(controller.dispose);
}

The interesting part is:

  • this is no-longer tightly coupled with the class definition, so it can be extracted into a function.
    We could theoretically have semi-complex logic:
    ```dart
    AnimationController someReusableLogic(BuildContext context) {
    final controller = AnimationController(...);
    controller.onDispose(controller.dispose);
    controller.forward();
    void listener() {}
    controller.addListener(listener);
    context.onDispose(() => controller.removeListener(listener));
    }
    ...

@override
void initState() {
controller = someReusableLogic(context);
}
```

  • all the logic is bundled together. Even if the widget grows to be 300 long, the logic of controller is still easily readable.

The problem with this approach is:

  • context.myLifecycle(() {...}) is not hot-reloadable
  • it is unclear how to have someReusableLogic read properties from the StatefulWidget without tightly coupling the function to the widget definition.
    For example, the AnimationController's Duration may be passed as a parameter of the widget. So we need to handle the scenario where the duration changes.
  • it is unclear how to implement a function which returns an object that can change over time, without having to resort to a ValueNotifier and dealing with listeners

    • This is especially important for computed states.


I'll think about a language proposal. I have some ideas, but nothing worthy of talking about right now.

As I mentioned before, this issue is more about code-reusability than verbosity

Ok. Can you please file a new bug then that talks about that specifically? This bug is literally called "Reusing state logic is too verbose/difficult". If verbosity is not the issue then _this_ isn't the issue.

With a context.onDispose, we could do:

@override
void initState() {
  controller = AnimationController(...);
  context.onDispose(controller.dispose);
}

I'm not sure why context is relevant in this (and onDispose violates our naming conventions). If you just want a way to register things to run during dispose, though, you can do this easily today:

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

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

  @override
  void dispose() {
    if (_disposeQueue != null) {
      for (VoidCallback callback in _disposeQueue)
        callback();
    }
    super.dispose();
  }
}

Call it like this:

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

  @override
  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));
}
...

@override
void initState() {
  controller = someReusableLogic(context);
}

You can do that too:

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;
}
...

@override
void initState() {
  controller = someReusableLogic(this);
}

The problem with this approach is:

  • context.myLifecycle(() {...}) is not hot-reloadable

In this context it doesn't seem to matter since it's only for things called in initState? Am I missing something?

  • it is unclear how to have someReusableLogic read properties from the StatefulWidget without tightly coupling the function to the widget definition.
    For example, the AnimationController's Duration may be passed as a parameter of the widget. So we need to handle the scenario where the duration changes.

It's pretty simple to add a didChangeWidget queue just like the dispose queue:

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

  @override
  void didUpdateWidget(T oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (_didUpdateWidgetQueue != null) {
      for (VoidCallback callback in _didUpdateWidgetQueue)
        callback();
    }
  }

  @override
  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;
}

Used like this:

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

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

class MyApp extends StatelessWidget {
  @override
  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;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

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

  @override
  void initState() {
    super.initState();
    controller = conditionalAnimator(this, () => widget.animating, () { print(controller.value); });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: FadeTransition(
        opacity: controller,
        child: Text('Hello', style: TextStyle(fontSize: 100.0, color: Colors.white)),
      ),
    );
  }
}
  • it is unclear how to implement a function which returns an object that can change over time, without having to resort to a ValueNotifier and dealing with listeners

    • This is especially important for computed states.

Not sure what this means here, what's wrong with ValueNotifier and, say, a ValueListenableBuilder?

As I mentioned before, this issue is more about code-reusability than verbosity

Ok. Can you please file a new bug then that talks about that specifically? This bug is literally called "Reusing state logic is too verbose/difficult". If verbosity is not the issue then this isn't the issue.

I'm starting to get quite uncomfortable with this discussion. I have already answered this point before:
The topic of this issue is reusability, and verbosity is discussed as a consequence of a reusability issue; not as the primary topic.

There is only a single bullet point in the top comment mentioning verbosity, and that is with StreamBuilder, targeting mainly the 2 levels of indentations.

I'm not sure why context is relevant in this [...]. If you just want a way to register things to run during dispose, though, you can do this easily today:

When I brought up context.onDispose, I mentioned explicitly that I don't think it a good solution.
I explained it because you asked how it is related to the discussion.

As for why context instead of StateHelper, it is because this is more flexible (like working with StatelessWidget)

context.myLifecycle(() {...}) is not hot-reloadable

In this context it doesn't seem to matter since it's only for things called in initState? Am I missing something?

We may change:

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

into:

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

This will not apply the changes to the myLifecycle callback.

But if we used:

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

then hot-reload would work.

Not sure what this means here, what's wrong with ValueNotifier and, say, a ValueListenableBuilder?

This syntax was designed to avoid having to use Builders, so we circled back to the original problem.

Furthermore, if we really want to make our function composable, instead of your ValueGetter + queueDidUpdateWidget suggestion, functions will have to take a ValueNotifier as parameter:

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

as we may want to obtain isAnimating from somewhere other than didUpdateWidget depending on which widget is using this function.
In one place, it may be didUpdateWidget; in another it may be didChangeDependencies; and in yet another place it may be inside the callback of a stream.listen.

But then we need a way to convert these scenarios into a ValueNotifier easily and make our function listen to such notifier.
So we are making our life significantly harder.
It more realiable and easier to use a ConditionalAnimatorBuilder than this pattern I think.

As for why context instead of StateHelper, it is because this is more flexible (like working with StatelessWidget)

StatelessWidget is for, well, stateless widgets. The whole point is that they wouldn't create state, dispose things, react on didUpdateWidget, etc.

Re the hot reload thing, yes. That's why we use methods rather than putting closures in initState.

I'm sorry I keep saying this, and I understand that it must be frustrating, but I still don't understand what the problem we're trying to solve here is. I thought it was verbosity, per the original bug summary and a big chunk of the original description, but I understand that that is not it. So what is the problem? It sounds like there are many mutually exclusive desires here, spread across the many many comments in this bug:

  • Declaring how to dispose something should be done in the same place that allocates it...
  • ...and the place that allocates it needs to run only once since it's allocating it...
  • ...and it needs to work with hot reload (which by definition doesn't rerun code that only runs once)...
  • ...and it needs to be able to create state that works with stateless widgets (which by definition don't have state)...
  • ...and it needs to enable hooking into things like didUpdateWidget and didChangeDependencies...

This iterative dance we're involved in here isn't a productive way to get things done. As I've tried to say before, the best way to get something here is to describe the problem you're facing in a way that we can understand, with all the needs described in one place and explained with use cases. I recommend not listing solutions, especially not solutions that you know do not satisfy your needs. Just make sure the need that makes those solutions inappropriate is listed in the description.

To be honest, fundamentally it sounds to me like you're asking for an entirely different framework design. That's perfectly fine, but it isn't Flutter. If we were to do a different framework it would be, well, a different framework, and we've still got a lot of work to do on _this_ framework. Actually, a lot of what you describe is very similar to how Jetpack Compose is designed. I'm not a huge fan of that design because it requires compiler magic, so debugging what's going on is really hard, but maybe it's more up your alley?

It sounds like there are many mutually exclusive desires here, spread across the many many comments in this bug:

They are not mutually exclusive. Hooks do every single one of these. I won't go into details since we don't want to focus on solutions, but they do check all the boxes.

As I've tried to say before, the best way to get something here is to describe the problem you're facing in a way that we can understand, with all the needs described in one place and explained with use cases.

I still fail to understand how that top comment fails to do that.
It isn't clear to me what isn't clear to others.

Actually, a lot of what you describe is very similar to how Jetpack Compose is designed. I'm not a huge fan of that design because it requires compiler magic, so debugging what's going on is really hard, but maybe it's more up your alley?

I'm not familiar with it, but with a quick search, I would say _yes_.

They are not mutually exclusive.

Are all the bullet points I listed above part of the problem we're trying to solve here?

but they do check all the boxes

Can you list the boxes?

I still fail to understand how that top comment fails to do that.

For example, the OP explicitly says that the problem is about StatefulWidgets, but one of the recent comments on this issue said a particular suggestion was no good because it didn't work with StatelessWidgets.

In the OP you say:

It is difficult to reuse State logic. We either end up with a complex and deeply nested build method or have to copy-paste the logic across multiple widgets.

So from this I assume that the requirements include:

  • Solution must not be deeply nested.
  • Solution must not require lots of similar code in places that try to add state.

The first point (about nesting) seems fine. Definitely not trying to suggest that we should do things that are deeply nested. (That said, we may disagree about what is deeply nested; that isn't defined here. Other comments later imply that builders cause deeply nested code, but in my experience builders are pretty good, as shown in the code I cited earlier.)

The second point seems to straight up be a requirement that we not have verbosity. But then you've explained several times that this is not about verbosity.

The next statement that the OP makes that's describing a problem is:

Reusing a State logic across multiple StatefulWidget is very difficult, as soon as that logic relies on multiple life-cycles.

Honestly I don't really know what this means. "Difficult" to me usually means that something involves a lot of complicated logic that is hard to understand, but allocating, disposing, and reacting to life-cycle events is very simple. The next statement that gives a problem (here I'm skipping the example which is explicitly described as "not complex" and therefore presumably not a description of the problem) is:

The problem starts when we want to scale that approach.

This suggested to me that by "very difficult" you meant "very verbose" and that the difficulty came from there being a lot of occurrences of similar code, since the only difference between the "not complex" example you give and the "very difficult" result of scaling the example is literally just that the same code happens many times (i.e. verbosity, boilerplate code).

This is further supported by the next statement that describes a problem:

Copy-pasting this logic everywhere "works", but creates a weakness in our code:

  • it can be easy to forget to rewrite one of the steps (like forgetting to call dispose)

So presumably it's very difficult because the verbosity makes it easy to make a mistake when copy and pasting the code? But again, when I tried to address this problem, which I would describe as "verbosity", you said the problem isn't verbosity.

  • it adds a lot of noise in the code

Again this sounds like just saying verbosity/boilerplate to me, but again you've explained that it isn't that.

The rest of the OP is just describing solutions that you don't like, so it's presumably not describing the problem.

Does this explain how the OP fails to explain the problem? Everything in the OP that actually describes a problem seems to be describing verbosity, but every time I suggest that that is the problem, you say it isn't and that there is another problem.

I think the misunderstanding boils down to the word meaning.
For example:

it adds a lot of noise in the code

Again this sounds like just saying verbosity/boilerplate to me, but again you've explained that it isn't that.

This point isn't about the number of controller.dispose(), but the value that these lines of code bring to the reader.
That line should always be there and is always the same. As such, its value for the reader is almost null.

What matters is not the presence of this line, but its absence.

The problem is, the more of such controller.dispose() we have, the more likely we are to miss an actual issue in our dispose method.
If we have 1 controller and 0 dispose, it's easy to catch
If we have 100 controllers and 99 dispose, finding the one missing is difficult.

Then we have:

So presumably it's very difficult because the verbosity makes it easy to make a mistake when copy and pasting the code? But again, when I tried to address this problem, which I would describe as "verbosity", you said the problem isn't verbosity.

As I've mentioned in the previous point, not all lines of codes are equal.

If we compare:

+ T state;

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

vs:

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

+    },
+ );

then both of these snippets have the same number of lines and do the same thing.
But ValueListenableBuilder is preferable.

The reason for that is, it's not the number of lines that matters, but what these lines are.

The first snippet has:

  • 1 property declaration
  • 1 method declaration
  • 1 assignment
  • 2 method calls
  • all of which are spread across 2 different life-cycles. 3 if we include build

The second snippet has:

  • 1 class instantiation
  • 1 anonymous function
  • no life-cycle. 1 if we include build

Which makes the ValueListenableBuilder _simpler_.

There is also what these lines do not say:
ValueListenableBuilder handles valueListenable changing over time.
Even in the scenario where widget.valueNotifier does not change over time as we speak, it doesn't hurt.
One day, that statement may change. In which case, ValueListenableBuilder gracefully handles the new behavior, whereas, with the first snippet, we now have a bug.

So not only is ValueListenableBuilder simpler, but it is also more resilient to changes in the code – for the exact same number of lines.


With that, I think we can both agree that ValueListenableBuilder is preferable.
The question is then, "Why not have an equivalent to ValueListenableBuilder for every reusable state logic?"

For example, instead of:

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

we would have:

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

  },
);

with the added benefit that changes to initialText can be hot-reloaded.

This example may be a bit trivial, but we could use this principle for slightly more advanced reusable state logics (like your ModeratorBuilder).

This is "fine" in small snippets. But it causes some problems as we want to scale the approach:

  • Builders circle back to the "too much noise" issue.

For example, I've seen some people manage their model this way:

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

But then, a widget may want to listen to both name, age and gender all at once.
Which means we would have to do:

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

This is obviously not ideal. We removed the pollution inside initState/dispose to pollute our build method.

(let's ignore Listenable.merge for the sake of the example. It doesn't matter here; it's more about the composition)

If we used Builders extensively, it's easy to see ourselves in this exact scenario – and with no equivalent to Listenable.merge (not that I like this constructor, to begin with 😛 )

  • Writing a custom builder is tedious

    There is no easy solution to create a Builder. No refactoring tool will help us here – we can't just "extract as Builder".
    Furthermore, it isn't necessarily intuitive. Making a custom Builder isn't the first thing that people will think about – especially as many will be against the boilerplate (although I'm not).

    People are more likely to bake a custom state-management solution and potentially end-up with bad code.

  • Manipulating a tree of Builders is tedious

    Say we wanted to remove a ValueListenableBuilder in our previous example or add a new one, that's not easy.
    We can spend a few minutes stuck counting () and {} to understand why our code doesn't compile.


Hooks are there to solve the Builder issues we've just mentioned.

If we refactor the previous example to hooks, we would have:

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

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

It is identical to the previous behavior, but the code now has a linear indentation.
Which means:

  • the code drastically more readable
  • it is easier to edit. We don't need to fear (){}; to add a new line.

That's one of the main provider is liked. It removed a lot of nesting by introducing MultiProvider.

Similarly, as opposed to the initState/dispose approach, we benefit from hot-reload.
If we added a new useValueListenable, the change would be applied immediately.

And of course, we still have the ability to extract reusable primitives:

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

and such change can be automated with extract as function, which would work in most scenarios.


Does that answer your question?

Sure. The problem with something like that though is that it just doesn't have enough information to actually do the right thing. For example:

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

...would end up being buggy in really confusing ways.

You can work around that with compiler magic (that's how Compose does it) but for Flutter that violates some of our fundamental design decisions. You can work around it with keys, but then performance suffers greatly (since variable lookup ends up involving map lookups, hashes, and so on), which for Flutter violates some of our fundamental design goals.

The Property solution I suggested earlier, or something derived from that, seems like it avoids the compiler magic while still achieving the goals you've described of having all the code in one place. I don't really understand why it wouldn't work for this. (Obviously it would be extended to also hook into didChangeDependencies and so on to be a full solution.) (We wouldn't put this into the base framework because it would violate our performance requirements.)

Precisely due to the bugs that might occur, as you say, is the reason why hooks should not be called conditionally. See the Rules of Hooks document from ReactJS for more details. The basic gist is that since in their implementation they are tracked by call order, using them conditionally will break that call order and thus not allow them to be tracked correctly. To properly use the hook, you call them at the top level in build without any conditional logic. In the JS version, you get back

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

The Dart equivalent can be similar, it is only longer due to not having unpacking like JS does:

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

If you want conditional logic, you can then decide to use title in the build method _after having called them at the top level_, because now the call order is still preserved. Many of these issues you raise have been explained in the hooks document I linked above.

Sure. And you can do that in a package. I'm just saying that that kind of requirement would violate our design philosophy, which is why we wouldn't add that to Flutter the framework. (Specifically, we optimize for readability and debuggability; having code that looks like it works but, because of a conditional (which might not be obvious in the code) sometimes doesn't work, is not something we want to encourage or enable in the core framework.)

The debugging / conditional behavior is not an issue. That's why an analyzer plugin is important. Such a plugin would:

  • warn if a function uses a hook without being named useMyFunction
  • warn if a hook is used conditionally
  • warn if a hook is used in a loop/callback.

This covers all the potential mistakes. React proved that this is a feasible thing.

Then we are left with the benefits:

  • more readable code (as shown previously)
  • better hot-reload
  • more reusable/composable code
  • more flexible – we can easily make computed states.

About computed states, hooks are quite powerful to cache the instance of an object. This can be used to rebuild a widget only when its parameter changes.

For example, we can have:

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

Such useMemo hook allows easy performance optimizations and handling both init + update declaratively, which avoids bugs too.

This is something that the Property / context.onDispose proposal miss.
They are difficult to use for declarative states without tightly coupling the logic to a life-cycle or complexifying the code with ValueNotifier.

More onto why the ValueGetter proposal is not practical:

We may want to refactor:

final int userId;

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

into:

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

With hooks, this change works flawlessly, as useMemo isn't tied to any life-cycle.

But with Property + ValueGetter, we would have to change the implementation of the Property to make this work – which is undesired as the Property code may be reused in multiple places. So we lost reusability once again.

FWIW this snippet is equivalent to:

class Example extends StatefulWidget {
  final int userId;
  @override
  _ExampleState createState() => _ExampleState();
}

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

  @override
  void initState() {
    super.initState();
    future = fetchUser(widget.userId);
  }

  @override
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.userId != widget.userId) {
      future = fetchUser(widget.userId);
    }
  }

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

I suppose we'll have to find a solution that solves the same problems as @rrousselGit mentions but also has readability and debuggability in mind, then. Vue has its own implementation that might be more in line with what you're looking for, where conditionals or call order do not cause bugs like in React.

Maybe a next step is to create a solution unique to Flutter that is this framework's version of hooks, given Flutter's constraints, just as Vue has made their version given Vue's constraints. I use React's hooks regularly and I would say that just having an analyzer plugin can sometimes be not enough, it should probably be more integrated into the language.

In any case, I don't think we will ever reach a consensus. It sounds like we disagree even on what is readable

As a reminder, I'm sharing this only because I know that the community has some issues with this problem. I personally do not mind if Flutter does nothing about this (although I find this kind of sad), as long as we have:

  • a proper analyzer plugin system
  • the ability to use packages inside dartpad

If you want to pursue the hooks plugin, which I strongly encourage, but are running into some problems, then I recommend filing issues for those problems, and filing PRs to fix those issues. We are more than happy to work with you on that.

Here's a new version of the earlier Property idea. It handles didUpdateWidget and disposal (and can easily be made to handle other such things like didChangeDependencies); it supports hot reload (you can change the code that registers the property and hot reload, and it'll do the right thing); it's type-safe without needing explicit types (relies on inference); it has everything in one place except the property declaration and usage, and performance should be reasonably good (though not quite as good as the more verbose ways of doing things).

Property/PropertyManager:

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

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

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

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

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

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

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

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

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

  @override
  void initState() {
    super.initState();
    _ready = true;
    initProperties();
  }

  @override
  void reassemble() {
    super.reassemble();
    initProperties();
  }

  @protected
  @mustCallSuper
  void initProperties() { }

  @override
  void didUpdateWidget(W oldWidget) {
    super.didUpdateWidget(oldWidget);
    for (Property<Object, W> property in _properties)
      property._didUpdateWidget(oldWidget);
  }

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

Here's how you'd use it:

import 'dart:async';

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

import 'properties.dart';

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

class MyApp extends StatelessWidget {
  @override
  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;

  @override
  _ExampleState createState() => _ExampleState();
}

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

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

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

For convenience you could create prepared Property subclasses for things like AnimationControllers and so on,

You can probably make a version of this that would work in State.build methods too...

I share some of the doubts that @Hixie brings to the table. On the other side I see the clear advantages that Hooks have and it seems quite a number of developers like it.
My problem with the package approach that @timsneath proposed is that code using Hooks looks dramatically different from code without. If they don't get them into the official canon we will end up with Flutter code that isn't readable for people just following the Flutter canon.
If packages start to implement things that should be the responsebility of the framework we will get a lot of different Flutter dialects which makes learning new code bases difficult. So for me I probably would start using hooks the moment it get part of Flutter.
It's much like my current view on the freezed package. I love the functionality but unless Unions and data classes arent part of Dart I don't want to include them in my code base because it would make it more difficult for people to read my code.

@escamoteur Just so I understand, are you suggesting that we fundamentally change how widgets work? Or are you suggesting that there should be some specific new abilities? Given how things like Hooks and the Property proposal above are possible without any changes to the core framework it's not clear to me what you would actually like changed.

It's orthogonal from the conversation about any proposed change itself, but I think what I've heard from @escamoteur, @rrousselGit and others both here and elsewhere is that being _in_ the framework is perceived as an important way to establish the legitimacy of a particular approach. Correct me if you disagree.

I understand that line of thinking -- since there's a lot that comes out of being in the framework (e.g. DartPad doesn't support third-party packages today, some customers are leery about how many packages they depend on after being burned with NPM, it feels more 'official', it is guaranteed to move forward with changes like null-safety).

But there are also significant costs of being included: in particular, it ossifies an approach and an API. That's why we both hold a very high bar to what we add, particularly when there's not unanimous agreement (c.f. state management), where there's the likelihood of evolution, or where we can as easily add something as a package.

I wonder if we need to document our package-first philosophy, but again, _where_ it goes is separate from a discussion about _what_ we might want to change to improve state logic reuse.

Our package policy is documented here: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#deciding-where-to-put-code

I fully understand the package-first approach and agree that it is an important thing.
But I also believe that some problems need to be solved in the core, not by packages.

That is why I am not arguing that provider should be merged in Flutter, but also believe that this issue describes a problem that Flutter should solve natively (not necessarily with hooks of course).

With Provider, Flutter ships a built-in primitive to solve this sort of problem: InheritedWidgets.
Provider only adds an opinionated layer on the top to make it ""nicer"".

Hooks are different. They _are_ the primitive. They are an unopinionated low-level solution to a specific problem: Reusing logic across multiple states.
They aren't the final product, but something that people are expected to use to build custom packages (like I did with hooks_riverpod)

It would be helpful for me (in terms of understanding the desires here, and the needs that hooks meets and so on) if someone could provide a detailed review of how the Property approach I doodled above above compares to hooks. (My goal with the Property idea is very much to layer opinion on top of the framework to solve the problem of how to reuse logic across multiple states.)

I think the Property proposal fails to solve a key goal of this issue: State logic should not care about where the parameters are coming from and in which situation they are updating.

This proposal increases readability to some extent by regrouping all the logic in one place; but it fails to solve the reusability issue

More specifically, we cannot extract:

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

out of _ExampleState and reuse it in a different widget, as the logic is bound to Example and initState+didUpdateWidget

What would it look like with hooks?

I agree with @timsneath after having seen something similar in the Rust community. It is very difficult to extract something out of the core once it's out in. The BLoC pattern was specified before provider came along but now provider is the recommended version. Perhaps flutter_hooks can be the "blessed" version in the same way. I say this because in the future there might be improvements over hooks that people come up with. React, having had hooks now, can't really change them or get out of them. They must support them, much as they do class components, essentially forever, since they are in the core. Therefore, I agree with the package philosophy.

The problem seems to be that adoption will be low and people will use whatever suits them. This can be solved as I say by recommending people to use flutter_hooks. This also might not be a large problem if we analogously look at how many state management solutions there are, even if many people use provider. I also have experienced some problems and "gotchas" wit hooks in other frameworks that should be addressed in order to create a superior solution to composable and reusable life cycle logic.

What would it look like with hooks?

Without using any primitive hooks shipped by React/flutter_hooks, we could have:

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

  @override
  _FetchUserState createState() => _FetchUserState();
}

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

  @override
  void initHook() {
    userFuture = fetchUser(hook.userId);
  }  

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


  @override
  User build() {
    return useFuture(userFuture);
  }
}

Then used:

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

  final int userId;

  @override
  Widget build(BuildContext context) {
    AsyncSnapshot<User> user = use(FetchUser(userId));

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

In this situation, the logic is completely independent from Example and the life-cycles of a StatefulWidget.
So we could reuse it in a different widget that manages its userId differently. Maybe that other widget will be a StatefulWidget that manages its userId internally. Maybe it will obtain the userId from an InheritedWidget instead.

This syntax should make it obvious that hooks are like independent State objects with their own life-cycles.

As a side-note, one drawback of the package-first approach is: Packages authors are less likely to publish packages relying on hooks to solve problems.

For example, one common problem that Provider users faced is, they want to automatically dispose of the state of a provider when it is no longer used.
The issue is, Provider users are also quite fond of the context.watch/context.select syntax, in opposition to the verbose Consumer(builder: ...)/Selector(builder:...) syntax.
But we cannot have both this nice syntax _and_ solve the previously mentioned problem without hooks (or https://github.com/flutter/flutter/pull/33213, which was rejected).

The problem is:
Provider cannot depend on flutter_hooks to solve this problem.
Due to how popular Provider is, it would be unreasonable to depend on hooks.

So in the end, I opted for:

  • forking Provider (under the codename of Riverpod)
  • voluntarily lose the "Flutter favorite"/Google recommendation as a consequence
  • solve this problem (and some more)
  • add a dependency on hooks to offer a syntax that people who enjoy context.watch would like.

I am quite satisfied with what I came up with, as I think it brings a significant improvement over Provider (It makes InheritedWidgets compile-safe).
But the way to get there left me a bad aftertaste.

There's basically three differences as far as I can tell between the hooks version and the Property version:

  • The Hooks version is a lot more backing code
  • The Property version is a lot more boilerplate code
  • The Hooks version has the problem in build methods where if you call the hooks in the wrong order things go bad and there's not really any way to immediately see that from the code.

Is the boilerplate code really that big of a deal? I mean, you can easily reuse the Property now, the code is all in one place. So it really is _only_ a verbosity argument now.

I think a good solution should not depend on other packages knowing about it. It should not matter whether it's in the framework or not. People not using it should not be a problem. If people not using it is a problem then that, IMHO, is a red flag for the API.

I mean, you can easily reuse the Property now, the code is all in one place.

The code being in one place does not mean it is reusable.
Would you mind making a secondary widget that reuses the code currently located inside _ExampleState in a different widget?
With a twist: that new widget should manage its userID internally inside its State, such that we have:

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

If people not using it is a problem then that, IMHO, is a red flag for the API.

People not using something because it isn't official doesn't mean that the API is bad.

It is totally legitimate to not want to add extra dependencies because this is extra work to maintain (due to versioning, license, depreciation and other things).
From what I remember, Flutter has a requirement to have as few dependencies as possible.

Even with Provider itself, which is widely accepted and almost official now, I have seen people say "I prefer to use the built-in InheritedWidgets to avoid adding a dependency".

Would you mind making a secondary widget that reuses the code currently located inside _ExampleState in a different widget?

The code in question is all about getting a userId from a widget and passing it to a fetchUser method. Code for managing the userId changing locally in the same object would be different. That seems to be fine? I'm not really sure what problem you're trying to solve here.

For the record I would not use Property to do what you describe, it would just look like:

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

  @override
  _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...

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

People not using something because it isn't official doesn't mean that the API is bad.

Agreed.

The fact that people don't use something itself being bad is what means the API is bad. When you say "Packages authors are less likely to publish packages relying on hooks to solve problems", that indicates that hooks depends on other people using it to be useful to you. A good API, IMHO, does not become bad if nobody else adopts it; it should hold up even if nobody else knows about it. For example, the Property example above does not depend on other packages using it to itself be useful.

Even with Provider itself, which is widely accepted and almost official now, I have seen people say "I prefer to use the built-in InheritedWidgets to avoid adding a dependency".

What's wrong with people preferring to use InheritedWidget? I don't want to force a solution on people. They should use what they want to use. You're literally describing a non-problem. The solution to people preferring to use InheritedWidget is to get out of their way and let them use InheritedWidget.

. A good API, IMHO, does not become bad if nobody else adopts it; it should hold up even if nobody else knows about it. For example, the Property example above does not depend on other packages using it to itself be useful.

There is a misunderstanding.

The problem is not about people not using hooks in general.
It's about Provider not being able to use hooks to fix problems because hooks are not official whereas Provider is.


Code for managing the userId changing locally in the same object would be different. That seems to be fine? I'm not really sure what problem you're trying to solve here.

For the record I would not use Property to do what you describe, it would just look like:

This doesn't answer the question. I asked this specifically to compare code reusability between hooks vs Property.

With hooks, we could reuse 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));
  }
}

With hooks, we could reuse FetchUser:

I don't understand why this is desirable. FetchUser does not have any interesting code, it's just an adapter from Hooks to the fetchUser function. Why not just call fetchUser directly? The code that you're reusing isn't interesting code.

It's about Provider not being able to use hooks to fix problems because hooks are not official whereas Provider is.

IMHO a good solution to the code reuse problem would not need to be adopted by Provider at all. They would be entirely orthogonal concepts. This is something the Flutter style guide talks about under the heading "avoid complecting".

I don't understand why this is desirable. FetchUser does not have any interesting code, it's just an adapter from Hooks to the fetchUser function. Why not just call fetchUser directly? The code that you're reusing isn't interesting code.

It doesn't matter. We are trying to demonstrate code-reusability. fetchUser could be anything – including ChangeNotifier.addListener for example.

We could have an alternate implementation that does not depend on fetchUser, and simply provide an API to do implicit data-fetching:

int userId;

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

IMHO a good solution to the code reuse problem would not need to be adopted by Provider at all. They would be entirely orthogonal concepts. This is something the Flutter style guide talks about under the heading "avoid complecting".

That's why I mentioned that hooks are a primitive

As a metaphor:
package:animations depends on Animation. But that is not a problem, because this is core primitive.
It would be a different matter if instead package:animations was using a fork of Animation maintained by the community

@escamoteur Just so I understand, are you suggesting that we fundamentally change how widgets work? Or are you suggesting that there should be some specific new abilities? Given how things like Hooks and the Property proposal above are possible without any changes to the core framework it's not clear to me what you would actually like changed.

@Hixie no my point is that if hooks gets even more popular we should think of including them in the framework and teach them to all so that we keep a common understanding how Flutter code looks and behaves like.
I very much share your concerns but on the other side a Widget with hooks looks really elegant.
It wouldn't prohibit to do things as before.

It wouldn't prohibit to do things as before.

I think it will, I don't think it will be a good idea for the Flutter team to say "hey we now recommend flutter hooks but you still can do things as before" people will get confused about this. Also if Flutter team recommends hooks in the future then they also will need to stop publishing the actual flutter code as examples.

People always follow the "official way" of doing things and I thing there should not have two official ways of using Flutter.

It doesn't matter. We are trying to demonstrate code-reusability. fetchUser could be anything – including ChangeNotifier.addListener for example.

Sure. That's what functions are good for: abstracting out code. But we already have functions. The Property code above, and the _setUserId code above, shows that you can bring all the code that calls those functions to one place without needing any particular help from the framework. Why do we need Hooks to wrap the calls to those functions?

IMHO a good solution to the code reuse problem would not need to be adopted by Provider at all. They would be entirely orthogonal concepts. This is something the Flutter style guide talks about under the heading "avoid complecting".

That's why I mentioned that hooks are a primitive

They are a convenience, they're not a primitive. If they were a primitive, the question "what is the problem" would be much easier to answer. You'd say "here is a thing I want to do and I can't do it".

As a metaphor:
package:animations depends on Animation. But that is not a problem, because this is core primitive.
It would be a different matter if instead package:animations was using a fork of Animation maintained by the community

The Animation class hierarchy does something fundamental: it introduces tickers and a way to control them and subscribe to them. Without the Animation class hierarchy, you have to invent something like the Animation class hierarchy to do animations. (Ideally something better. It's not our best work.) Hooks doesn't introduce a new fundamental feature. It just provides a way to write the same code differently. It might be that that code is simpler, or factored differently than it would otherwise be, but it's not a primitive. You don't need a Hooks-like framework to write code that does the same thing that Hooks-using code does.


Fundamentally, I don't think the problem described in this issue is something the framework needs to fix. Different people will have very different needs for how to address it. There's lots of ways to fix it, we've discussed several in this bug already; some of the ways are very simple and can be written in a few minutes, so it's hardly a problem so difficult to solve that it provides value for us to own and maintain the solution. Each of the proposals has strengths and weaknesses; the weaknesses are in each case things that would be blockers for someone to use them. It's not even really clear that the everyone agrees that the problem needs fixing at all.

Hooks _are_ primitives
Here's a thread from Dan: https://twitter.com/dan_abramov/status/1093698629708251136 explaining this. Some wordings differ, but the logic mostly applies to Flutter due to the similarity between React class Components and Flutter StatefulWidgets

More specifically, you could think of flutter_hooks as dynamic State mixins.

If they were a primitive, the question "what is the problem" would be much easier to answer. You'd say "here is a thing I want to do and I can't do it".

It is in the OP:

It is difficult to reuse State logic. We either end up with a complex and deeply nested build method or have to copy-paste the logic across multiple widgets.
It is neither possible to reuse such logic through mixins nor functions.

It might be that that code is simpler, or factored differently than it would otherwise be, but it's not a primitive. You don't need a Hooks-like framework to write code that does the same thing that Hooks-using code does.

You don't need classes to write a program. But classes allows you to structure your code and factorize it in a meaningful way.
And classes are primitives.

Same thing with mixins, which are primitives too

Hooks are the same thing.

Why do we need Hooks to wrap the calls to those functions?

For when we need to call this logic not in _one_ place but in _two_ places.

It is neither possible to reuse such logic through mixins nor functions.

Please give me a concrete example where this is the case. So far all the examples we've studied have been simple without hooks.

So far in this thread I haven't seen any other solution than @rrousselGit hooks that solve and make it easy to reuse and compose state logic.

Granted I haven't been doing much dart and flutter lately so I might be missing things in the property solution code samples above but, are there any solutions? What are the options today that doesn't require copy paste instead of reuse?
What are the answer to @rrousselGit question:

Would you mind making a secondary widget that reuses the code currently located inside _ExampleState in a different widget?
With a twist: that new widget should manage its userID internally inside its State

If it's not possible to reuse such an easy state logic with the property solution above what are the other options?
Is the answer simply that it shouldn't be easy reusable in flutter? Which is totally fine but a bit sad IMHO.

BTW, Does SwiftUI do it in a new/other inspiring way? Or are they lacking the same state logic reusability aswell? Haven't used swiftui at all myself. Maybe it just too different?

All Builders, basically. Builders are the only way to reuse state at the moment.
Hooks makes Builders more readable and easier to create


Here's a collection of custom hooks me or some clients made last month for different projects:

  • useQuery – which is an equivalent of the ImplicitFetcher hook I gave previously but makes a GraphQL query instead.
  • useOnResume which gives a callback to perform custom action on AppLifecycleState.resumed without having to
    go to the trouble of making a WidgetsBindingObserver
  • useDebouncedListener which listens to a listenable (usually TextField or ScrollController), but with a debounce on the listener
  • useAppLinkService which allows widgets to perform some logic on a custom event similar to AppLifecycleState.resumed but with business rules
  • useShrinkUIForKeyboard for smoothly handling the keyboard appearance. It returns a boolean that indicates whether the UI should adapt to the bottom padding or not (which is based on listening to a focusNode)
  • useFilter, which combines useDebouncedListener and useState (a primitive hook which declares a single property) to expose a filter for a search bar.
  • useImplicitlyAnimated<Int/Double/Color/...> – equivalent to TweenAnimationBuilder as a hook

Apps also use many low-level hooks for different logic.

For example, instead of:

Whatever whatever;

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

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

They do:

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

This avoids duplicate between initState/didUpdateWidget/didChangeDependencies.

They also use a lot of useProvider, from Riverpod which would otherwise have to be a StreamBuilder/ValueListenableBuilder


The important part is, widgets rarely use "just one hook".
For example, a widget may do

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

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

It's concise and very readable (assuming you have a basic knowledge of the API of course).
All the logic can be read from top to bottom – there is no jump from between methods to understand the code.
And all hooks used here are reused in multiple places in the codebase

If we were to do the exact same thing without hooks, we would have:

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

  @override
  _ChatScreenState createState() => _ChatScreenState();
}

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

  @override
  void dispose() {
    timer?.cancel();
    super.dispose();
  }

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

This is significantly less readable.

  • We have 10 levels of indentation – 12 if we make a FilterBuilder to reuse the filter logic
  • The filter logic is not reusable as it stands.

    • we may forget to cancel the timer by mistake

  • half of the build method is not useful for the reader. The Builders distracts us from what matters
  • I lost a good 5 minutes trying to understand why the code doesn't compile because of a missing parenthesis

As a user of flutter_hooks myself, I'll contribute my opinion. Before using hooks I was happy with Flutter. I didn't see the need for something like it. After reading about it and watching a youtube video about it, I still wasn't convinced, it looked cool, but I needed some practice or examples to really motivate it. But then I noticed something. I was avoiding stateful widgets at all cost, there was just a lot of boilerplate involved, and skipping around the class trying to find things. Because of that I had moved most of my ephemeral state into a state management solution along with the rest of the app state, and just used stateless widgets. However, this causes the business logic to depend on Flutter quickly because of reliance on getting the Navigator, or BuildContext for accessing InheritedWidgets / Providers higher in the tree. Not saying it was a good state management approach, I know it was not. But I did anything I could do to not to have to worry about state management in the UI.

After using hooks for a little while I found myself a lot more productive, a lot more happy using Flutter, putting ephemeral state in the right place (along with the UI) rather than with the app state.

To me, it is like a garbage collector for ephemeral state / controllers. I don't have to remember to dispose of all the subscriptions in the UI, although I'm still very conscious of the fact that this is what flutter_hooks does for me. It also makes it a ton easier to maintain & refactor my code. Speaking from writing ~10 apps in the past year for my graduate research and fun.

Like others, I don't know exactly what the main motivation should be to include it in the Flutter SDK itself. However, here are two thoughts on that subject.

  1. Occasionally I'll make a hook to make it easier to use a package that has controllers that need to be initialized / disposed. (For example golden_layout, or zefyr). I believe that the other users using flutter_hooks would benefit from such a package. However, I can't seem to justify publishing a package that literally contains 1-3 functions. The alternative would be to create a kitchen-sink package that contains a lot of hooks for various packages that I use, I can then just use a git dependency, but then anyone using those other packages + flutter_hooks would have to depend on my git in order to benefit (which is less discoverable, and likely contains dependencies on packages they don't care about), or on a package that contains 3 functions or I publish a garden-sink package to pub.dev. All ideas seem ridiculous, and not very discoverable. The other users of flutter_hooks could easily copy and paste those functions into their code or try to figure out the logic themselves, but that totally misses the point of sharing code / packages. The functions would much better go into the original packages, and not in some 'extension package'. If flutter_hooks was part of the framework, or even just a package used or exported from the framework like characters, then the authors of the original package would much more likely accept a pull request for simple hook functions, and we won't have a mess of 1-3 function packages.
    If flutter_hooks is not adopted by Flutter I foresee a bunch of 1-3 function packages cluttering up the pub.dev search results. The fact that these packages would be really small makes me really agree with @rrousselGit that this is a primitive. If the 1228 stars on the flutter_hooks repository isn't any indication of it solving the problems mentioned by @rrousselGit I don't know what is.

  2. I was watching a youtube video about contributing to the Flutter repo since I've been interested in seeing what I could do to help out. As I was watching, the person creating the video added in the new property fairly easily but very nearly forgot to take care of updating dispose, didUpdateWidget, and debugFillProperties. Seeing all of the complexities of a stateful widget again, and how easy it is to miss something made me distrust them again, and made me not as excited about contributing to the main Flutter repository. Not saying that it completely deterred me, I'm still interested in contributing, but it feels like I would be creating boilerplate code that is hard to maintain & review. It is not about the complexity of writing the code, but the complexity of reading the code and verifying that you've properly disposed and taken care of ephemeral state.

Sorry for the long-winded response, however, I've been looking at this issue from time to time, and am somewhat baffled by the response from the Flutter team. It seems like you have not taken the time to try an app both ways, and see the difference for yourself. I understand the desire to not maintain an additional dependency or integrate it too much into the framework. However, the core part of the flutter_hook framework is only 500ish lines of pretty well documented code. Again, sorry if this is tangential to the conversation & I hope I'm not offending anyone for giving my 2 cents and speaking up. I didn't speak up earlier because I felt @rrousselGit was making very good points and being clear.

Sorry for the long-winded response, however, I've been looking at this issue from time to time, and am somewhat baffled by the response from the Flutter team. It seems like you have not taken the time to try an app both ways, and see the difference for yourself.

To be fair, this is an incredibly long thread and the founder of the framework has actively contributed several times daily, with several solutions, requested feedback on them, and engaged with them as well as worked to understand what's being requested. I honestly struggle to think of a clearer example of a maintainer being helpful.

I wish this was slightly more patience with this issue - I don't understand hooks any more deeply after reading through this thread, other than they're another way of tying lifetime of Disposables to a State. I don't prefer that approach stylistically, and I feel there's something fundamentally flawed if the position is 'just take the time to write a whole new app in the paradigm, then you'll understand why it needs to be shoehorned into the framework!' - as the React engineer noted in this thread, it really wouldn't be advisable for Flutter, and benefits described in this thread are small compared to the cost of the kind of rewiring that means you need a whole new codebase to see the benefit.

I honestly struggle to think of a clearer example of a maintainer being helpful.

Agreed. I am grateful for Hixie to take the time to participate in this discussion.

i don't understand hooks any more deeply after reading through this thread

To be fair this issue is explicitly trying to avoid talking about hooks specifically.
This is more about trying to explain the problem rather than the solution

Do you feel that it fails to do that?

I can feel both sides (@rrousselGit and @Hixie) here and wanted to leave some feedback from a (my) usage side / perspective of the Flutter framework.

The flutter_hooks approach does reduce the boilerplate quite a lot (just from the examples shown here since we can reuse such state configurations) and reduces complexity by not having to actively think about initialising / disposing ressources. Generally speaking it does a good job improving and supporting the development flow / speed... even though it does not fit in so nicely to the "core" of Flutter itself (subjectively).

Like at least >95% of the code I write results in the build method to be declarative only, no local variables or calls outside the returned widget subtree, all the logic part is inside those state functions to initialise, assign and dispose ressources and add listeners (in my case MobX reactions) and such logical stuff. Since this is also the approach for the most part in Flutter itself, it feels very native. Doing so also gives you the developer the opportunity to always be explicit and open about what you do - it does force me to always convert such widgets to be a StatefulWidget and writing similar code in initState / dispose, but it also always results in writing down exactly what you intend to do directly in the Widget its being used. For me personally, like @Hixie already mentioned himself, it does not bother me in any way writing this kind of boilerplate code and allows me as the developer to decide how to handle it right on instead of relying on something like flutter_hooks to do it for me and resulting in not understanding why something might behave like it does. Extracting widgets in small bits also ensures that those kind of boilerplate is spot on the use case it's being used for. With flutter_hooks I would still need to think about what kind of states are worth being written to be a hook and therefore reused - different flavours might either result in various "single" use hooks or no hooks at all since I might not reuse configurations too often but tend to write more custom ones.

Don't get me wrong, the approach in such hooks seems very nice and useful, but for me it feels like a very fundamental concept which changes the core concept of how to handle this. It feels very good as a package itself to give devs the opportunity to use this kind of approach if they are not happy with how to do it "natively", but making it part of the Flutter framework itself would, at least to be clean / unified, result in either rewriting big parts of Flutter to make use of this concept (a lot of work) or use it for future / selected stuff (which might be confusing to have such mixed approaches).

If it would be integrated into the Flutter framework itself and supported / actively used, I would obviously hop into this. Since I understand and even like the current approach and see the (possible) actions needed to implement this natively, I can understand the hesitation and / or why it should not be done and rather keep it as a package.

Correct me if I'm wrong but this thread is about the problems of reusing state logic in multiple widgets in a readable and composable way. Not hooks specifically. I believe this thread was opened because of wishes to have a discussion around the problem with an open approach to what the solution should be.

Hooks are mentioned though since they are one solution and I believe @rrousselGit has been using them here to try and explain the issue/problem that they solve (since they are a solution) so that another solution perhaps more native to flutter could be come up with and presented. So far to my knowledge there hasn't been any other solutions presented in this thread though that solves the reusability issues?

With that said, I don't know where the thread is going at the moment.
I think the issue really exists. Or are we debating this?
If we are all agreeing that it's hard to reuse state logic in a composable way in multiple widgets with the core of flutter today, what solutions are there that could be a core solution? since builders really are (to quote)

significantly less readable

The property solution doesn't seem to be so easily reusable or is that a wrong conclusion I've drawn(?) since there was no answer on how to use it to:

making a secondary widget that reuses the code currently located inside _ExampleState in a different widget?
With a twist: that new widget should manage its userID internally inside its State

I'd be willing to help with a design document as @timsneath suggested. I think it is probably a better format to explain the problem with a few case study examples, as well as mention the different solutions and explore if we can find a solution that fits flutter and where it is at. I agree that the discussion in the issue is getting a little lost.

I'm quite skeptical about the idea of making a design document at the moment.
It is evident that for now, @Hixie is against solving this problem inside Flutter directly.

To me, it looks like we are disagreeing on the importance of the problem and the role of Google in solving that problem.
If both sides don't agree on this, I don't see how we can have a productive discussion on how to tackle this problem – whatever the solution might be.

This issue thread was a very interesting read and I am happy to see that the exchange of viewpoints has remained civil. I am however a bit surprised at the current impasse.

When it comes to hooks, my view is that while Flutter does not necessarily need the specific hooks solution presented by @rrousselGit, nor is he saying that. Flutter does need a solution that delivers similar benefits as hooks does, for exactly all the reasons Remi and other proponents mention. @emanuel-lundman summarized the arguments well above and I agree with his views.

In lack of any other viable proposals offering the same capabilities and given the fact that hooks have a well proven track record in React, and that there is an existing solution that it could be based on for Flutter, I don’t think it would be a bad choice to do so. I do not think the hooks concept, as a primitive that is also included in the Flutter SDK (or even lower), would take anything away from Flutter. In my opinion it would only enrich it and make it even easier to develop maintainable and delightful apps with Flutter.

While the argument that hooks is available as a package for those that want to reap its benefits, is a valid point, I feel that it is not optimal for a primitive like hooks. Here is why.

Very often when making even internally reusable packages, we debate if the package needs to be “pure”, in the sense that it may only depend on the Dart+Flutter SDK, or if we allow some other packages in it and if so, which ones. Even Provider is out for “pure” packages, but often allowed in for higher-level packages. For an app there is always also the same debate, which packages are OK and which are not. Provider is green, but something like Hooks is still a question mark as a package.

If a hooks like solution would be a part of the SDK, it would be an obvious choice to use the capabilities it offers. While I want to use Hooks and allow it in already now as a package, I’m also concerned that it creates a Flutter code style and introduces concepts that might not be familiar to Flutter devs not using it. It feels a bit like a fork in the road if we go down this path without support in the SDK. For smaller personal projects, it is an easy choice to use Hooks. I recommend trying it together with Riverpod.

(Our package conservatism comes I guess from being burned by packages and dependency mess on other package managers in the past, probably not unique.)

I am not saying that hooks would be the only way to solve the current problem, even if it is the only working demonstrated solution so far. It could certainly be interesting and a valid approach to investigate options on a more generic level before committing to a solution. For that to happen there needs to be a recognition of that Flutter SDK _currently has a flaw when it comes to easy re-usable state logic_, which there despite elaborate explanations currently does not seem to be.

For me there are two main reasons to not just put Hooks into the core framework. The first is that the API has dangerous traps in it. Primarily, if you somehow end up calling the hooks in the wrong order then things will break. This seems like a fatal problem to me. I understand that with discipline and following the documentation you can avoid it but IMHO a good solution to this code reuse problem would not have that flaw.

The second is that there really should be no reason people can't just use Hooks (or another library) to solve this problem. Now, with Hooks specifically that doesn't work, as people have discussed, because writing the hook is burdensome enough that people wish that unrelated libraries would support hooks. But I think a good solution to this problem wouldn't need that. A good solution would stand alone and not need every other library to know about it.

We recently added RestorableProperties to the framework. It would be interesting to see if they could be leveraged here somehow...

I agree @Hixie on the API having hidden problems that require an analyzer or linter to solve. I think we, as in whoever wants to participate, should look into various solutions, maybe via the design doc suggestion, or otherwise, on the problem of reusable life cycle management. Ideally it would be more Flutter-specific and leverage Flutter APIs while also solving the problems that the hook API does. I think the Vue version is a good model to start from, as I mentioned before, as it doesn't rely on hook call order. Is anyone else interested in investigating with me?

@Hixie but you do agree with the issue then that there ain't a good way to reuse state logic in a composable way between widgets? That's why you started to think about leveraging ResuableProperties somehow?

For me there are two main reasons to not just put Hooks into the core framework. The first is that the API has dangerous traps in it. Primarily, if you somehow end up calling the hooks in the wrong order then things will break. This seems like a fatal problem to me. I understand that with discipline and following the documentation you can avoid it but IMHO a good solution to this code reuse problem would not have that flaw.

From having worked with hooks and worked with other people that use hooks this really ain't such a big issue IMHO. And not at all compared to all the big gains (big gains in dev speed, reusability, composability and easily readable code) they bring to the table.
A hook is a hook, like a class is a class, not just a function, and you can't use it conditionally. You learn that fast. And your editor can help with this issue as well.

The second is that there really should be no reason people can't just use Hooks (or another library) to solve this problem. Now, with Hooks specifically that doesn't work, as people have discussed, because writing the hook is burdensome enough that people wish that unrelated libraries would support hooks. But I think a good solution to this problem wouldn't need that. A good solution would stand alone and not need every other library to know about it.

Writing hooks aren't burdensome.
Its still easier than the solutions available now IMHO (to use that phrase again 😉).
Maybe I'm misinterpretating what you're writing. But I don't think anyone has said that?
I read it like people really appreciate all the gains the hook solution brings to the table and wish they could use it everywhere. To reap all the benefits. Since a hook is reusable it would be great if third party developers could feel confident to code and ship their own hooks without requiring everyone to write their own wrappers. Reap the benifit of the reusability of the state logic.
I think @rrousselGit and @gaearon have explained the primitive thing already. So I won't get into that.
Maybe I don't get this statement because I can't see that it's a good summary of what people have written in this thread. I'm sorry.

Hope there is a way forward. But I think it's about time to at least agree this is an issue and either go forward coming up with alternate solutions that are better since hooks doesn't seem to be even on the table.
Or just decide to skip fixing the issue in flutter core.

Who decides path forward?
What's the next step?

This seems like a fatal problem to me. I understand that with discipline and following the documentation you can avoid it but IMHO a good solution to this code reuse problem would not have that flaw.

In React, we solve this with a linter — static analysis. In our experience this flaw has not been important even in a large codebase. There are other problems that we might consider flaws, but I just wanted to point out that while the reliance on persistent call order is what people intuitively think will be a problem, the balance ends up pretty different in practice.

The real reason I'm writing this comment is that Flutter uses a compiled language though. "Linting" is not optional. So, if there is an alignment between the host language and the UI framework, it is definitely possible to enforce that "conditional" problem never comes up statically. But that only works when the UI framework can motivate language changes (e.g. Compose + Kotlin).

@Hixie but you do agree with the issue then that there ain't a good way to reuse state logic in a composable way between widgets? That's why you started to think about leveraging ResuableProperties somehow?

It's certainly something people have brought up. It's not something I have a visceral experience with. It's not something I've felt was a problem when writing my own apps with Flutter. That doesn't mean that it's not a real problem for some people, though.

Since a hook is reusable it would be great if third party developers could feel confident to code and ship their own hooks without requiring everyone to write their own wrappers

My point is that a good solution here wouldn't require anyone to write wrappers.

What's the next step?

There are many next steps, for example:

  • If there are specific problems with Flutter that we haven't talked about here, file issues and describe the problems.
  • If you have a good idea for how to solve this issue's problem in ways that are better than Hooks, create a package that does so.
  • If there are things that can be done to improve Hooks, do so.
  • If there are problems with Flutter that prevent Hooks from reaching its full potential, file those as new issues.
    etc.

This issue thread was a very interesting read and I am happy to see that the exchange of viewpoints has remained civil.

I'd hate to see what an uncivil thread looks like then. There's so very little empathy in this thread that it's been hard to read and follow from the sidelines

My point is that a good solution here wouldn't require anyone to write wrappers.

You don't have to write wrappers though. But you may want to reap the benefits and reusability stuff in your own code that you've grown acustomed to. You sure could still use the libraries as is. If you do write a hook wrapping stuff (if possible) it's probably not because you think it's burden but that it's better than the alternative.

That's actually a good reason and a reason mentioned why a solution to the issue in this thread would be great in core. A reusable composable state logic solution in core would mean that people wouldn't have to write wrappers since such reusable logic safely could be shipped in all packages without adding dependencies.

A reusable composable state logic solution in core would mean that people wouldn't have to write wrappers since such reusable logic safely could be shipped in all packages without adding dependencies.

The point I'm trying to make is that IMHO a good solution wouldn't require _anyone_ to write that logic. There just wouldn't be any redundant logic to reuse. For example, looking at the "fetchUser" example from earlier, nobody would have to write a hook, or its equivalent, to call the "fetchUser" function, you would just call the "fetchUser" function directly. Similarly "fetchUser" wouldn't need to know anything about hooks (or whatever we use) and hooks (or whatever we use) wouldn't need to know anything about "fetchUser". All while keeping the logic that you do write trivial, like it is with hooks.

The current restrictions are caused by the fact that hooks are a patch on the top of language limitations.

In some language, hooks are a language construct, such as:

state count = 0;

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

This would be a variant of async/sync functions, which can perserve some state across calls.

It doesn't require a non-conditional usage anymore since, as part of the language, we can differentiate each variable by their line number rather than their type.

I would add that the hooks limitations are similar to the --track-widget-creation limitations.

This flag breaks the const constructor canonalisation for widgets. But that is not a problem as widgets are declarative.

In that sense, hooks are the same. The limitations don't really matter, as they are manipulated declaratively.
We won't obtain one very specific hook without reading the others.

Maybe the fetchuser example isn't the ideal one.
But the useStream, useAnimstion or useStreamCintroller make the Widget Tree much cleaner and prevents you from forgetting dispose or to take care of dudChangeDependencues.
Therefore the current way has its on traps you can get caught in. So I guess the potential problem with the call sequence isn't a bigger one as those.
I m not sure if I would start writing my own hooks, but having a collection of often needed ready to be use inside the framework would be nice.
It would just be an alternative way to deal with them.

@Hixie, really sorry for not being able to grasp what you're trying to describe, I blame that it's late in the evening here but probably it's just me 😳.. But in the good solution you describe, where would the state values, the state business logic and lifetime event logic that the solution to the problem wraps/encapsulates to easily be composable and shared between widgets reside? Could you elaborate a bit on what a good solution does and how you see it would ideally work?

Just interjecting here a bit, seeing that there's mentions about civility of this discussion. I don't personally feel that anyone here is being uncivil.

That said, I think it's worth noting that this is a topic that people deeply care about, on all sides.

  • @rrousselGit has been answering beginner questions about state management on StackOverflow and on the package:provider issue tracker for years now. I'm following only the latter of these firehoses, and I have nothing but respect for Remi's diligence and patience.
  • @Hixie and others on the Flutter team care deeply about the API of Flutter, its stability, surface, maintainability, and readability. It's thanks to this that Flutter's developer experience is where it is today.
  • Flutter developers care deeply about state management, because that's what they're doing a significant portion of their development time.

It's clear that all parties in this discussion have a good reason to argue for what they do. It's also understandable that it takes time to get the messages across.

So, I'd be happy if the discussion continued, either here or in some other form. If I can help in any way, like with a formal doc, just let me know.

If, on the other hand, people think the discussion here is getting out of hand, then let's pause and see if there's a better way to communicate.

(Separately, I want to give a shout out to @gaearon for joining this discussion. React's experience in this respect is invaluable.)

@emanuel-lundman

But in the good solution you describe, where would the state values, the state business logic and lifetime event logic that the solution to the problem wraps/encapsulates to easily be composable and shared between widgets reside? Could you elaborate a bit on what a good solution does and how you see it would ideally work?

Unfortunately I can't elaborate because I don't know. :-)

@escamoteur

Maybe the fetchuser example isn't the ideal one.
But the useStream, useAnimstion or useStreamCintroller make the Widget Tree much cleaner and prevents you from forgetting dispose or to take care of dudChangeDependencues.

One of the difficulties in this issue has been a "moving of the goalposts" where a problem is described, then when it's analyzed, the problem is dismissed as not the real problem and a new problem is described, and so on. What might be really useful would be to come up with some canonical examples, e.g. a demo Flutter application that has a real example of the problem, one that isn't overly simplified for the sake of exposition. Then we could reimplement that using Hooks, and other proposals, and really evaluate them against each other in concrete terms. (I would do this myself except I don't really understand exactly what the problem is, so it's probably best if someone who advocates for Hooks would do it.)

What might be really useful would be to come up with some canonical examples, e.g. a demo Flutter application that has a real example of the problem, one that isn't overly simplified for the sake of exposition

What do you think about the example I gave here? https://github.com/flutter/flutter/issues/51752#issuecomment-669626522

This is a real-world code-snippet.

I think that'd be a great start. Can we get it into a state where it runs as a stand-alone app, with a version that doesn't use hooks and a version that does?

Sorry, I meant as a code-snippet, not as an app.

I think one of the issue with the "demo Flutter application" idea is, the examples made in this thread are very much real.
They aren't oversimplified.
The primary use-case of hooks if to factorize micro states, like debounce, event handlers, subscriptions, or implicit side-effects – which are combined together to achieve more useful logics.

I have some examples on Riverpod, such as https://marvel.riverpod.dev/#/ where the source code is here: https://github.com/rrousselGit/river_pod/tree/master/examples/marvel/lib
But that isn't going to be very different from what was mentioned until now.

@Hixie

I really have trouble understanding why this is a problem. I've written plenty of Flutter applications but it really doesn't seem like that much of an issue? Even in the worst case, it's four lines to declare a property, initialize it, dispose it, and report it to the debug data (and really it's usually fewer, because you can usually declare it on the same line you initialize it, apps generally don't need to worry about adding state to the debug properties, and many of these objects don't have state that needs disposing).

I'm in the same boat.
I admit, I don't really understand the issues described here either. I don't even understand what is the "state logic" people refer to, that needs to be reusable.

I have many stateful form widgets, some that have tens of form fields, and I do have to manage the textcontrollers and focusnodes myself. I create and dispose them in the statelesswidget's lifecycle methods. While it is quite tedious, I don't have any widget that uses the same amount of controllers/focusNodes or for the same use case. The only common thing between them is the general concept of being stateful and being a form. Just because it's a pattern it doesn't mean the code is repeated.
I mean, in a lot of parts of my code I have to loop through arrays, I wouldn't call doing "for(var thing in things)" throughout my app repeating code.

I love the power of the StatefulWidget which comes from the simplicity of its lifecycle api. It allows me to write StatefulWidgets that do one thing and do it in isolation from the rest of the app. The "state" of my widgets is always private to themselves, so reusing my widgets is not an issue, neither is code reuse.

I have a couple of issues with the examples brought up here, which are somewhat in line with your points:

  • creating multiple stateful widgets with the exact same "state logic" seems just wrong and counter to the idea of having widgets self-contained. But again, I'm confused as to what people mean by common "state logic".
  • hooks don't seem to do anything that I can't already do using plain dart and basic programming concepts (such as functions)
  • the issues seem to be related or caused by a certain style of programming, a style that seems to favor "reusable global state".
  • abstracting away a couple of lines of code smells of "premature code optimization"and adds complexity in order to solve an issue that has little to nothing to do with the framework and everything to do with how people use it.

This is significantly less readable.

  • We have 10 levels of indentation – 12 if we make a FilterBuilder to reuse the filter logic
  • The filter logic is not reusable as it stands.

    • we may forget to cancel the timer by mistake
  • half of the build method is not useful for the reader. The Builders distracts us from what matters
  • I lost a good 5 minutes trying to understand why the code doesn't compile because of a missing parenthesis

Your example is more of a showcase of how verbose Provider is and why abusing InheritedWidgets for everything is a bad thing, rather than any real issue with Flutter's StatelessWidget and State lifecycle api.

@rrousselGit Sorry if I wasn't clear. The suggestion I was making above was specifically to create vanilla Flutter applications (using StatefulWidget etc) that are realistic and show the problems you're describing, so that we can then make proposals based on a real full application. The concrete examples we've discussed here, such as the "fetchUser" example, have always ended up with a discussion along the lines of "well you could handle that case like this and it would be simple and wouldn't need Hooks" followed by "well, that is oversimplified, in the real world you need Hooks". So my point is, let's create a real world example that really _does_ need Hooks, that isn't oversimplified, that shows the difficulty in reusing code, so that way we can see whether it is possible to avoid these problems without using any new code, or if we do need new code, and in the latter case, whether it has to be shaped like Hooks or if we can find an even better solution.

I don't even understand what is the "state logic" people refer to, that needs to be reusable.

Fair enough
A parallel to state logic would be UI logic, and what Widgets bring to the table.

We could technically remove the Widget layer. In that situation, what would be left is the RenderObjects.

For example, we could have a minimalist counter:

var counter = 0;

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

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

That's not necessarily complex. But it is error-prone. We have a duplicate on the counterLabel rendering

With widgets, we have:

class _CounterState exends State {
  int counter = 0;

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

The only thing this did is factorize the Text logic, by making it declarative.
It's a minimalist change. But over a large codebase, that is a significant simplification.

Hooks do the exact same thing.
But instead of Text, you get custom hooks for state logics. Which includes listeners, debouncing, making HTTP requests, ...


Your example is more of a showcase of how verbose Provider is and why abusing InheritedWidgets for everything is a bad thing, rather than any real issue with Flutter's StatelessWidget and State lifecycle api.

This is unrelated to provider (this code does not use provider after-all).
If anything, provider have it better because it has context.watch instead of Consumer.

The standard solution would be to replace Consumer with ValueListenableBuilder – which leads to the exact same problem.

I agree @Hixie, I do think we need two side by side comparisons to judge the effectiveness of just Flutter versus with hooks. This would also help to convince others whether hooks is better or not, or maybe another solution is even better if the vanilla app is built with this third solution. This vanilla app concept has been around for a while, with things like TodoMVC showing the difference between various front-end frameworks, so it's not necessarily new. I can help out with creating these example apps.

@satvikpendem
I'd be willing to help out.
I think the example app in the flutter_hooks repo probably showcases several different hooks and what they make easier / problems they solve, and would be a good starting place.

I also think that we could also use a few of the examples and approaches that are presented in this issue.

Update: The code is here, https://github.com/TimWhiting/local_widget_state_approaches
I'm not sure the proper name for the repository, so don't assume that that is the problem we are trying to solve. I've done the basic counter app in stateful and hooks. I don't have much time later tonight, but I'll try to add more use cases that are more illustrative of what might be the issue. Anyone who wants to contribute, please request access.

The concrete examples we've discussed here, such as the "fetchUser" example, have always ended up with a discussion along the lines of "well you could handle that case like this and it would be simple and wouldn't need Hooks" followed by "well, that is oversimplified, in the real world you need Hooks".

I disagree. I don't think I've seen such "you could handle that case like this" and agreed that the resulting code was better than the hook variant.

My point all along was that while we can do things differently, the resulting code is error-prone and/or less readable.
This applies to fetchUser too

Hooks do the exact same thing.
But instead of Text, you get custom hooks for state logics. Which includes listeners, debouncing, making HTTP requests, ...

Nope, I still don't get what this common state logic is supposed to be. I mean I have many widgets that read from a database in their "initState/didUpdateDependency" methods, but I can't find two widgets that make the exact same query therefore their "logic" is not the same.

Using the example of making HTTP request. Assuming I have a "makeHTTPRequest(url, paramters)" somewhere in my service class that some of my widgets need to use, why would I use a hook instead of just calling the method directly whenever I need it? How is using hooks different from plain method calls in this case?

Listeners. I don't have widgets listening to the same things. Each of my widgets is responsible to subscribe to whatever they need to and make sure they unsubscribe. Hooks might be syntactic sugar for most of the things, but since my widgets wouldn't listen to the same combination of objects, the hooks would have to be "parametrized" somehow. Again, how are hooks different from a plain old function?


This is unrelated to provider (this code does not use provider after-all).
If anything, provider have it better because it has context.watch instead of Consumer.

Huh? Your counterexample to what your "ChatScreen" HookWidget is supposed to solve, was this:

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

How is this unrelated to provider? I'm confused. I'm no expert in Provider, but this definitely seems like code using Provider.

I would like to insist on the fact that this issue is not about complex states.
It is about tiny increments that can be applied to the entire codebase.

If we disagree with the value of the examples given here, an application will not bring anything to the conversation – as there is nothing that we can do with hooks which we can't do with StatefulWidget.

My recommendation would be to instead compare side-by-side micro-snippets like ImplicitFetcher, and _objectively_ determine which code is better using measurable metrics, and do that for a wide variety of small snippets.


How is this unrelated to provider? I'm confused. I'm no expert in Provider, but this definitely seems like code using Provider.

This code isn't from Provider but from a different project that doesn't use InheritedWidgets.
Provider's Consumer doesn't have a provider parameter.

And as I mentioned, you could replace Consumer -> ValueListenableBuilder/StreamBuilder/BlocBuilder/Observer/...

in their "initState/didUpdateDependency" methods, but I can't find two widgets that make the exact same query therefore their "logic" is not the same.

The state logic we want to reuse is not "make a query" but "do something when x changes". The "do something" may change, but the "when x changes" is common

Concrete example:
We may want a widget to make an HTTP request whenever the ID it receives changes.
We also want to cancel the pending requests using package:async's CancelableOperation.

Now, we have two widgets wanting to do exactly the same thing, but with a different HTTP request.
In the end, we have:

CancelableOperation<User> pendingUserRequest;

initState() {
  pendingUserRequest = fetchUser(widget.userId);
}

didUpdateWidget(oldWidget) {
  if (widget.userId != oldWidget.userId) {
      pendingUserRequest.cancel();
      pendingUserRequest = fetchUser(widget.userId);
  }
}

dispose() {
  pendingUserRequest.cancel();
}

VS:

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

The only difference is that we changed fetchUser with fetchMessage. The logic is otherwise 100% the same. But we cannot reuse it, which is error-prone.

With hooks, we could factorize this into a useUnaryCancelableOperation hook.

Which means that with the same two widgets would instead do:

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

VS

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

In this scenario, all the logic related to making the request and canceling it is shared. We are left only with a meaningful difference, which is fetchUser vs fetchMessage.
We could even make a package out of this useUnaryCancelableOperation, and now everyone can reuse it in their app.

If we disagree with the value of the examples given here, an application will not bring anything to the conversation – as there is nothing that we can do with hooks which we can't do with StatefulWidget.

If that's really the case then I guess we should close this bug, because we've already discussed the examples given here and they haven't been compelling. I really would like to understand the situation better, though, and it sounded from earlier comments in this bug like the benefits are at the application level, hence my suggestion that we study examples of applications.

The only difference is that we changed fetchUser with fetchMessage. The logic is otherwise 100% the same. But we cannot reuse it, which is error-prone.

What is error prone and what is there to reuse? Implementing a whole new layer of abstraction and class hierarchy just so we don't have to implement three methods in a class and is way overkill.

Again, just because something is a common pattern it doesn't mean that you need to create a new feature for it. Besides, if you want to reduce repetitive code in this case you can just extend the StatefulWidget* class and override the initstate/didUpdateWidget methods with the common bits.

With hooks, we could factorize this into a useUnaryCancelableOperation hook.

Which means that with the same two widgets would instead do:

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

VS

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

In this scenario, all the logic related to making the request and canceling it is shared. We are left only with a meaningful difference, which is fetchUser vs fetchMessage.
We could even make a package out of this useUnaryCancelableOperation, and now everyone can reuse it in their app.

I'm sorry but that's big definite no from me. Aside from the fact that it saves only a minor amount of code redundancy, "factorizing" out a code that conceptually belongs to "initState" and "update" lifecycle methods, into the build method is a big no.

I expect my build methods to only build the layout and nothing else. Setting up and tearing down dependencies definitely doesn't belong in the build method, and I'm quite happy having to explicitly rewrite the same type of code to make it explicit for my future self and others what my widget is doing. And let's not stick everything in the build method.

If that's really the case then I guess we should close this bug

@Hixie Please don't. People care about this issue. I have talked with you on reddit about the same thing, but in context of SwiftUI: https://github.com/szotp/SwiftUI_vs_Flutter

It's not about hooks, it's about somehow improving stateful widgets. People simply hate writing boilerplate. For devs writing SwiftUI code, who are used to RAII and copy semantics of Views, manually managing disposables seems just off.

So I encourage flutter team to at least see this as a problem and think about alternative solutions / improvements.

I expect my build methods to only build the layout and nothing else. Setting up and tearing down dependencies definitely doesn't belong in the build method,
That's an important point. Build methods should be pure. Still I wished we could have the advantages without the difficulties

I really don't understand the push for more examples here. It's clear on it's face.

The problem solved by hooks is simply and obvious, it keeps the code DRY. The benefits of this are the obvious ones, less code == less bugs, maintenance is easier, less places for bugs to hide, lower line count overall increases readability, junior programmers are better insulated etc.

If you're talking a real world use case, it's an app where you're setting up and tearing down 12 animator controllers in 12 different views, every single time, leaving open the door to missing a dispose() call, or overriding some other lifecycle method. then apply that to dozens of other stateful instances, and you're easily looking at hundreds or thousands of pointless lines of code.

Flutter is full of these cases where we need to constantly repeat ourselves, setting up, and tearing down state of little objects, that create all kinds of opportunities for bugs that do not need to exist, but do, because there's currently no elegant approach to sharing this rote setup/teardown/sync logic.

You can see this issue in literally _any_ state that has a setup and teardown phase, or has some lifecycle hook that always needs to be tied into to.

Myself I find using widgets is the best approach, I rarely use AnimatorController for example cause the setup/teardown is so annoying, verbose and bug prone, instead using TweenAnimationBuilder everywhere I can. But this approach has its limitations as you get to a higher number of stateful objects in a given view, forcing nesting and verbosity where really none should be required.

@szotp I haven't... I would much rather we establish one or more baseline apps that demonstrate the problem so that we can evaluate the solutions. I would do it myself but I don't understand exactly what it is we're trying to solve so I'm the wrong person to do it.

@escamoteur Baseline apps would help us design solutions that don't have the difficulties.

@esDotDev We've discussed cases like this in this bug so far, but every time we have, solutions other than Hooks get dismissed because they don't solve some problem that wasn't included in the example that the solution addressed. Hence, simple descriptions of problems don't seem to be sufficient to capture the full extent. For example, the "12 animator controllers" case maybe could be solved by an array of animation controllers and the functional features in Dart. TweenAnimationBuilder might be another solution. Neither of those involve Hooks. But I'm sure if I suggest that, someone will point out something I've missed and say "it doesn't work because..." and bring up that (new, in the context of the example) problem. Hence the need for a baseline app that we all agree covers the full spread of the problem.

If anyone would like to drive this forward I really think the best way to do it is what I described above (https://github.com/flutter/flutter/issues/51752#issuecomment-670249755 and https://github.com/flutter/flutter/issues/51752#issuecomment-670232842). That will give us a starting point that we can all agree represents the extent of the problem we're trying to solve; then, we can design solutions that address those problems in ways that address all the desires (e.g. @rrousselGit's need for reuse, @Rudiksz's need for clean build methods, etc), and most importantly we can evaluate those solutions in the context of the baseline apps.

I think we could all fairly easily agree on he problem:
_There is no elegant way to share the setup/teardown tasks associated with things like Streams, AnimatorControllers etc. This leads to unnecessary verbosity, openings for bugs, and reduced readability._

Does anyone not agree with that? Can't we start there and move forward in search of a solution? We have to agree that is a core problem first, which it seems like we still do not.

As I write that though, I realized matches exactly the name of the issue, which is open ended and leaves room for discussion:
"Reusing state logic is either too verbose or too difficult"

To me that is a very obvious problem, and we should be quickly moving past the debate stage and into brainstorming about what would work, if not hooks then what. We need micro-states that are re-usable... I'm sure we can figure something out. It would really clean up a lot of Flutter views at the end of the day and make them more robust.

@Hixie Please don't. People care about this issue. I have talked with you on reddit about the same thing, but in context of SwiftUI: https://github.com/szotp/SwiftUI_vs_Flutter

Your SwiftUI example can be replicated in dart in a few lines of code, by simply extending the StatefulWidget class.

I have StatefulWidgets that don't subscribe to any notifiers and/or do any external calls, and in fact most of them are like that. I have about 100 custom widgets (although not all Stateful), and maybe 15 of them have any kind of "common state logic" as described by the examples here.

In the long run, writing a few lines of code (aka boilerplate) is a small tradeoff in order to avoid unnecessary overhead. And again, this issue of having to implement the initState/didUpdate methods seems way overblown. When I create a widget that uses any of the patterns described here, I maybe spend first 5-10 minutes on "implementing" the lifeCycle methods and then a few days actually writing and polishing the widget itself while never touching said lifecycle methods. The amount of time I spend on writing the so called boilerplate setup/teardown code is minuscule compared to my app code.

As I said the fact that StatefulWidgets make so few assumptions about what they are used for is what makes them so powerful and efficient.

Adding a new type of widget to Flutter that subclasses StatefulWidget (or not) for this particular use case would be fine, but let's don't bake it into the StatefulWidget itself. I have a lot of widgets that don't need the overhead that would come with a "hooks" system or microstates.

@esDotDev I agree that that is a problem some people are facing; I even proposed some solutions earlier in this issue (search for my various versions of a Property class, might be buried now since GitHub doesn't like to show all the comments). The difficulty is that those proposals were dismissed because they didn't solve specific issues (e.g. in one case, didn't handle hot reload, in another, didn't handle didUpdateWidget). So then I made more proposals, but then those were again dismissed because they didn't handle something else (I forget what). This is why it's important to have some sort of baseline we agree represents the _entire_ problem so that we can find solutions for that problem.

The goal has never changed. The criticisms made are that the solutions suggested have a lack in flexibility. None of them continue to work outside of the snippet they were implemented for.

This is why the title in this issue mentions "Difficult": Because there is currently no flexibility in the way we currently solve problems.


Another way to look at it is:

This issue is basically arguing that we need to implement a Widget layer, for State logic.
The solutions suggested are "But you can do it with RenderObjects".

_In the long run, writing a few lines of code (aka boilerplate) is a small tradeoff in order to avoid unnecessary overhead._

Couple nits with this statement:

  1. It's not really a few lines, if you take brackets, line spacing @overides, etc into acct, you can be looking at 10-15+ lines for a simple animator controller. That is non-trivial in my mind... like way beyond trivial. 3 lines to do this bugs me (in Unity it's Thing.DOTween()). 15 is ridiculous.
  1. It's not about the typing, though that is a pain. It's about the silliness of having 50 line class, where 30 lines of it is rote boilerplate. Its obfuscation. It's about the fact that if you _dont_ write the boilerplate, there's no warnings or anything, you just added a bug.
  2. I don't see any overhead worth discussing with something like Hooks. We're talking about an array of objects, with a handful of fxns on each. In Dart, which is very fast. This is a red herring imo.

@esDotDev

To me that is a very obvious problem, and we should be quickly moving past the debate stage and into brainstorming about what would work, if not hooks then what.

Extending widgets. Like the way ValueNotifier extends the ChangeNotifier to simplify a common usage pattern, everybody can write their own flavors of StatelessWidgets for their specific use cases.

Yes I agree that is an effective approach, but it does leave something to be desired. If I have 1 animator, then I can just use a TweenAnimationBuilder. Cool, it's still like 5 lines of code, but whatever. it works... not TOO bad. But if I have 2 or 3? Now I'm in nesting hell, if I have a cpl other stateful objects for some reason, well it's all kinda become a mess of indentation, or I'm creating some very specific widget that encapsulates a random collection of setup, update and teardown logic.

Extending widgets. Like the way ValueNotifier extends the ChangeNotifier to simplify a common usage pattern, everybody can write their own flavors of StatelessWidgets for their specific use cases.

You can extend only one base-class at a time. That does not scale

Mixins are the next logical attempt. But as the OP mentions, they do not scale either.

@esDotDev

or I'm creating some very specific widget that encapsulates a random collection of setup, update and teardown logic.

A kind of widget that has to set up 3-4 kind of AnimationControllers does sound a like a very specific use case and supporting random collection of setup/teardown logic definitely shouldn't be part of a framework. In fact that's why the initState/didUpdateWidget methods are exposed int he first place, so you can do your random collection of setup to your heart's desire.

My longest initState method is 5 lines of code, my widgets don't suffer of excessive nesting, so we definitely have different needs and use cases. Or a different development style.

@esDotDev

3. I don't see any overhead worth discussing with something like Hooks. We're talking about an array of objects, with a handful of fxns on each. In Dart, which is very fast. This is a red herring imo.

If the proposed solution is anything like the flutter_hooks package that's wholy untrue. Yes, conceptually it's an array with functions in it, but the implementation is nowhere near trivial or efficient.

I mean, I may be wrong, but it seems like the HookElement checks wheather it should rebuild itself in its own build method?!
Also checking wheather the hooks should be intialized, reinitialized or disposed on every single widget build seems like a significant overhead. It just doesn't feel right, so I hope I'm wrong.

Would it make sense to take one of @brianegan 's architecture examples as base app for a comparison?

If I may interject here, not sure if this was said already. But in React we don't really think of lifecycle with hooks, and that might sound scary if it's how you're used to building Components/Widgets, but here's why lifecycle doesn't really matter.

Most times, when you're building Components/Widgets with state or actions to take based on props, you want something to happen when that state/props change (for example, like I saw mentioned in this thread, you'll want to re-fetch a user's details when the userId prop has changed). It's usually much more natural to think of that as an "effect" of the userId changing, rather than something that happens when all the props of the Widget change.

Same thing for cleanup, it's usually much more natural to think "I need to clean up this state/listener/controller when this prop/state changes" rather than "I need to remember to clean up X when all the props/state change or when the entire component gets destroyed".

I haven't written Flutter in a while, so I'm not trying to sound like I know the current climate or limitations this approach would have on the current Flutter mindset, I am open to differing opinions. I just think that a lot of people not familiar with React hooks are having the same confusion I had when I was introduced to them because my mindset was so ingrained in the lifecycle paradigm.

@escamoteur @Rudiksz @Hixie there has been a GitHub project created by @TimWhiting that I've been invited to where we are starting to create these examples. Each person/group can create how they'd solve a predefined problem. They're not full blown apps, more like pages, but we can add apps as well, if they serve to show more complex examples. Then we can discuss problems and create a better API. @TimWhiting can invite anyone interested I presume.

https://github.com/TimWhiting/local_widget_state_approaches

Jetpack compose is also has similar to hooks, which was compared with react here.

@satvikpendem @TimWhiting That's great! Thank you.

@esDotDev
very specific use case and supporting random collection of setup/teardown logic definitely shouldn't be part of a framework.

This is the nail that hooks hits on the head. Each type of object is responsible for it's own setup and teardown. Animators know how to create, update and destroy themselves, as do streams, and so on. Hooks specifically solves this issue of 'random collections' of state scaffolding scattered throughout your view. The allows the bulk of the view code to focus on business logic and layout formatting, which is a win. It doesn't force you into creating custom widgets, just to hide some generic boilerplate that is the same in every project.

My longest initState method is 5 lines of code, my widgets don't suffer of excessive nesting, so we definitely have different needs and use cases. Or a different development style.

Mine too. But it's initState() + dispose() + didUpdateDependancies(), and missing either of the last 2 can cause bugs.

I think the canonical example would be something like: Write a view that uses 1 streamcontroller and 2 animator controllers.

You have 3 options as far as I can see:

  1. Add 30 lines or so of boilerplate to your class, and some mixins. Which is not only verbose, but fairly hard to follow initially.
  2. Use 2 TweenAnimationBuilders and a StreamBuilder, for about 15 indentation levels, before you even get to your view code, and you still have lots of boilerplate for the Stream.
  3. Add like 6 lines of non-indented code at the top of build(), to get your 3 stateful sub-objects, and define any custom init/destroy code

Maybe there's a 4th option which is a SingleStreamBuilderDoubleAnimationWidget, but this is just a make-work thing for developers and pretty annoying in general.

Also worth noting that the cognitive load of 3 is significantly lower than the other 2 for a new developer. Most new dev's don't even know that TweenAnimationBuilder exists, and simply learning the concept of SingleTickerProvider is a task in it's own. Just saying, "Give me an animator please", is a easier and more robust approach.

I'll try and code something up today.

2. Use 2 TweenAnimationBuilders and a StreamBuilder, for about 15 indentation levels, before you even get to your view code, and you still have lots of boilerplate for the Stream.

Right. Show us a real world example of code that uses 15 levels of indentation.

How does replacing 30 lines of code with 6 lines + hundreds of lines in a library reduce cognitive load? Yeah, I can just ignore the "magic" the library does, but not its rules. For example the hooks package tels you in no uncertain terms that hooks must be used only in build methods. Now you have an extra constraint to worry about.

I have probably less than 200 lines of code that involves focusnodes, textcontrollers, singletickerproviders or the various lifecycle methods of my statefulwidgets, in a project with 15k lines of code. What cognitive overload are you talking about?

@Rudiksz please stop being passive aggressive.
We can disagree without fighting.


Hooks constraints are the least of my worries.

We aren't talking about hooks specifically, but about the problem.
If we have to, we can come up with a different solution.

What matters is the problem we want to solve.

Futhermore, Widgets can only be used inside build and unconditionally (or we are otherwise changing the tree depth, which is a no go)

That is identical to the hooks constraints, but I don't think people complained about it.

Futhermore, Widgets can only be used inside build and unconditionally (or we are otherwise changing the tree depth, which is a no go)

That is identical to the hooks constraints, but I don't think people complained about it.

No ,it's not identical. The problem that is presented here seems to be related to code that _prepares_ widgets _before_ being (re)built. Preparing state, dependencies, streams, controllers, whatnot, and handling changes in the tree structure. None of these should be in the build method, even if it's hidden behind a single function call.
The entry point for that logic should never be in the build method.

Forcing me to put initialization logic of any kind into the build method is nowhere the same as "forcing" me to compose a widget tree in the build method. The whole reason for the build method is to take an existing state (set of variables) and produce a widget tree that then gets painted.

Conversely, I would also be against forcing me to add code that builds widgets inside the initState/didUpdateWidget methods.

As it is now, statefulwidget lifecycle methods have a very clear role and make it very easy and straighforward to separate code with entirely different concerns.

Conceptually I'm starting to understand the problems that are being described here, but I still fail to see it as an actual problem. Maybe some real examples (that are not the counter app) can help me change my mind.

As a side-note, Riverpod, my latest experiment, has some very hook-like ideas, without the constraints.

For example, it solves the:

Consumer(
  provider: provider,
  builder: (context, value) {
    return Consumer(
      provider: provider2,
      builder: (context, value2) {
        return Text('$value $value2');
      },
    );
  },
)

by having:

Consumer(
  builder (context, watch) {
    final value = watch(provider);
    final value2 = watch(provider2);
  },
)

Where watch can be called conditionally:

Consumer(
  builder: (context, watch) {
    final value = watch(provider);
    if (something) {
      final value2 = watch(provider2);
    }
  },
)

We could even get rid of Consumer entirely by having a custom StatelessWidget/StatefulWidget base class:

class Example extends ConsumerStatelessWidget {
  @override
  Widget build(ConsumerBuildContext context) {
    final value = context.watch(provider);
    final value2 = context.watch(provider2);
  }
}

The main issue is, this is specific to one kind of object, and it works by relying on the fact that the object instance has a consistent hashCode across rebuilds.

So we are still far from the flexibility of hooks

@rrousselGit I think without extending StatelessWidget/StatefulWidget classes and creating something like ConsumerStatelessWidget, it is possible to have something like context.watch by using extension methods on BuildContext class and having the provider provide the watch function with InheritedWidgets.

That's a different topic. But tl;dr, we can't rely on InheritedWidgets as a solution to this issue: https://github.com/flutter/flutter/issues/30062

To solve that issue, using InheritedWidgets would block us because of https://github.com/flutter/flutter/issues/12992 and https://github.com/flutter/flutter/pull/33213

Conceptually I'm starting to understand the problems that are being described here, but I still fail to see it as an actual problem.

Comparing Flutter to SwiftUI, to me it's obvious that there is actual problem, or rather - things are not as great as they could be.

It may be hard to see, because Flutter & others worked hard around it: we have wrappers for each specific case: AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity, etc. StatefulWidget works great for implementing these small reusable utilities, but it's just too low level for non-reusable, domain specific components, where you may have multitude of text controllers, animations, or whatever business logic calls for. The usual solution is to either bite the bullet and write all that boilerplate, or make a carefully constructed tree of providers and listeners. Neither approach is satifying.

It's like @rrousselGit says, in the olden days (UIKit) we were forced to manually manage our UIViews (the equivalent of RenderObjects), and remember to copy values from model to view and back, delete unused views, recycle, and so on. This was not a rocket science, and many people did not see this old problem, but I think everyone here would agree that Flutter clearly improved the situation.
With statefulness, the issue is very similiar: it's boring, error prone work that could be automated.

And, by the way, I don't think hooks solve this at all. It's just that hooks are the only approach that is possible without changing flutter's internals.

StatefulWidget works great for implementing these small reusable utilities, but it's just too low level for non-reusable, domain specific components, where you may have multitude of text controllers, animations, or whatever business logic calls for.

I'm confused when you say that building your non-reusable domain specific components you need a widget that is high-level. Usually it's the exact opposite.

AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity are all widget that implement a certain use case. When I need a widget that has so specific logic that I can't use any of these higher level widgets, that's when I drop down to a lower level api so I can write my own specific use case. The so called boilerplate is implementing how my unique widget manages its unique combination of streams, network calls, controllers and whatnot.

Advocating for hooks, hook like behaviour or even just "automation" is like saying that we need a low level widget that can handle high level, non-reusable logic anybody would ever want to have without having to write the so called boilerplate code.

With statefulness, the issue is very similiar: it's boring, error prone work that could be automated.

Again. You want to automate __"non-reusable, domain specific components, where you may have multitude of text controllers, animations, or whatever business logic calls for"__?!

It's like @rrousselGit says, in the olden days (UIKit) we were forced to manually manage our UIViews (the equivalent of RenderObjects), and remember to copy values from model to view and back, delete unused views, recycle, and so on. This was not a rocket science, and many people did not see this old problem, but I think everyone here would agree that Flutter clearly improved the situation.

I did ios and android development 6-7 years ago (around the time Android switched to their material design) and I don't remember any of this managing and recycling views being an issue and Flutter doesn't seem better or worse. I can't speak about the current affairs, I quit when Swift and Kotlin were launched.

The boilerplate I am forced to write in my StatefulWidgets is about 1% of my code base. Is it error prone? Every line of code is a potential source of bugs, so sure. Is it cumbersome? 200 lines of code ouot of 15000? I really don't think so, but that's just my opinion. Flutter's text/animation controllers, focusnodes all have issues that can be improved upon, but being verbose is not one.

I'm really curious to see what people are developing that they need so much boilerplate.

Listening to some of the comments here sounds like managing 5 lines of code instead of 1 is like 5 times harder. It's not.

Wouldn't you agree though that instead of setting up initState and dispose for each AnimationController for example can be more error prone than just doing it once and reusing that logic? Same principle as using functions, reusability. I agree that it is problematic to put hooks in the build function though, there definitely is a better way.

It really feels like the difference between those who do and do not see the problem here are that the former have used hook like constructs before, such as in React, Swift, and Kotlin, and the latter have not, such as working in pure Java or Android. I think the only way to be convinced, in my experience, is to try hooks and see if you can go back to the standard way. Oftentimes, many people can't, again, in my experience. You know it when you use it.

To that end, I'd encourage people who are skeptical to use flutter_hooks for a small project and see how it fares, then redo it in the default way. It is not sufficient that we simply create versions of the app for one to read as in @Hixie 's suggestion (although we will do that as well of course), I do believe each person must use hooks themselves to see the difference.

It is not sufficient that we simply create versions of the app for one to read as in @Hixie 's suggestion (although we will do that as well of course), I do believe each person must use hooks themselves to see the difference.

I wasted days trying provider, even more days trying bloc, I didn't find either of them was a good solution. If it works for you, great.

In order for me to even try your proposed solution to a problem that you are having, you need to demonstrate its advantages. I looked at examples with flutter hooks and I looked at its implementation. Just no.

Whatever boilerplate reducing code is added to the framework, I hope the Stateful/StatelessWidgets are left unchaged. There's not much more I can add to this conversation.

Let's start again, in a hypothetical world where we can change Dart, without talking about hooks.

The problem debated is:

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

This code is not readable.

We could fix the readability issue by introducing a new keyword which changes the syntax into:

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

This code is significantly more readable, is unrelated to hooks, and doesn't suffer from its limitations.
The readability gain is not much about the number of lines, but the formatting and indentations.


But what about when the Builder is not the root widget?

Example:

Widget build(context) {
  return Scaffold(
    body: StreamBuilder(
      stream: stream,
      builder: (context, value) {
        return Consumer<Value2>(
          builder: (context, value2, child) {
            return Text('${value.data} $value2');
          },
        );
      },
    ),
  );
}

We could have:

Widget build(context) {
  return Scaffold(
    body: {
      final value = keyword StreamBuilder(stream: stream);
      final value2 = keyword Consumer<Value2>();
      return Text('${value.data} $value2');
    }
  );
}

But how does this relate to the reusability issue?

The reason why this is related is, Builders are technically a way to reuse state logic. But their issue is, they make the code not very readable if we plan to use _many_ builders, like in this comment https://github.com/flutter/flutter/issues/51752#issuecomment-669626522

With this new syntax, we fixed the readability problem. As such, we can extract more things into Builders.

So for example, the useFilter mentioned in this comment could be:

FilterBuilder(
  debounceDuration: const Duration(seconds: 2),
  builder: (context, filter) {
    return TextField(onChange: (value) => filter.value = value);
  }
)

Which we can then use with the new keyword:

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

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

What about the "extract as function" we talked about with hooks, to create custom hooks/Builders?

We could do the same thing with such keyword, by extracting a combination of Builders in a function:

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

  @override
  Widget build(BuildContext context) {
    final chat = keyword ChatBuilder();

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

Obviously, not much thought was given in all the implications of such syntax. But that's the basic idea.


Hooks are this feature.
The limitations of hooks exist because they are implemented as a package rather than a language feature.

And the keyword is use, such that keyword StreamBuilder becomes use StreamBuilder, which is ultimately implemented as useStream

This code is significantly more readable

I think this is a matter of opinion. I agree that some people think that versions you describe as more readable are better; personally I prefer the much more explicit magic-less versions. But I have no objection to making the second style possible.

That said, the next step is to work on @TimWhiting's app (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/stateful/counter.dart) to make it into something that has all the problems that we want to solve.

For what it's worth, https://github.com/flutter/flutter/issues/51752#issuecomment-670959424 pretty much parallels the inspiration for Hooks in React. The Builder pattern seems identical to the Render Props pattern that used to be prevalent in React (but led to similarly deep trees). Later @trueadm suggested syntax sugar for Render Props, and later that led to Hooks (to remove unnecessary runtime overhead).

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

If readablity and indentation is the issue this can be rewritten as

  @override
  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'),
      );

If functions are not your thing, or you need reusability then extract them as widgets

class NewWidget extends StatelessWidget {
  var someValueListenable;

  var someStream;

  @override
  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;

  @override
  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);

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      tween: Tween(),
      builder: (context, value3, _) {
        return Text('$value $value2 $value3');
      },
    );
  }
}

There's nothing in your example that warrants a new keyword or feature.

I know what you are going to say. The 'value' variable has to be passed through all the widgets/function calls, but that's just a result of how you architect your application. I break up my code using both with "build" methods and custom widgets, depending on the use case, and never have to pass the same variable to a chain of three calls.

Reusable code is reusable when it relies as little as possible on external side effects (such as InheritedWidgets, or (semi)global state).

@Rudiksz I don't think you're adding anything to the discussion here. We are aware of strategies to mitigate these issues because we do them all day long. If you don't feel like it's an issue, then you can simply continue to use things as they are and this does not affect you at all.

Clearly there are many many people who do see this as a fundamental pain point, and just cycling back and forth is pointless. You are not going, through various arguments, convince people that they do not want what they want or change anyone's mind here. Everyone in this discussion clearly has hundreds or thousands of hours in Flutter under their belt, and it's not expected that we all should agree on things.

I think this is a matter of opinion. I agree that some people think that versions you describe as more readable are better; personally I prefer the much more explicit magic-less versions. But I have no objection to making the second style possible.

If it is a matter of opinion, I would guess that it is quite lopsided in one direction.

  1. Both have magic. I don't necessarily know what any of these builders are doing internally. The non-magic version is writing the actual boilerplate inside these builders. Using a SingleAnimationTIckerProvider mixin is magic too for 95% of Flutter devs.
  2. One obfuscates very important variable names that will be used later in the tree, which is value1 and value2, the other has them front and center at top of build. This a clear parsing / maintenance win.
  3. One has 6 levels of indendation before the widget tree even begins, the other has 0
  4. One is 5 vertical lines, the other is 15
  5. One shows the actual content piece prominently (Text()) the other hides it, nested, way down into the tree. Another clear parsing win.

In a general sense I could see this maybe being a matter of taste. But Flutter specifically has an issue with line count, indendation and signal:nois ratio in general. While I absolutely _love_ the ability to declaratively form a tree in Dart code, it can lead to some extremely unreadable/verbose code, especially when you rely on multiple layers of wrapped builders. So in the context of Flutter itself, where we are continually fighting this battle, this sort of optimization is a killer feature, because it provides us a really excellent tool to combat this somewhat pervasive issue of general verbosity.

TL;DR - Anything that significantly reduced indentation and line count in Flutter, is doubly valuable, due to the generally high line counts and indentation inherent in Flutter.

@Rudiksz I don't think you're adding anything to the discussion here. We are aware of strategies to mitigate these issues because we do them all day long. If you don't feel like it's an issue, then you can simply continue to use things as they are and this does not affect you at all.

Except if it's a change in the core framework then it does affect me, no?

Clearly there are many many people who do see this as a fundamental pain point, and just cycling back and forth is pointless. You are not going, through various arguments, convince people that they do not want what they want or change anyone's mind here. Everyone in this discussion clearly has hundreds or thousands of hours in Flutter under their belt, and it's not expected that we all should agree on things.

Right, this horse was already beaten to death countless times, so I won't fall in the trap of answering any more comments.

Builders are technically a way to reuse state logic. But their issue is, they make the code not very readable.

This states it perfectly. To think of it in Flutter terms, we need single-line builders.

Builders that do not require dozens of tabs and lines, but still allow some custom boilerplate to be hooked into the widget lifecycle.

The "everything is a widget" mantra specifically is not a good thing here. The relevant code in a builder is usually just it's setup props, and the stateful thing that it returns which the build fxn needs. Every single line break and tab is basically pointless.

Except if it's a change in the core framework then it does affect me, no?

@Rudiksz I don't think anyone is proposing that we change Stateful widgets. You can always use them in their current form if you want. Whatever solution we might come up with would either use Stateful widgets with no changes, or another type of widget entirely. We aren't saying that Stateful widgets are bad, just that we'd like another type of Widget that allows more composable widget state. Think of it as a Stateful widget that instead of one state object associated with it contains multiple state objects, and one build function that is separate but has access to those state objects. This way, you can reuse bits of common state (along with state logic associated with that state) with their initState and dispose already implemented for you. Essentially more modular state, that can be composed in different ways in different situations. Again, this is not a concrete proposal, but maybe another way to think of it. Maybe it could turn into a solution that is more flutter like, but I don't know.

That said, the next step is to work on @TimWhiting's app (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/stateful/counter.dart) to make it into something that has all the problems that we want to solve.

This is tricky because this problem is inherently one of a death by a thousand cuts. It just adds bloat and decreases readability across the codebase. It's impact is felt worst in small widgets, where the entire class is <100 lines, and half of it is spent managing an animator controller. So I don't know what seeing 30 of these examples will show, that 1 does not.

It really is the difference between this:

@override
  Widget build(BuildContext context) {
    final controller = get AnimationController(vsync: this, duration: widget.duration);
    //do stuff
  }

And this:

  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: widget.duration);
  }

  @override
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.duration != oldWidget.duration) {
      _controller.duration = widget.duration;
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    //Do Stuff
  }

There simply is no better way to illustrate than this. You can extend this use case to any controller type object. AnimatorController, FocusController and TextEditingController are probably the most common and annoying ones to manage in day to day use. Now just picture 50, or 100 of these sprinkled all over my code base.

  • You have about 1000-2000 lines that could just disappear.
  • You have probably dozens of bugs and RTE's (at various pts of development) that did not ever need to exist because some override or another was missing.
  • You have widgets, that when looked at with cold eyes, are much harder to grok at a glance. I need to read each of these overrides, I can't simply assume they are boilerplate.

And you can of course extend this to custom controllers. The entire concept of using controllers is less appealing in Flutter because you know you will have to bootstrap, manage and destroy them like this, which is annoying and bug prone. It leads you to avoid making your own and instead creating custom StatefulWidgets/Builders. It would be nice if controller type objects were just easier to use and more robust, since builders have readability issues (or at the least, are significantly more verbose and white-space laden).

This is tricky

Yup, API design is tricky. Welcome to my life.

So I don't know what seeing 30 of these examples will show, that 1 does not.

It's not 30 examples that will help, it's 1 example that's elaborate enough that it can't be simplified in ways that you would then say "well, sure, for this example that works, but it doesn't for a _real_ example".

It's not 30 examples that will help, it's 1 example that's elaborate enough that it can't be simplified in ways that you would then say "well, sure, for this example that works, but it doesn't for a real example".

I've said it a few times already, but that way of judging hooks is unfair.
Hooks aren't about making possible something that was previously impossible. It's about providing a consistent API to solve this sort of problem.

Requesting an application that shows something which can't be simplified differently is judging a fish by its ability to climb trees.

We're not just trying to judge hooks, we're trying to evaluate different solutions to see if there are any that address the needs of everyone.

(I'm curious what way you would evaluate different proposals here, if not by actually writing applications in each of the proposals and comparing them. What evaluation metric would you propose instead?)

A proper way to judge the solution to this problem is not an application (as each individual usage of the API will be dismissed like the examples here were).

What we should judge the proposed solution on is:

  • Is the resulting code objectively better than the default syntax?

    • Does it avoid mistakes?

    • Is it more readable?

    • Is it easier to write?

  • How reusable is the code produced?
  • How many problems can be solved with this API?

    • Do we lose some benefits for some specific problems?

When evaluated on this grid, the Property/addDispose proposal may have a good "is the resulting code better?" score, but evaluate poorly against both reusability and flexibility.

I don't know how to answer those questions without actually seeing each proposal in real use.

Why?

I didn't need to make applications using Property to know that this proposal will have difficulty with producing truly reusable code and solving many problems.

We can take any existing *Builder and try to reimplement them using the proposed solution.
We can also try and reimplement the hooks listed in this thread, or some hooks made in the React community (there are numerous compilations of hooks available online).

I didn't need to make applications using Property to know that this proposal will have difficulty with producing truly reusable code and solving many problems.

Unfortunately, I do not share your instincts here (as demonstrated by the fact that I thought Property (https://github.com/flutter/flutter/issues/51752#issuecomment-667737471) worked great until you said it had to handle values both from a widget and local state with the same API, which I wasn't aware was a constraint until you brought it up). If I provide a version of Property that solves that problem as well, is it definitely going to be ok, or will there be some new problem that it doesn't cover? Without a target we all agree is the target, I don't know what we're designing solutions for.

We can take any existing *Builder and try to reimplement them using the proposed solution.

Clearly not _any_. For example, you gave one in the OP, and when I made my first Property proposal (https://github.com/flutter/flutter/issues/51752#issuecomment-664787791) you pointed out problems with it that were not illustrated by the original Builder.

We can also try and reimplement the hooks listed in this thread, or some hooks made in the React community (there are numerous compilations of hooks available online).

I don't mind where we start. If you have a particularly good example that you think illustrates the problem and it uses hooks, then great, let's add that to @TimWhiting's repository. The whole point is to implement the same thing in multiple different ways, I don't mind where the ideas come from.

It's not 30 examples that will help, it's 1 example that's elaborate enough that it can't be simplified in ways that you would then say "well, sure, for this example that works, but it doesn't for a _real_ example".

There will never be anything more elaborate than the simple desire to to use an AnimatorController (or any other re-useable stateful component you can think of) without an unreadable builder or a bunch of bug-prone lifecycle boilerplate.

There has been no solution proposed that addressed the readability and robustness benefits requested, in a general purpose way.

I do insist on the fact that _any_ builder will do, as this issue could be renamed into "We need syntax sugar for Builders" and lead to the same discussion.

All other arguments made (such as creating and disposing an AnimationController) are made on the basis that they too could be extracted into a builder:

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

    }
  );
}

In the end, I think the perfect example is trying to reimplement StreamBuilder in its entirety, and test it in different scenarios:

  • the stream comes from the Widget
  • // from an InheritedWidget
  • from the local State

and test each individual case against "the stream can change over time", so:

  • didUpdateWidget with a new stream
  • the InheritedWidget updated
  • we called setState

This is currently unsolvable with Property or onDispose

@esDotDev Can you enumerate "the readability and robustness benefits requested"? If someone makes a proposal that handles AnimationController with those readability and robustness benefits, then we're done here?

@rrousselGit I'm not advocating Property, as you've said before that it doesn't solve your problems. But if someone were to create a solution that did everything that StreamBuilder does, but without the indentation, would that be it? You'd be happy?

But if someone were to create a solution that did everything that StreamBuilder does, but without the indentation, would that be it? You'd be happy?

Most likely, yes

We'd need to compare this solution to other solutions of course. But that would reach the acceptable level.

@esDotDev Can you enumerate "the readability and robustness benefits requested"?

Robustness in that it can fully encapsulate the boilerplate around dependencies and lifecycle. ie. I should not have to tell fetchUser every single time, that it should probably rebuild when id changes, it knows to do that internally. Should not have to tell an Animation to dispose itself everytime it's parent Widget is disposed, etc. (I don't fully understand whether Property can do this tbh). This stops developers from making mistakes for rote tasks, all across the code base (one of the main benefits of using Builders currently imo)

Readability being that we can get the stateful thing with a single line of non-indented code, and the variable for the thing is hoisted and clearly visible.

@esDotDev If someone makes a proposal that handles AnimationController with those readability and robustness benefits, then we're done here?

If you mean specifically AnimationController no. If you mean any AnimationController/FocusController/TextEdiitingController-like object, then yes.

Having a function-like API that returns a value that has a defined life-time that isn't clear is

I think this is a key mis-understanding. The life-time of a hook is clear, because they are by definition sub-states. They _always_ exist for the lifetime of the State which "uses" them. It could not actually be more clear. The syntax is maybe weird and unfamiliar, but it's certainly not lacking clarity.

Similar to how the lifetime of a TweenAnimationBuilder() is also clear. It will go away when it's parent goes away. Like a child widget, these are child states. They are fully independent state "components", we can assemble and re-use with ease and we explicitly do not manage their lifetime because we want it to naturally be bound to the state in which it's declared, a feature not a bug.

@esDotDev

etc

Can you be more specific? (This is why I suggested coming up with a demo application that covered all the bases. I continue to think that's the best way to do this.) Are there features that matter other than only calling the initializer unless the configuration has changed and automatically disposing allocated resources when the host element is disposed?

TextEdiitingController-like object

Can you be more specific? Is TextEditingController more elaborate than AnimationController in some way?


@rrousselGit

But if someone were to create a solution that did everything that StreamBuilder does, but without the indentation, would that be it? You'd be happy?

Most likely, yes

Here is a solution that does everything StreamBuilder does, without any of the indenting:

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

I'm guessing this violates some other constraint, though. Which is why I would prefer to have something we can all agree is a full description of the problem before we try to solve it.

It is simply the same constraints builders have @Hixie no one is asking for more than that. A builder can tie into widget.whatever, a builder can fully manage whatever internal state is required in the context of the widget tree. That is all a hook can do, and all that anyone is asking for a micro-state or whatever you want to call it.

Can you be more specific? Is TextEditingController more elaborate than AnimationController in some way?

No, but it might do different things in init/dispose, or it will bind to different properties, and that I would want to encapsulate that specific boilerplate.

@esDotDev So you want the same thing as a builder, but without the indenting, and on one line (minus the builder callback itself, presumably)? The example I just posted (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483) does that with builders today, so presumably there are additional constraints beyond that?

(FWIW, I don't think builders, or something like builders but on one line, are a good solution, because they require that each data type have its own builder created for it; there's no good way to just build one on the fly.)

(FWIW, I don't think builders, or something like builders but on one line, are a good solution, because they require that each data type have its own builder created for it; there's no good way to just build one on the fly.)

I don't understand what this means. Could you rephrase it? 🙏

You have to create an AnimationBuilder for Animations and a StreamBuilder for Streams and so on. Instead of just having a single Builder and saying "here's how you get a new one, here's how you dispose it, here's how you get the data out" etc when you create your StatefulWidget.

I'm guessing this violates some other constraint, though. Which is why I would prefer to have something we can all agree is a full description of the problem before we try to solve it.

I think it fairly obviously violates any request for readable code, which is ultimately the goal here, otherwise we'd all just use a million specifically typed-builders, nest em forever, and call it a day.

I think what is being requested is some thing like (bear with me, I don't, use Streams much):

Widget build(context) {
   var snapshot1 = get AsyncSnapshot<int>(stream1);
   var snapshot2 = get AsyncSnapshot<int>(stream2);
   return Column(children: [Text(snapshot1.data), Text(snapshot2.data)]);
}

This is all the code. There would be nothing more, as the Stream is created for us, the stream is disposed for us, we cant shoot ourselves in the foot AND the code is vastly more readable.

You have to create an AnimationBuilder for Animations and a StreamBuilder for Streams and so on.

I don't see that as an issue. We have RestorableInt vs RestorableString vs RestorableDouble already

And generics can solve that:

GenericBuilder<Stream<int>>(
  create: (ref) {
    var controller = StreamController<int>();
    ref.onDispose(controller.close);
    return controller.stream;
  }
  builder: (context, Stream<int> stream) {

  }
)

Similarly, Flutter or Dart could include a Disposable interface if that really is a problem.

@esDotDev

I think what is being requested is some thing like:

That would violate some of the pretty reasonable constraints others have listed (e.g. @Rudiksz), namely guaranteeing that no initialization code ever happens during the call to the build method.

@rrousselGit

I don't see that as an issue. We have RestorableInt vs RestorableString vs RestorableDouble already

And we have AnimationBuilder and StreamBuilder and so on, yes. In both cases it is unfortunate.

GenericBuilder

That's similar to what I proposed for Property, but if I understood your concerns there you believed that to be too verbose.

Earlier, you said that if someone were to create a solution that did everything that StreamBuilder does, but without the indentation, you'd likely be happy. You haven't commented on my attempt at doing that (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483). Are you happy with that solution?

@esDotDev

I think what is being requested is some thing like:

That would violate some of the pretty reasonable constraints others have listed (e.g. @Rudiksz), namely guaranteeing that no initialization code ever happens during the call to the build method.

It is not important that this code be in build. The important part is that

  1. I am not forced to indent my tree, or add a ton of extra lines.
  2. The lifecycle code specific to this thing is encapsulated.

This would be amazing:

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

Or, not as succint, but still way more readable than using builders, and less verbose & error prone than doing it directly:

AsyncSnapshot<int> stream1;
AsyncSnapshot<int> stream2;
@override 
void initState(){
    snapshot1 = createLifecycleState(widget.stream1);
    snapshot2 = createLifecycleState(widget.stream2);
   super.initState();
}

Widget build(context) {
   return Column(children: [Text(snapshot1.data), Text(snapshot2.data)]);
}

I don't understand why we keep coming back to verbosity.
I explicitly said multiple times that it is not the problem and that the problem is reusability vs readability vs flexibility.

I even made a grid to evaluate the solution https://github.com/flutter/flutter/issues/51752#issuecomment-671000137 and a test case https://github.com/flutter/flutter/issues/51752#issuecomment-671002248


Earlier, you said that if someone were to create a solution that did everything that StreamBuilder does, but without the indentation, you'd likely be happy. You haven't commented on my attempt at doing that (#51752 (comment)). Are you happy with that solution?

That reaches the minimum acceptable level of flexibility.

Evaluating it as per https://github.com/flutter/flutter/issues/51752#issuecomment-671000137 gives:

  • Is the resulting code objectively better than the default syntax?

    • Does it avoid mistakes?

      _The default syntax (StreamBuilder without hacking) is less error prone. This solution doesn't avoid mistakes, it creates some_ ❌

    • Is it more readable?

      _It clearly isn't more readable_ ❌

    • Is it easier to write?

      _It isn't easy to write_ ❌

  • How reusable is the code produced?
    _StreamBuilder isn't tied to the Widget/State/life-cycles, so this pass_ ✅
  • How many problems can be solved with this API?
    _We can make custom builders, and use this pattern. So this pass_. ✅
  • Do we lose some benefits for some specific problems?
    _No, the syntax is relatively consistent_. ✅
  1. This feature IMO could extend to all builder widgets, including LayoutBuilder for example.
  2. There needs to be a way to disable listening, so that you can create 10x controllers and pass them to leafs for rebuilding, or flutter needs to somehow know which part of the tree used the value obtained by the builder.
  3. Using this should not be much more verbose that hooks.
  4. Compiler must be extended to handle this properly.
  5. Debugging helpers are needed. Let's say that you put breakpoins in one of your widgets that uses this. When reaching a breakpoint inside a build method, because one of the builders got triggered, IDE could display extra info for each builder that was used:
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)]);
}

Also, @Hixie

That would violate some of the pretty reasonable constraints others have listed (e.g. @Rudiksz), namely guaranteeing that no initialization code ever happens during the call to the build method.

We are already implictly doing that by using *Builders. We just need a syntax sugar to deindent them. It's a lot like async/await and futures, I think.

@esDotDev What you describe sounds very similar to what I proposed earlier with Property (see e.g. https://github.com/flutter/flutter/issues/51752#issuecomment-664787791 or https://github.com/flutter/flutter/issues/51752#issuecomment-667737471). Is there something that prevents this kind of solution from being created as a package? That is, what change would the core framework need to have made to it to allow you to use this kind of feature?

@rrousselGit As with Shawn I would ask you the same thing then. If the only difference between the current StreamBuilder feature and one that would satisfy your needs is a different syntax, what is it that you require of the core syntax to enable you to use such a feature? Would it not be sufficient to just create the syntax you would prefer and use that?

The problem I have with your grid is that if I were to apply it to the solutions so far I would get this, which I assume is very different than what you would get:

StatefulWidget

  • Is the resulting code objectively better than the default syntax?

    • Does it avoid mistakes?

      _It's the same as the default syntax, which isn't particularly error-prone._ 🔷

    • Is it more readable?

      _It's the same, so it's equally readable, which is reasonably readable._ 🔷

    • Is it easier to write?

      _It's the same, so it's equally easy to write, which is reasonably easy._ 🔷

  • How reusable is the code produced?
    _There is very little code to reuse._ ✅
  • How many problems can be solved with this API?
    _This is the baseline._ 🔷
  • Do we lose some benefits for some specific problems?
    _Doesn't seem that way._ ✅

Variations on Property

  • Is the resulting code objectively better than the default syntax?

    • Does it avoid mistakes?

      _It moves the code to a different place, but doesn't particularly reduce the number of mistakes._ 🔷

    • Is it more readable?

      _It puts initialization code and cleanup code and other lifecycle code in the same place, so it's less clear._ ❌

    • Is it easier to write?

      _It mixes initialization code and cleanup code and other lifecycle code, so it's harder to write._ ❌

  • How reusable is the code produced?
    _Exactly as reusable as StatefulWidget, just in different places._ ✅
  • How many problems can be solved with this API?
    _This is syntactic sugar for StatefulWidget, so no difference._ 🔷
  • Do we lose some benefits for some specific problems?
    _Performance and memory usage would suffer slightly._ ❌

Variations on Builders

  • Is the resulting code objectively better than the default syntax?

    • Does it avoid mistakes?

      _It is basically the StatefulWidget solution but factored out; mistakes should be about equivalent._ 🔷

    • Is it more readable?

      _Build methods are more complex, rest of logic moves to a different widget, so about the same._ 🔷

    • Is it easier to write?

      _Harder to write the first time (creating the builder widget), mildly easier thereafter, so about the same._ 🔷

  • How reusable is the code produced?
    _Exactly as reusable as StatefulWidget, just in different places._ ✅
  • How many problems can be solved with this API?
    _This is syntactic sugar for StatefulWidget, so mostly no difference. In some aspects it's actually better, for example, it reduces the amount of code that has to run when handling a dependency change._ ✅
  • Do we lose some benefits for some specific problems?
    _Doesn't seem that way._ ✅

Hook-like solutions

  • Is the resulting code objectively better than the default syntax?

    • Does it avoid mistakes?

      _Encourages bad patterns (e.g. construction in the build method), risks bugs if accidentally used with conditionals._ ❌

    • Is it more readable?

      _Increases the number of concepts that must be known to understand a build method._ ❌

    • Is it easier to write?

      _Developer has to learn to write hooks, which is a new concept, so harder._ ❌

  • How reusable is the code produced?
    _Exactly as reusable as StatefulWidget, just in different places._ ✅
  • How many problems can be solved with this API?
    _This is syntactic sugar for StatefulWidget, so no difference._ 🔷
  • Do we lose some benefits for some specific problems?
    _Performance and memory usage suffer._ ❌

I don't understand why we keep coming back to verbosity.
I explicitly said multiple times that it is not the problem and that the problem is reusability vs readability vs flexibility.

Apologies, I misremembered who it was who said that Property was too verbose. You're right, your concern was just that there was a new use case that hadn't been listed before that it didn't handle, though I think it would be trivial to extend Property to handle that use case as well (I haven't tried, it seems better to wait until we have a clear demo app so that we can solve things once and for all rather than have to iterate repeatedly as the requirements are adjusted).

@szotp

  1. This feature IMO could extend to all builder widgets, including LayoutBuilder for example.

LayoutBuilder is a very different widget than most builders, FWIW. None of the proposals that have been discussed so far would work for LayoutBuilder-like problems, and none of the requirements described before your comment include LayoutBuilder. If we should also use this new feature to handle LayoutBuilder that's important to know; I recommend working with @TimWhiting to make sure the sample app we're going to base proposals on includes layout builders as an example.

  1. There needs to be a way to disable listening, so that you can create 10x controllers and pass them to leafs for rebuilding, or flutter needs to somehow know which part of the tree used the value obtained by the builder.

I'm not sure exactly what this means. As far as I can tell, you can do this today with listeners and builders (e.g. I use ValueListenableBuilder in the app I cited earlier to do exactly this).

That would violate some of the pretty reasonable constraints others have listed (e.g. @Rudiksz), namely guaranteeing that no initialization code ever happens during the call to the build method.

We are already implictly doing that by using *Builders.

I don't think that's accurate. It depends on the builder, but some work very hard to separate initState, didChangeDependencies, didUpdateWidget, and build logic, so that only the minimum amount of code needs to run each build based on what has changed. For example, ValueListenableBuilder only registers the listeners when first created, its builder can rerun without either the parent or the initState of the builder rerunning. This is not something Hooks can do.

@esDotDev What you describe sounds very similar to what I proposed earlier with Property (see e.g. #51752 (comment) or #51752 (comment)).

If I understand right, we could make UserProperty that automatically handles the DidDependancyChange for UserId, or AnimationProperty, or any other property we need to handle the init/update/dispose for that type? Then this seems nice to me. The most common use cases could quickly be created.

The only thing throwing me off is the future builder here. But I think this is just due to the example you've chosen?

For example, could I create this?

class _ExampleState extends State<Example> with PropertyManager {
  AnimationProperty animProp1;
  AnimationProperty animProp2;

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

  @override
  Widget build(BuildContext context) {
    return Column(children: [
       FadeTransition(opacity: anim1, child: ...),
       FadeTransition(opacity: anim2, child: ...),
   ])
  }
}

If so, this totally LGTM! In terms of adding to framework, its a case of whether this should be promoted to a first class syntactical approach (which means it becomes general practice in a year or so), or whether it exists as a plugin that some single digit percentage of developers use.

It's a matter of whether you want to be able to update the verbose and (slightly?) error prone examples with a better more concise syntax. Having to manually sync properties and manually dispose() things, does lead to bugs, and cognitive load.

Imo it would be nice if a developer could use animators, with proper didUpdate and dispose and debugFillProperties and the whole works, without ever having to think twice about it (exactly like we do when we use TweenAnimationBuilder now, which is the main reason we recommend all our developers default to using it over manually managing Animators).

If so, this totally LGTM! In terms of adding to framework, its a case of whether this should be promoted to a first class syntactical approach (which means it becomes general practice in a year or so), or whether it exists as a plugin that some single digit percentage of developers use.

Given how trivial Property is, my recommendation to someone who likes that style would be to just create their own (maybe starting with my code if that helps) and using it directly in their app as they see fit, adjusting it to address their needs. It could be made into a package if lots of people like it, too, though again for something that trivial it's not clear to me how much that's beneficial vs just copying it into one's code and adjusting it as needed.

The only thing throwing me off is the future builder here. But I think this is just due to the example you've chosen?

I was trying to address an example @rrousselGit gave. In principle it can be adapted to work for anything.

For example, could I create this?

You'd want to move the AnimationController constructor into a closure that would be called, rather than calling it every time, since initProperties is called during hot reload to get the new closures but typically you don't want to create a new controller during hot reload (e.g. it would reset animations). But yes, other than that it seems fine. You could even make an AnimationControllerProperty that takes the AnimationController constructor arguments and does the right thing with them (e.g. updating the duration on hot reload if it changed).

Imo it would be nice if a developer could use animators, with proper didUpdate and dispose and debugFillProperties and the whole works, without ever having to think twice about it (exactly like we do when we use TweenAnimationBuilder now, which is the main reason we recommend all our developers default to using it over manually managing Animators).

My worry about having developers not think about it is that if you don't think about when things are allocated and disposed you're more likely to end up allocating a lot of things you don't always need, or running logic that doesn't need to run, or doing other things that lead to less efficient code. That's one reason I'd be reluctant to make this a default recommended style.

You could even make an AnimationControllerProperty that takes the AnimationController constructor arguments and does the right thing with them (e.g. updating the duration on hot reload if it changed).

Thanks @Hixie that's really cool and I think addresses the issue quite well.

I'm not suggesting devs should never think about these things, but I think the 99% use case these things are almost always bound to the StatefulWidget they are used in, and doing anything other than that is already bringing you into intermediate developer land.

Again, I don't see how this is fundamentally any different than recommending TweenAnimationBuilder over raw AnimatorController. It's basically the idea that IF you want the state wholly contained/managed within this other state (and that is usually what you want), then do it this way its simpler and more robust.

At this point, we should organize a call and discuss it together with the different interested parties.
Because this discussion isn't progressing, as we are answering the same question over and over.

I don't understand how after such a long discussion, with so many arguments made, we can still argue that Builders do not avoid mistakes compared to StatefulWidget, or that hooks aren't more reusable than raw StatefulWidgets.

That is especially frustrating to argue considering all the major declarative frameworks (React, Vue, Swift UI, Jetpack Compose) have one way or another to natively solve this problem.
It seems like only Flutter refuses to consider this problem.

@esDotDev 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. It's not really about verbosity or code reuse. I mean, it's fine to use them for those reasons too, if you find them useful for those reasons, but that's not the main use case, at least for me. It's also something that Property (or Hooks) doesn't give you. With those, you end up rebuilding the _entire_ widget when something changes, which is not good for performance.

@rrousselGit

It seems like only Flutter refuses to consider this problem.

I've spent literally hours of my own time this weekend, not to mention many hours of Google's time before that, considering this problem, describing possible solutions, and trying to drawn out precisely what we're trying to solve. Please don't confuse lack of understanding over what is a problem with refusal to consider it. Especially when I've already described the best thing that can be done to move this forward (creating a demo app that has all the state logic that is "too verbose or too difficult", to quote the issue title, to reuse), which others on this bug have taken on as a task, and which you have refused to participate in.

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.

Interesting. For us we really have never measured or observed an improvement in performance from saving small rebuilds like this. It's much more important over a large app code base to keep the code succinct, readable, and free of any routine errors that can happen when you're kicking out hundreds of class files every couple of weeks.

From our experience the cost of repainting the pixels, which seems to happens to the full tree unless you are purposeful about defining your RepaintBoundaries, is vastly more important a factor in realworld performance than partial widget layout costs. Especially when you get into 4k monitor range.

But this is a good example of when builders actually make sense for this sort of thing. If I want to create a sub-context, then a builder makes sense and is a nice way to get there.

Many times we do not, and in this case, Builder is just adding clutter, but we accept it, cause the alternative is just a different type of clutter, and at least with Builder, things are more or less guaranteed bug free. In cases where the entire view rebuilds, or there is not necessarily view rebuilds at all (TextEditingController, FocusController) using a builder makes little sense, and in all cases, rolling the boilerplate by hand is fundamentally not DRY.

It's certainly very situation-specific, as performance issues often are. I think it makes sense for people to use something like Hooks or Property if they like that style. That's possible today and doesn't seem to need anything extra from the framework (and as Property shows, it really doesn't require much code).

No, but its a bit like asking the community to build TweenAnimationBuilder and ValueListenableBuilder and not providing them a StatefulWidget to build on.

Not that you are asking, but one of the primary benefits of this type of architecture is that it naturally lends itself to tiny little components that can be easily shared. If you put one little foundational piece in place...

StatefulWidget is a _lot_ of code, compared to Property, and it's non-trivial (unlike Property, which is mostly glue code). That said, if Property is something that makes sense to reuse widely (as opposed to crafting bespoke versions for each application or team based on its precise needs), then I would encourage someone who advocates for its use to make a package and upload it to pub. The same applies, indeed, to Hooks. If it is something the community likes then it will see a lot of use, just like Provider. It's not clear why we would need to put something like that in the framework itself.

I guess because this is inherently extensible. Provider is not, it's just a simple tool. This is something that is made to be extended, just like StatefulWidget, but for StatefulComponents. The fact it is relatively trivial should not necessarily be held against it?

A note on "those who prefer this style". If you can save 3 overrides and 15-30 lines, that is just going to be a readability win in most cases. Objectively speaking imo. It also objectively eliminates 2 entire classes of errors (forgetting to dispose things, forgetting to update deps).

Thanks so much for the awesome discussion, excited to see where this goes, I'll definitely leave it here :)

I'm sorry to say this thread makes me disillusioned getting back into flutter which was the plan when finishing up another project I'm working on. I also feel the frustration because of

That is especially frustrating to argue considering all the major declarative frameworks (React, Vue, Swift UI, Jetpack Compose) have one way or another to natively solve this problem.

I agree with @rrousselGit in that I don't think we should spend time building flutter example apps since the issue has been described clearly over and over again in this thread. I can't see how it would not just get the same response. Because it will be the same things been presented here. My take of this thread is that from a flutter frameworks point of view it's better for flutter developers to repeat the same lifetime code in multiple widgets instead of just writing it once and be done with it.
Also, we can't write an app in flutter if we are looking for a solution since we need the solutions to write an app. Since the flutter people in this conversation at least have been pretty clear they don't like hooks. And Flutter just don't have another solution to the problem as described in the OP. How should it even be written.

Feels (at least to me) that this isn't taken seriously, I'm sorry @Hixie, I mean it's not taken seriously in the sense of: We understand the problem and want to solve it. Like other declarative frameworks apparently seem to have done.
On another but kind of the same note things like this:

Is it easier to write?
Developer has to learn to write hooks, which is a new concept, so harder

Makes me sad. Why improve or change anything ever? you could always make that argument no matter what. Even if the new solutions are much easier and pleasant once learned. You can replace hooks in that statement with many things. My mother could have used that kind of sentence about microwaves 30 years ago. It e.g. works the same if you replace "hooks" in the sentence with "flutter" or "dart".

Is it easier to write?
It's the same, so it's equally easy to write, which is reasonably easy

I don't think what @rrousselGit mean with is it easier to write? (a boolean answer question) was that if it's the same the answer is not false/undefined.

I can't see how we will ever be able to get somewhere since we don't even agree there is a problem, only that a lot of people find this a problem. E.g:

It's certainly something people have brought up. It's not something I have a visceral experience with. It's not something I've felt was a problem when writing my own apps with Flutter. That doesn't mean that it's not a real problem for some people, though.

And even though many have provided lots of arguments many times why a solution to the OP needs to be in core.
E.g. it needs to be in core to make third parties able to use it just as easily and naturally as they use and create widgets today. And a multiple of other reasons. The mantra seems to be, just put in a package. But there already are packages as the hooks one. If that's what's been settled on then why not just close the thread.

I really hope you take @rrousselGit up on his offer and organize a call, maybe it will be easier to have a realtime discussion about this instead of writing stuff back and fort back and forth all the time. If any folks from the other frameworks that have solved the issue described in the OP, if one of them is really kind maybe they could get into the call for awhile as well and share they 5-cents about stuff that comes up. One could always ask.

Anyway, I'm unsubscribing now since I get a bit sad live-following this thread since I don't see it going anywhere. But I do hope this thread will get to the state of agreeing there is a problem so that it can focus on possible solutions to the OP. Since it seems a bit futile to propose solutions if you don't grasp the problem people are facing, as @Hixie probably agrees, I mean, since the people with the problems will tell you why the solution doesn't work afterwards.

I really do wish you the best of luck in ending this thread either by just flatly saying flutter shouldn't solve this problem in core despite people wanting it. Or by finding a solution. 😘

LayoutBuilder is a very different widget than most builders, FWIW. None of the proposals that have been discussed so far would work for LayoutBuilder-like problems, and none of the requirements described before your comment include LayoutBuilder. If we should also use this new feature to handle LayoutBuilder that's important to know; I recommend working with @TimWhiting to make sure the sample app we're going to base proposals on includes layout builders as an example.

@Hixie Yes, we definitely need some samples. I'll prepare something (but I still think that compiler changes are needed so the sample may be incomplete). The general idea is - a syntax sugar that flattens builders and does not care about how the builder is implemented.

Still, I'm getting impression than nobody at Flutter team took a deeper look at SwiftUI, I think our concerns would be easy to understand otherwise. It's important for the future of framework that people migrating from other platforms have as smooth ride as possible, and so, good understanding of other platforms is needed, and knowledge of pros & cons. And seeing if some of Flutter's cons could be fixed. Obviously Flutter took a lot of good ideas from React and I could do the same with newer frameworks.

@emanuel-lundman

Feels (at least to me) that this isn't taken seriously, I'm sorry @Hixie, I mean it's not taken seriously in the sense of: We understand the problem and want to solve it. Like other declarative frameworks apparently seem to have done.

I completely agree that I don't understand the problem. That's why I keep engaging on this issue, trying to understand it. It's why I've suggested creating a demo app that encapsulates the problem. Whether it's something that in the end we decide to fix by fundamentally changing the framework, or by adding a small feature to the framework, or by a package, or not at all, really depends on what the problem actually is.

@szotp

Still, I'm getting impression than nobody at Flutter team took a deeper look at SwiftUI, I think our concerns would be easy to understand otherwise.

I've studied Swift UI. It's certainly less verbose to write Swift UI code than Flutter code, but the readability cost is very high IMHO. There's a lot of "magic" (in the sense of, logic that works in ways that are not obvious in the consumer code). I can totally believe that it is a style that some people prefer, but I also believe that one of the strengths of Flutter is that it has very little magic. That does mean that you write more code sometimes, but it also means that debugging that code is _much_ easier.

I think there's room for lots of styles of frameworks. MVC-style, React-style, super terse magical, magic-free but verbose... One of the advantages of Flutter's architecture is that the portability aspect is entirely separate from the framework itself, so it is possible to leverage all of our tooling -- cross platform support, hot reload, etc -- but create an entirely new framework. (There are other Flutter frameworks, e.g. flutter_sprites, already.) Similarly the framework itself is designed in a layered fashion so that for example you can reuse all of our RenderObject logic but with a replacement for the Widgets layer, so if the verbosity of the Widgets is too much, someone could create an alternative framework that replaces it. And of course there's the packaging system so features can be added without losing any of the existing framework code.

Anyway, the point of my digression here is just that this is not all-or-nothing. Even if on the long term we don't end up adopting a solution that makes you happy, that doesn't mean you can't continue to benefit from the parts of the offering that you do like.


I urge people interested in this problem to work with @TimWhiting to create an app that demonstrates why you would want to reuse code and what it looks like today when you can't (https://github.com/TimWhiting/local_widget_state_approaches). This will directly help us create proposals for how to address this problem in a way that addresses the needs of _all_ the people who are commenting here (including those who like Hooks and those who do not like Hooks).

It can't really be so hard to understand why "_a syntax sugar that flattens builders and does not care about how the builder is implemented._" is desired by devs as a first class feature. We've outlined the issues with the alternative approaches over and over.

In short, builders solve the re-usability issue, but are hard to read and compose. The "problem" is simply that we would like builder-like functionality that is significantly easier to read.

No app can show that more clearly, if you just fundamentally don't agree that 3 nested builders are hard to read, or that builders in general do not really serve a code-reuse purpose. More important to just hear that many of us do in fact really like to cut down on nesting, and really do not like to duplicate code all over our application, and so we are stuck between 2 non-ideal options.

I've spent literally hours of my own time this weekend, not to mention many hours of Google's time before that, considering this problem, describing possible solutions, and trying to drawn out precisely what we're trying to solve

I'm grateful for that

Please don't confuse lack of understanding over what is a problem with refusal to consider it

I am fine with a lack of understanding, but the current situation seems hopeless.
We are still debating about points that were made at the very beginning of the discussion.

From my perspective, I feel like I spent hours writing detailed comments showcasing the different problems and answering questions, but my comments were dismissed and the same question was asked again.

For example, the lack of readability of the current syntax is at the center of the discussion.
I made several analyses of the readability problem to back this up:

These analyses have a significant number of 👍 and other peoples seem to agree

And yet according to your recent answer, there is no readability issue: https://github.com/flutter/flutter/issues/51752#issuecomment-671009593

You also suggested:

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

knowing that this isn't readable

From these two comments, we can conclude that:

  • we disagree that there is a readability issue
  • it is still unclear whether readability is part of the scope of this issue or not

This is disheartening to hear, considering the sole purpose of hooks is to improve the syntax of Builders – which are at the peak of reusability but have a poor readability/writability
If we don't agree on such basic fact, I am not sure what we can do.

@Hixie thanks, this helps a lot to understand your point of view. I agree that they may have went way overboard with code magic, but I'm sure there are at least few things that they got right.

And I very much like Flutter's layered architecture. I would also like to keep using widgets. So perhaps the answer is to just improve extensibility of Dart & Flutter, which for me would be:

Make code generation more seamless - it may be possible to implement SwiftUI magic in Dart, but the usual setup required is just too big and too slow.

If using code generation was as simple as importing package and slapping some annotations, then people who have the discussed issue would just do that and stop complaining. The rest would continue using good old StatefulWidgets directly.

EDIT: I think flutter generate was a step in good direction, shame that it got removed.

I think this would be a very interesting question to ask in the next Flutter Developer Survey.

It would be a good start. Divide this problem in different parts/questions and see if this is a real problem that Flutter developers wish to be solved.

Once that it is clear, this conversation will be more fluent and enriching

From my perspective, I feel like I spent hours writing detailed comments showcasing the different problems and answering questions, but my comments were dismissed and the same question was asked again.

If I'm asking the same questions it's because I don't understand the answers.

For example, going back to your earlier comment (https://github.com/flutter/flutter/issues/51752#issuecomment-670959424):

The problem debated is:

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

This code is not readable.

I really don't see what isn't readable about it. It explains exactly what is happening. There's four widgets, three of the widgets have builder methods, one just has a string. I would personally not omit the types, I think that makes it harder to read because I can't tell what the variables all are, but it's not a huge problem.

Why is this unreadable?

To be clear, clearly you do find it unreadable, I'm not trying to say that you're wrong. I just don't understand why.

We could fix the readability issue by introducing a new keyword which changes the syntax into:

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

This code is significantly more readable, is unrelated to hooks, and doesn't suffer from its limitations.

It's certainly less verbose. I'm not sure it's any more readable, at least for me. There are more concepts (now we have both widgets and this "keyword" feature); more concepts means more cognitive load. (It's also potentially less efficient, depending on how much these objects are independent; e.g. if the animation always changes more often than the value listenable and stream, now we're rebuilding the ValueListenableBuilder and the StreamBuilder even though normally they wouldn't be triggered; also the initializer logic now has to be entered and skipped every build.)

You've said that the verbosity is not the issue, so it being more terse isn't why it's more readable, I assume (though I'm confused about this too since you did put "too verbose" in the issue title and in the issue's original description). You mentioned wanting less indenting, but you described the version of using builders without indenting as unreadable as well, so presumably it's not the indenting in the original that is the problem.

You say that builders are the peak of reusability and that you just want an alternative syntax but the proposals you've suggested aren't anything like builders (they don't create widgets or elements), so it's not specifically the builder aspect that you are looking for.

You have a solution that you like (Hooks), which as far as I can tell works great, but you want something changed in the framework so that people will use Hooks? Which I don't understand either, because if people don't like Hooks enough to use it as a package, it is probably not a good solution for the framework either (in general, we're moving more towards using packages, even features the Flutter team creates, for what it's worth).

I understand that there's a desire for easier code reuse. I just don't know what that means.

How does the following compare in readability to the two versions above?

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 If there is too much friction around our current codegen solution, please don't hesitate to file a bug asking for improvements there.

@jamesblasco I don't think there's any doubt that there's a real problem here that people want solved. The question for me is exactly what that problem is, so that we can design a solution.

I could answer the concerns about the hooks flaws or the desired to be included in the code, but I don't think that's what we should focus on right now.

We should first agree on what the problem is. If we don't, I don't see how we could agree on other topics.

I really don't see what isn't readable about it. It explains exactly what is happening. There's four widgets, three of the widgets have builder methods, one just has a string. I would personally not omit the types, I think that makes it harder to read because I can't tell what the variables all are, but it's not a huge problem.

I think a big part of the issue here is that the way you code is drastically different from how most people code.

For example, Flutter and the app example you gave both:

  • do not use dartfmt
  • use always_specify_types

With just these two points, I would be surprised if that represented more than 1% of the community.

As such what you evaluate as readable is likely very different from what most people think is readable.

I really don't see what isn't readable about it. It explains exactly what is happening. There's four widgets, three of the widgets have builder methods, one just has a string. I would personally not omit the types, I think that makes it harder to read because I can't tell what the variables all are, but it's not a huge problem.

Why is this unreadable?

My recommendation would be to analyze where your eye is looking at when searching for a specific thing, and how many steps it takes to get there.

Let's make an experiment:
I will give you two widget trees. One using a linear syntax, the other one with a nested syntax.
I will also give you specific things you need to look for in that snippet.

Is it easier to find the answer to using the linear syntax or the nested syntax?

The questions:

  • What is the non-builder widget returned by this build method?
  • Who creates the variable bar?
  • How many builders do we have?

Using builders:

Widget build(context) {
  return ValueListenableBuilder(
    valueListenable: someValueListenable,
    builder: (context, foo, _) {
      return StreamBuilder(
        stream: someStream,
        builder: (context, baz) {
          return TweenAnimationBuilder(
            tween: Tween(...),
            builder: (context, bar) {
              return Container();
            },
          );
        },
      );
    },
  );
}

Using a linear syntax:

Widget build(context) {
  final foo = keyword ValueListenableBuilder(valueListenable: someValueListenable);
  final bar = keyword StreamBuilder(stream: someStream);
  final baz = keyword TweenAnimationBuilder(tween: Tween(...));

return Image(); }


In my case, I have a hard time looking through the nested code to find the answer.
On the other hand, finding the answer with the linear tree is instantaneous

You mentioned wanting less indenting, but you described the version of using builders without indenting as unreadable as well, so presumably, it's not the indenting in the original that is the problem.

Was the StreamBuilder split in multiple variables a serious suggestion?
From my understanding, this was a sarcastic suggestion to make an argument. Was it not? Do you really think that this pattern would lead to more readable code, even on large widgets?

Ignoring the fact that the example does not work, I don't mind breaking it down to explain why it isn't readable. Would that be valuable?

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

That looks better.
But that is assuming people do not use dartfmt

With dartfmt, we have:

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

which is almost no different from the original code.

You say that builders are the peak of reusability and that you just want an alternative syntax but the proposals you've suggested aren't anything like builders (they don't create widgets or elements), so it's not specifically the builder aspect that you are looking for.

That's an implementation detail.
There's no particular reason for having an element or not.
In fact, it may be interesting to have an Element, so that we could include LayoutBuilder and potentially GestureDetector.

I think it's low priority. But in the React community, among the different hooks libraries, I've seen:

  • useIsHovered – returns a boolean that tells whether the widget is hovered
  • useSize – (Probably should be useContraints in Flutter) which gives the size of the associated UI.

(It's also potentially less efficient, depending on how much these objects are independent; e.g. if the animation always changes more often than the value listenable and stream, now we're rebuilding the ValueListenableBuilder and the StreamBuilder even though normally they wouldn't be triggered; also the initializer logic now has to be entered and skipped every build.)

That depends on how the solution is solved.

If we go for a language fix, this issue would not be a problem at all.

We could make that:

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

"compiles" into:

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

If we are using hooks, then flutter_hooks comes with a HookBuilder widget, so that we can still split things when we need to.
Similarly, it would need proper benchmarks to determine whether it really is an issue, especially in the example made here.

With hooks, we are rebuilding only one Element.
With Builders the rebuild is split across multiple Elements. That adds some overhead too, even if small.

It is not impossible that it is faster to re-evaluate all the hooks. It appears that this was the conclusion the React team they came up with when designing hooks.
This may not apply to Flutter though.

Why is this unreadable?

Because of the nesting - nesting makes it harder to quickly scan through and know which parts you can ignore and which are essential for understanding of what is going on. The code is kinda “sequential” in nature but nesting hides this. Nesting also makes it hard to work with it - imagine you want to reorder two things - or inject a new thing in between two - trivial in truly sequential code, but hard when you need to work with nesting.

This is very similar to async/await sugar vs working with raw Future API, dame continuation based concept underneath (and even arguments for and against are very similar) - yes Future API can be used directly and does not hide anything, but readability and maintainability is certainly not good - async/await is a winner there.

My recommendation would be to analyze where your eye is looking at when searching for a specific thing, and how many steps it takes to get there.

I've been programming for 25 years now in over 10 different languages and that's easily the worse ways to evaluate what makes a code readable. Readability of source code is tricky, but it's more about how well it expresses programming concepts and logic, rather than "where my eyes are looking" or how many lines of code it uses.

Or rather, it seems to me that you guys are focusing too much on readability and less on maintainability.

Your examples are less readable, because the __intent__ of the code is less evident and different concerns being hidden away into the same place makes it harder to maintain.


final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);

What would the value even be? A widget? A string variable? I mean it is used inside a
return Text('$value $value2 $value3');

Basically what you want is that by referencing variable A in the build method of widget B, it should cause B to rebuild whenever the value of A changes? That's literally what mobx does, and it does it exactly with the right amount of magic/boilerplate.


final value2 = keyword StreamBuilder(stream: someStream);

What should this return? A widget? A stream? A String value?

Again, it looks like a string value. So you want to be able to simply reference a stream in a build method, cause that widget to rebuild whenever the stream emits a value and have access to the emitted value and create/update/dispose of the stream whenever the widget is created/updated/destroyed? In one single line of code? Inside the build method?

Yes, with mobx you can have your build methods look exactly like your "more readable" example (except you reference observables). You still have to write the actual code that does all the work, just like you do with hooks. The actual code is about 10 lines, and it is reusable in any widget.


final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

A class called "TweenAnimationBuilder" returning a string?! I'm not even going near why this is a terrible idea.

There is no difference in indent/readability between:

Future<double> future;

AsyncSnapshot<double> value = keyword FutureBuilder<double>(future: future);

and:

Future<double> future;

double value = await future;

Both do the exact same thing: Listening to an object and unwrapping its value.

I really don't see what isn't readable about it. It explains exactly what is happening. There's four widgets, three of the widgets have builder methods, one just has a string. I would personally not omit the types, I think that makes it harder to read because I can't tell what the variables all are, but it's not a huge problem.

The same argument could be applied to Promise/Future chains.

foo().then(x =>
  bar(x).then(y =>
    baz(y).then(z =>
      qux(z)
    )
  )
)

vs

let x = await foo();
let y = await bar(x);
let z = await baz(y);
await qux(z);

One could say that the first way of writing makes it clear that Promises are being created under the hood, and how exactly the chain is formed. I wonder if you subscribe to that, or if you consider Promises to be fundamentally different from Builders in that they deserve a syntax.

A class called "TweenAnimationBuilder" returning a string?! I'm not even going near why this is a terrible idea.

You can make the same argument about Promises/Futures, and say that await obscures the fact that it returns a Promise.

I should note that the idea of "unwrapping" things via syntax is hardly new. Yes, in the mainstream languages it came via async/await, but, for example, F# has computational expressions, similar to do notation in some hardcore FP languages. There, it has a lot more power and is generalized to work with any wrappers that satisfy certain laws. I'm not suggesting adding Monads to Dart, but I think it's worth bringing up that there is definitely precedent for type-safe syntax for "unwrapping" things that doesn't necessarily correspond to asynchronous calls.

Taking a step back, I think one thing many people here are struggling with (myself included) is this question about readability. As @rrousselGit has mentioned, there's been a lot of examples throughout this thread of readability issues with the current Builder based approach. To many of us, it seems self evident that this:

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

is significantly less readable than this:

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

But it's clearly not self evident, since @Hixie and @Rudiksz aren't convinced (or are actively opposed) to the idea that the second is more readable than the first.

So here's my breakdown (for whatever small amount it's worth) about why the second code block is more readable than the first:

1. The first code bock is significantly more indented than the second

In my experience, indentation typically equates to asynchronicity, branching, or callbacks, all of which require more cognitive load to think through than non indented, linear code. The first code block has several layers of indentation, and as such it takes me a non trivial amount of time to work through what's happening here, and what's ultimately being rendered (a single Text). Maybe other people are better at working through that indentation in their minds.


In the second code block, there's no indentation which alleviates the problem.

2. The first code block requires more syntax to express its intent

In the first code block, there's three return statements, three builder statements, three lambda headers, three contexts and finally three values. Ultimately what we care about is those three values - the rest is boilerplate to get us there. I actually find this to be the most challenging part of this code block. There's so much going on, and the parts I actually care about (the values being returned by the builders) are such a small part that I spend most of my mental energy grokking the boilerplate as opposed to focusing on the parts that I actually need (again, the values).


In the second code block, there's a huge reduction of boilerplate so I can focus on the part I care about - again, the values.

3. The first code block hides the most important part of the build method in the deepest part of the nesting

I recognize that all of the portions of this build method are important, but I've found that when I'm reading this style of declarative UI code, the thing I'm usually looking for is whatever is displayed to the user, which in this case is the Text widget embedded in the deepest nested builder. Rather than being front and center, that Text widget is buried in multiple layers of indentation, syntax, and intent. If you throw a Column or a Row in one of these layers it becomes even more deeply nested, and at that point you don't even have the benefit of just tracing to the most indented section.


In the second code block, the actual render-able Widget being returned is at the bottom of the function, which is immediately apparent. Furthermore, I've found that when you have something like the syntax OP proposed, you can count on the visual Widget always being at the bottom of the function, which makes the code much more predictable and easy to read.

Regarding nesting, there's a difference between nesting expressing a _tree_ and nesting expressing a _sequence_.

In the case of normal View -> Text nesting and such, nesting is important because it represents the parent-child relationships on the screen. For features like Context (not sure if Flutter has it), it represents the scope of contexts. So the nesting itself has important semantic meaning in those cases and cannot be disregarded. You can't just swap parent and child's places and expect the result to be the same.

However, with nesting of Builders (aka "Render Props" in React), or nesting of Promises, the nesting is supposed to communicate a sequence of transformations / augmentations. The tree itself is not as important — for example, when nesting independent ABuilder -> BBuilder -> CBuilder, their parent-child relationships don't convey additional meaning.

As long as all three values are available in the scope below, their tree structure is not really relevant. It is conceptually "flat", and the nesting is only an artifact of the syntax. Of course, they may use each other's values (in which case their order matters), but this is the case with sequential function calls too, and that can be done without any nesting.

This is why async/await is compelling. It removes additional information (parent-child relationship of Promises) that describes a low-level mechanism, and instead lets you focus on the high-level intent (describing a sequence).

A tree is a more flexible structure than a list. But when each parent only has one child, it becomes a pathological tree — essentially, a list. Async/Await and Hooks recognize we're wasting syntax on something that doesn't convey information, and remove it.

This is actually interesting because I earlier said "this is not about the boilerplate" and now it seems like I'm contradicting myself. I think there's two things here.

By itself, Builders (or at least Render Props in React) are the solution (AFAIK) to the "reusable logic" problem. It's just that they're not very ergonomic if you use a lot of them. You're naturally discouraged by the indentation to use more than 4 or 5 of them in the same component. Each next level is a readability hit.

So the part that seems unsolved to me is reducing the reader cost of nesting. And that argument is precisely the same argument as for async / await.

It's not as readable for the following reasons:

  • Excessive use of whitespace does not scale well. We have only so many lines on our monitor, and forcing scrolling reduces readability and increases congitive load. Imagine that we already have a 60 line widget tree, and you just forced an additional 15 on me for builders, not ideal.
  • It wastes Hz space, which we are limited on leading to extra wrapping, which further wastes line space.
  • It pushes the leaf node, aka the content, further from the left side, and into the tree where it is harder to spot with a glance
  • It is significantly more difficult to identify the 'key players' or 'non boilerplate' at a glance. I have to "find the important stuff" before my reasoning can even begin.

Another way to look at this is to simply highlight the non-boilerplate code, and whether it's grouped together for your eye to easily feast upon, or scattered everywhere for your eye to have to scan around:

With the highlighting this is very easy to reason about. Without it, I need to read the entire chunk of verbosity before I can figure out who is using what and where:
image

Now compare to this, the highlighting is basically redundant, cause there is nowhere else for my eye to go:
image

Worth noting there is probably a disagreement in readability vs grokability. @Hixie likely spends his time in monolithic class files, where he must constantly read and understand massive trees, whereas your typical App Developer is much more about building hundreds of smaller classes, and when you're managing many small classes grok-ability is key. It's not so much that the code is readable when you slow down and read it, but it's can I tell what this is doing at a glance, so I can jump in and tweak or fix something.

For reference, the equivalent of Context in React is InheritedWidgets/Provider

The only difference between them is, in React before hooks we _had_ to use the Builder pattern to consume a Context/Inheritedwidget

Whereas Flutter has a way to bind the rebuild with a simple function call.
So there is no need for hooks to flatten trees using InheritedWidgets — which kind of works around the problem of Builders

That's probably one of the reasons why the discussion is harder, since we need Builders less often.

But it's worth mentioning introducing a hook-like solution would solve both https://github.com/flutter/flutter/issues/30062
and https://github.com/flutter/flutter/issues/12992

It also appears that @Hixie is more used to reading deep nested trees because Flutter is basically all trees, much more so than other languages in my opinion. As one of the main developers on Flutter itself, of course, he would have much more experience with that. Flutter essentially can be thought of as a left to right framework, with deep nesting, much like HTML which I suppose @Hixie has had experience with, having created the HTML5 spec. This to say, the rightmost point of the code block is where the main logic and return value reside.

However, most developers are not, or come from more top to bottom languages, where the logic is, again, read from top to bottom, rather than in nested trees; it is at the bottom-most point of the code block. Therefore, what's readable to him is not necessarily so with many other devs, which is potentially why you see the dichotomy of opinions on readability here.

Another way to look at it, is how much code does my brain need to visually redact. To me this accurately represents the "grunt work" my brain must do before I can parse what is being returned from the tree:
image

Simply put, the builder version has a 4x taller vertical footprint while adding literally no additional information or context, AND packs the code in a far more sparse/scattered way. In my mind, that is an open and shut case, it is objectively less readable for that reason alone, and that is not even considering additional cognitive load around indentation and lining up curly braces which we have all dealt with in flutter.

Think of my eye as a hungry cpu, which is more optimized for processsing? :)

In the case of normal View -> Text nesting and such, nesting is important because it represents _the parent-child relationships_ on the screen. For features like Context (not sure if Flutter has it), it represents the scope of contexts. So the nesting itself has important semantic meaning in those cases and cannot be disregarded. You can't just swap parent and child's places and expect the result to be the same.

Totally agree and I mentioned this earlier. Semantically it makes zero sense to be creating additional context layers in a visual display tree, because I'm using additional non-visual controllers that have state. Using 5 animators, now your widget is 5 layers deep? Just at that high level alone the current approach kinda smells.

There are two issues jumping out at me here.

  1. I suspect there's some disagreement over how difficult/explicit it should be when using some expensive resource. Flutter's philosophy is they it should be more difficult/explicit so that the developer thinks seriously about when and how to use them. Streams, animations, layout builders, etc. represent non-trivial costs that could be used inefficiently if they're too easy.

  2. Build is sync, but most interesting things you deal with as an app developer are async. Of course we can't make build async. We created these conveniences like Stream/Animation/FutureBuilder but they don't always work well enough for what a developer needs. It's probably telling that we don't use Stream or FutureBuilder much in the framework.

I don't think the solution is to tell developers to just always write custom render objects when working with async operations of course. But in the examples I'm seeing in this bug, there are mixtures of async and sync work that we can't just await. Build has to produce something on every call.

fwiw, the React team addressed re-use the readability issue as the 1 motivation:
Hooks Motivation: It’s hard to reuse stateful logic between components
React doesn’t offer a way to “attach” reusable behavior to a component ... you may be familiar with patterns ... that try to solve this. But these patterns require you to restructure your components when you use them, which can be cumbersome and make code harder to follow.

This is very similar to how Flutter currently offers us no way to natively 'compose state'. It also echo's what happens when we us builders, which is modifying our layout tree and making it more cumbersome to work with, and "harder to follow", said tree.

@dnfield if build must be called each time, perhaps we can make the hooks not in the build method so that build is always sync, ie put them inside the class where initState and dispose are. Are there problems with doing so, from those that write hooks?

You can make the same argument about Promises/Futures, and say that await obscures the fact that it returns a Promise.

No you don't. Await is literally just syntactic sugar for one single feature. Weather you use the verbose Futures, or the declarative syntax, the __intent__ of the code is the same.

The demands here are to move source code that deals with entirely different concerns under the same umbrella, hide all kinds of different behaviour behind a single keyword and claiming that somehow it reduces cognitive load.

That's entirely false, because now every time I use that keyword I need to wonder about wheather the result will do any asynchrounous operation, trigger unnecessary rebuilds, initialize long lived objects, do network calls, read files from the disk or simple return a static value. All these are very different situations and I would have to be familiar with the flavor of the hook I'm using.

I understand from the discussion that most developers here don't like to be bother with these kinds of details and want easy development, by just being able to use these "hooks" without having to worry about the implementation details.
Using this so called "hooks" willy-nilly without understanding their full implication will lead to inefficient and bad code, and will cause people to shoot themselves in the foot - therefore it doesn't even solve the issue of "protecting beginner developers".

If your use cases are simple then yeah, you can use hooks willy-nilly. You can use and nest builders all you want, but as your app becomes complex that you find yourself in difficulty reusing code, I would think that having to pay more attention to your own code and architecture is warranted. If I was building an app for potentially millions of users I would be very hesitant to use "magic" that abstracts away important details from me. Right now I find Flutter's API just right to be simple for very simple use cases, and still flexible to allow anybody to implement any kind of complex logic in a very efficient way.

@Rudiksz Again, no one is forcing you to move to hooks. The current style will still be there. What is your argument in the face of this knowledge?

And anyway, people can still write efficient code once they see that multiple hooks are somehow blocking their app; they'll see it when they profile or even just run the app, much as you would with the current style.

@Rudiksz Again, no one is forcing you to move to hooks. The current style will still be there. What is your argument in the face of this knowledge?

Oh dear, this same argument applies to also the people complaining about issues with the framework. No one is forcing them not to use the hooks package.

I'm going to be very blunt here.
This issue trully isn't about hooks, statefulwidget and who uses what, but rather about reverting decades of best practices in order for some people to be able to write 5 lines of code less.

Your argument doesn't really work. The reason this issue was created was because the flutter_hooks package doesn't do everything that could be possible with having something in the framework, while the current model is, by virtue of already being in the framework natively. The argument is to move features of flutter_hooks into the framework natively. Your argument posits that whatever I can do with the current model, I can also do with the hooks package, which is untrue, it seems like from others in this discussion. If they were true, then it would work, which would also mean that hooks were in the framework natively, and therefore, again, since hooks and non-hooks would be equivalent, you can use the current model just as well as the hooks based model, which is what I was arguing.

I'm not sure where your best practices are coming from, as I know that keeping code easily readable is a best practice, and that excessive nesting is an anti pattern. Which best practices exactly are you referring to?

fwiw, the React team addressed re-use the readability issue as the 1 motivation:
Hooks Motivation: It’s hard to reuse stateful logic between components
React doesn’t offer a way to “attach” reusable behavior to a component ... you may be familiar with patterns ... that try to solve this. But these patterns require you to restructure your components when you use them, which can be cumbersome and make code harder to follow.

I hear everybody raving about how Flutter is so much more amazing than React. Maybe it's because it doesn't do everything the way React does? You can't have it both ways, you can't say Flutter is miles ahead of React and also ask that it does everything exactly like React does.

Whatever solution Flutter decides to use for a given problem should stand on its own merits. I'm not familiar with React but apparently I'm missing out on some really amazing piece of technology. :/

I don't think anyone is arguing that Flutter should do everything like React.

But the fact is, Flutter's widget layer is heavily inspired from React. That's stated in the official documentation.
And as a consequence, Widgets have both the same benefits and the same problems than React Components.

This also means that React has more experience than Flutter on dealing with these issues.
It faced them for longer, and understands them better.

So it shouldn't be surprising that the solutions to Flutter problems are similar to the solutions to React problems.

@Rudiksz Flutter's user API is very similar to React's class based model, even as the internal API may be different (I don't know if they do differ, I don't really run into the internal API much). I do encourage you to try React with hooks to see how it is, as I stated earlier that there seems to be a dichotomy of opinions based almost exclusively on those who have and have not used hook-like constructs in other frameworks.

Given their similarity, it should be no surprise that the solutions to problems looks similar, as said above.

Please, let's try our best to not fight with each others.

The only thing fighting will lead us to is killing this discussion and failing to find a solution.

I hear everybody raving about how Flutter is so much more amazing than React. Maybe it's because it doesn't do everything the way React does? You can't have it both ways, you can't say Flutter is miles ahead of React and also ask that it does everything exactly like React does.

Pointing out that the React team had similar motivations when they came up with hooks, validates the concerns we're expressing here. It certainly validates that there is a problem re-using and combining common stateful logic within this type of component based framework, and also to some degree validates the discussion on readability, nesting and the general problem with "clutter" in your views.

Not raving about anything, I've never even worked in React, and I love Flutter. I can just easily see the problem here.

@Rudiksz we can't be sure if it's performant in practice until we put it in the practice. It is not very easy to decide now.

@Hixie this is an example for a journey that a common flutter user may have for implementing a widget for showing user's nickname from userId both with HookWidget and StatefulWidget.

__hook widget__

String useUserNickname(Id userid) {
  final name = useState("");
  useEffect(async () {
    name.value = "Loading...";
    name.value = await fetchNicknames()[userId;
  }, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__stateful widget__

class UserNickname extends Widget {
  final userId;
  // ... createState() ...
}

class UserNicknameState extends State {

  String nickname= "";

   initState() {
     super.initState();
     fetchAndUpdate();
   }

   fetchAndUpdate() async {
      setState(() { this.nickname = "Loading..." });
      final result = await fetchNicknames()[widget.userId];
      setState(() { this.nickname = result });
    }


 void didUpdateWidget(oldWidget) { 
     if (oldWidget.userId != widget.userId) {
        fetchAndUpdate();
     }
   }

  Widget build(BuildContext context) {
    return Text(this.nickname);
  }
}

so far nothing interesting. both solutions are pretty acceptable ,straightforward and performant.
now we want to use UserNickname inside a ListView. as you can see fetchNicknames returns a map of nick names, not only one nickname. so calling it everytime is redundant. few solutions we can apply here:

  • move calling fetchNicknames() logic to the parent widget and save the result.
  • using a cache manager.

first solution is acceptable but has 2 problems.
1 - it renders UserNickname useless because it is now only a Text widget and if you want to use it somewhere else you have to repeat what you did in the parent widget (which has the ListView). the logic for showing the nickname belongs to the UserNickname but we have to move it separately.
2 - we may use fetchNicknames() in many other sub trees and it is better to cache it for all the app not only one part of the application.

so imagine we choose cache manager and providing a CacheManager class with InheritedWidgets or Provider.

after adding support for caching:

__hook widget__

String useUserNickname(Id userid) {
  final context = useContext();
  final cache = Provider.of<CacheManager>(context);
  final name = useState("");
  useEffect(async () {
    name.value = "Loading...";
    var cachedValue = cache.get("nicknames");
    if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        cache.set("nicknames", result );
        cachedValue = result ;
    }
    final result = cachedValue[widget.userId];
    name.value = result ;
  }, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__stateful widget__

class UserNickname extends Widget {
  final userId;
  // ... createState() ...
}

class UserNicknameState extends State {

  String nickname= "";
  CacheManager cache;

   initState() {
     super.initState();
     fetchAndUpdate();
     this.cache = Provider.of<CacheManager>(context);
   }

   fetchAndUpdate() async {
      setState(() { this.nickname = "Loading..." });
      var cachedValue = this.cache.get("nicknames");
      if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        this.cache.set("nicknames", result );
        cachedValue = result ;
      }
      final result = cachedValue [widget.userId];
      setState(() { this.nickname = result });
    }


 void didUpdateWidget(oldWidget) { 
     if (oldWidget.userId != widget.userId) {
        fetchAndUpdate();
     }
   }

  Widget build(BuildContext context) {
    return Text(this.nickname);
  }
}

we have a socket server that notifies clients when nicknames changes.

__hook widget__

String useUserNickname(Id userid) {
  final context = useContext();
  final cache = Provider.of<CacheManager>(context);
  final notifications = Provider.of<ServiceNotifications>(context);
  final name = useState("");

  fetchData() async {
    name.value = "Loading...";
    var cachedValue = cache.get("nicknames");
    if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        cache.set("nicknames", result );
        cachedValue = result ;
    }
    final result = cachedValue[widget.userId];
    name.value = result ;
   }

  useEffect(() {
     final sub = notifications.on("nicknameChanges", fetchData);
     return () => sub.unsubscribe();
   }, [])

  useEffect(fetchData, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__stateful widget__

class UserNickname extends Widget {
  final userId;
  // ... createState() ...
}

class UserNicknameState extends State {

  String nickname= "";
  CacheManager cache;
  ServerNotification notifications;
  CancelableOperation sub;

   initState() {
     super.initState();
     fetchAndUpdate();
     this.cache = Provider.of<CacheManager>(context);
     this.notifications = Provider.of<ServerNotification>(context);
     this.sub = notifications.on("nicknameChanges", fetchAndUpdate);
   }

   dispose() {
      super.dispose();
      this.sub.unsubscribe();
   }

   fetchAndUpdate() async {
      setState(() { this.nickname = "Loading..." });
      var cachedValue = this.cache.get("nicknames");
      if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        this.cache.set("nicknames", result );
        cachedValue = result ;
      }
      final result = cachedValue [widget.userId];
      setState(() { this.nickname = result });
    }


 void didUpdateWidget(oldWidget) { 
     if (oldWidget.userId != widget.userId) {
        fetchAndUpdate();
     }
   }

  Widget build(BuildContext context) {
    return Text(this.nickname);
  }
}

so far both implementation are acceptable and good. IMO the boilerplate in the statful one is no problem at all. the problem arises when we need a widget like UserInfo that has both user's nickname and avatar. also we can't use UserNickname widget because we need to show in a sentence like "Welcome [username]".

__hook widget__

useFetchUserNickname(userId) // old code
useUserAvatar(userId) // implementation like `useFetchUserNickname`

class UserNickname extends HookWidget {
  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    final avatar = useUserAvatar(userId);
    return Row(
      children: [Image.network(avatar), Text(nickname)],
    );
  }
}

but for the __stateful widget__ we can't just use the logic that we wrote. we have to move the logic into a class (like a Property that you suggested) and still we need to write the widget glue with property class again in the new widget.

if you see the changes in the first 3 example, we didn't change the widget itself at all because the only necessary changes were in the state logic and the only place that did change was all, state logic.
this gave us a clean (opinionated), composable and totally reusable state logic that we can use anywhere.

IMHO the only problem is calling useUserNickname is scarry because a single function can do this much.
but in my years of experience in react and using flutter_hooks in 2 apps that are in production rn (that are using hooks heavily) proves that not having a good state management (I also tried MobX and other state management solutions but the glue in widget is always there) is much more scarier. I don't need to write 5 page docs for every screen in a frontend app that I may need to add some small feature in few month after the first release for it to understand how a page of my app works. App calls the sever too much? easy task I go the related hook and change it and the whole app fixes because whole app uses that hook. we can have similar things in the apps without using hooks with a good abstraction but what I'm saying is that hooks are that good abstraction.

I'm pretty sure @gaearon can word it better than me. (if he agrees with me ofc)

seeing example above, none of the methods above (stateful and hook widget) are more performant that the other. but the point is one of them encourages people to write the performant code.

also it's possible to update only the subtree that we need to update like StreamBuilder when there is too many updates (e.g animations) with:

1 - simply creating a new widget which totally viable option for both HookWidget and StatefulWidget/StatelessWidget
2 - using something similar to HookWidgetBuilder in flutter_hooks package because parent and child widgets data are very tightly coupled.

Side note: I really appreciate @Hixie and @rrousselGit for discussing this topic and putting this much energy in this issue. I really looking forward for the result of these talks.

I am coming up with something pretty cool/elegant I think, based on @Hixie's starting point. Not quite ready to share yet, but it is allowing me to create some pretty decent code samples, that I think will be easier to compare apples:apples rather than hooks which looks so foreign.

So, imagine we have a StatefulWidget with this signature:

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

  @override
  _ExampleSimpleState createState() => _ExampleSimpleState();
}

If we were to implement the state using vanilla animator controllers, we get something like:

class _ExampleSimpleVanillaState extends State<ExampleSimpleVanilla> with SingleTickerProviderStateMixin {
  AnimationController _anim1;
  AnimationController _anim2;
  AnimationController _anim3;

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

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: _anim2.value * 20, horizontal: _anim3.value * 30,),
      color: Colors.red.withOpacity(_anim1.value),
    );
  }

  @override
  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);
  }

  @override
  void dispose() {
    _anim1.dispose();
    _anim2.dispose();
    _anim3.dispose();
    super.dispose();
  }
}

If we create it using a StatefulProperty, we yield something more like this:

class _ExampleSimpleState extends State<ExampleSimple> with StatefulPropertyManager {
  StatefulAnimationProperty _anim1;
  StatefulAnimationProperty _anim2;
  StatefulAnimationProperty _anim3;

  @override
  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);
  }

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

Some notes on the differences here:

  1. Off the top, one is 20 lines, the other is 45. One is 1315 chars the other is 825. Only 3 lines and 200 characters matter in this class (what is happening in build), so this is already a massive improvement in signal:noise ratio (ie, is my eye directed to the important bits)
  2. The vanilla options has multiple points where bugs can be created. Forget to dispose, or forget to handle didChange, or make a mistake in didChange, and you have a bug in your code base. This gets worse, when multiple types of controllers are used. Then you have single functions tearing down objects of all different types, which won't be named nice and sequentially like this. That gets messy and is quite easy to make mistakes or miss entries.
  3. The vanilla option provides no method to re-use common patterns or logic, like playOnInit, so I have to duplicate this logic, or create some custom function in very single class that wants to use an Animator.
  4. There is no need to understand SingleTickerProviderMixin here, which is 'magic' and obfuscated what a Ticker is for me for months (in hindsight, I should've just read the class, but every tutorial just says: Add this magic mixin). Here you can look directly at the source code for StatefulAnimationProperty, and see how the animation controllers uses a ticker provider directly and in context.

You do have to instead understand what StatefulPropertyManager does, but crucially this learned once and applied to objects of any type, SingleTickerProviderMixin is mainly specific to using Animator Controllers, and every controller may have it's own mixing to make usage easier, which gets messy. Just having discrete "StatefulObjects" that know all this stuff (just like a builder does!), is much cleaner and scales better.

The code for StatefulAnimationProperty would look something like this:

class StatefulAnimationProperty extends BaseStatefulProperty<StatefulAnimationProperty> implements TickerProvider {
  final Duration duration;
  final TickerProvider vsync;
  final bool playOnInit;

  StatefulAnimationProperty({@required this.duration, @required this.vsync, this.playOnInit = false});

  AnimationController get controller => _controller;
  AnimationController _controller;

  Ticker _ticker;

  @override
  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);
  }

  @override
  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);
  }

  @override
  void update(StatefulAnimationProperty old) {
    if (duration != old.duration) {
      _controller.duration = duration;
    }
    if (vsync != old.vsync) {
      _controller.resync(vsync);
    }
    super.update(old);
  }

  @override
  void dispose() {
    _controller?.dispose();
    _ticker?.dispose();
    super.dispose();
  }
}

Finally, worth noting, readability can be made even better with use of extensions, so we could have something like:

  void initStatefulProperties({bool firstRun = false}) {
    _anim1.init(duration: widget.duration1, playOnInit: true);
    _anim2.init(duration: widget.duration2, playOnInit: true);
    _anim3.init(duration: widget.duration3, playOnInit: true);
    super.initStatefulProperties(firstRun: firstRun);
  }

[Edit] As if to make my own point, my vanilla example has a bug. I forgot to pass the correct duration to each animator in the didUpdateWidget. How long would it have taken us to find that bug in the wild, if no one noticed in code review?? Did anyone not spot it while reading? Leaving it in cause it's a perfect example of what happens in the real world.

Here's a birds eye view, with boilerplate marked in red:
image

This wouldn't be so bad if it was pure boilerplate, and the compiler yelled at you if it was missing. But it's all optional! And when ommitted, creates bugs. So this is actually very bad practice and not DRY at all. This is where builders come in, but they are only good for simplistic use cases.

What I think is very interesting about this, is how a hundred lines and a simple mixin to State, renders a bunch of existing classes redundant. There is virtually no need now to ever use the TickerProviderMixins for example. TweenAnimationBuilder need almost never be used, unless you actually _want_ to create a sub-context. Many traditional pain points like managing focus controllers and textInput controllers are eased substantially. Using Streams becomes way more attractive and less cludgy. Across the code base, use of Builders could be reduced in general which will lead to more easily grokable trees.

Also makes it _extremely_ easy to make your own custom state objects, like the FetchUser example listed earlier, which currently basically requires a builder.

I think this would be a very interesting question to ask in the next Flutter Developer Survey.

It would be a good start. Divide this problem in different parts/questions and see if this is a real problem that Flutter developers wish to be solved.

Once that it is clear, this conversation will be more fluent and enriching

The Emoji reactions under each comment gives a clear idea on if community sees this as a problem or not. The opinion of developers who read 250+ long comments for this issue mean a lot imho.

@esDotDev That's similar to some of the ideas I've been toying with, though I like your idea of just having the property itself be the ticker provider, I hadn't considered that. One thing that your implementation is missing that I think we'd need to add is handling of TickerMode (which is the point of the TickerProviderStateMixin).

The main thing I'm struggling with is how to do this in an efficient way. For example, ValueListenableBuilder takes a child argument that can be used to measurably improve performance. I don't see a way to do that with the Property approach.

@Hixie
I understand that efficiency losses with approaches like this seem to be inevitable. But I like the Flutter mindset of optimize after you profile your code. There are a lot of applications which would benefit from the clarity and conciseness of the Property approach. The option to profile your code, and refactor into builders or separate out a piece of the widget into it's own widget are always there.

The documentation would just need to reflect the best practices and make clear the tradeoffs.

The main thing I'm struggling with is how to do this in an efficient way. For example, ValueListenableBuilder takes a child argument that can be used to measurably improve performance. I don't see a way to do that with the Property approach.

Hm, I think the entire point of the Properties is for non-visual objects. If something wants to have a context slot in the tree, then that thing should be a builder (actually these are the only things that should now be builders I think?)

So we would have a StatefulValueListenableProperty that we use most of the time when we just want to bind the entire view. We then also have a ValueListenableBuilder in the chance that we want some sub-section of our tree to rebuild.

This also addresses the nesting issue, as using a builder as a leaf node, is not nearly as disruptive to readability, as nesting 2 or 3 at the top of your widget tree.

@TimWhiting A big part of Flutter's design philosophy is to guide people towards the right choice. I would like to avoid encouraging people to follow a style which they'd then have to move away from to get better performance. It may be that there is no way to address all the needs at once, but we should definitely give it a go.

@Hixie
What about something like this for builders?

class _ExampleSimpleState extends State<ExampleSimple> with StatefulPropertyManager {
  StatefulAnimationProperty _anim1;
  StatefulAnimationBuilderProperty _anim2;

  @override
  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);
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red.withOpacity(_anim1.controller.value),
      child: _anim2(child: SomeChildWidget()),
    );
  }
}

Can you elaborate? I'm not sure I understand the proposal.

I think he's saying that the StatefulProperty could provide an optional build method for properties that have some visual component:

return Column(
   children: [
      TopContent(),
      _valueProperty.build(SomeChildWidget()),
   ]
)

Which is quite 🔥 imo,

Yes, I don't know if that would work, but the build method would take a child just like a regular builder, except the other properties of the builder are set by the property.
If you need the context from the builder, than the build method accepts a builder argument that provides the context.

Under the hood the method might just create a normal builder with the specified properties, and pass the child argument to the normal builder and return that.

Suppose you had this code:

Widget build(BuildContext context) {
  return ExpensiveParent(
    child: ValueListenableBuilder(
      valueListenable: foo,
      child: ExpensiveChild(),
      builder: (BuildContext context, value, Widget child) {
        return SomethingInTheMiddle(
          value: value,
          child: child,
        );
      }
    ),
  );
}

...how would you convert that?

@esDotDev I like your idea of just having the property itself be the ticker provider, I hadn't considered that.

Ya one of the strongest aspects of this hook-style approach, is that you can _fully_ encapsulate the stateful logic, whatever that may be. So in these case the full list for AC is:

  1. Create the AC
  2. Give it a ticker
  3. Handle widget changes
  4. Handle cleanup of ac and ticker
  5. Rebuild view on tick

Currently things are split with developers handling (or not handling) 1,3,4 manually and repetitively, and the semi-magical SingleTickerProviderMixin taking care of the 2 and 5 (with us passing 'this' as vsync, which confused me for months!). And SingleTickerProviderMixin itself is clearly an attempted fix for this type of problem, otherwise why not just go all the way and have us implement TickerProvider for each class, it would be much more clear.

Suppose you had this code:

Widget build(BuildContext context) {
  return ExpensiveParent(
    child: ValueListenableBuilder(
      valueListenable: foo,
      child: ExpensiveChild(),
      builder: (BuildContext context, value, Widget child) {
        return SomethingInTheMiddle(
          value: value,
          child: child,
        );
      }
    ),
  );
}

...how would you convert that?

class _ExampleSimpleState extends State<ExampleSimple> with StatefulPropertyManager {
  StatefulValueListenableBuilder _fooBuilder;

  @override
  void initStatefulProperties({bool firstRun = false}) {
    _fooBuilder = initProperty(StatefulValueListenableProperty(valueListenable: widget.foo)); 
    super.initStatefulProperties(firstRun: firstRun);
  }

  @override
  Widget build(BuildContext context) {
    return ExpensiveParent(
      child: SomethingInTheMiddle(
        _fooBuilder.value,
        _fooBuilder.builder(childBuilder: () => ExpensiveChild()),
      ),
    );
  }
}

@Hixie
Thanks for the example. I gave it my best shot. I might have missed something.

Important to note would be that the builder caches the child. The question would be when does it need to actually rebuild the child? I think that was the question you were trying to raise..

@Hixie have you seen https://github.com/flutter/flutter/issues/51752#issuecomment-671104377
I think there are some really good points.
I build today something with a lot of ValueListenableBuilder and I can only say its not nice to read.

@Hixie
Thanks for the example. I gave it my best shot. I might have missed something.

I don't think this works because the Property binds to the state it is defined in, so ExpensiveParent is always getting rebuilt here. Then I think the caching of the child is also problematic, as in the Builder example it would know to only rebuild the child when the parent state is built, but in this method the Property does not know when to invalidate it's cache (but maybe this is solvable?)

But tbh, this is the perfect use case for builders, when you want to introduce a new context. I think it's quite elegant to just have the context of StatefulProperties (pure state) and StatefulWidgets (mix of state and layout).

Anytime you are intentionally creating a sub-context, you will by definition be doing it further down your tree, which helps combat one of the main drawbacks to builders (forced nesting across the entire tree)

@escamoteur (and @sahandevs who wrote that comment) yeah I was studying that earlier. I think it certainly helps show the kind of logic that people want to remove. I think, though, that the example itself is a bit dubious in that I would expect most of the logic (e.g. everything around caching) to be in the app state business logic, and nowhere near the widgets. I also can't see a good way to get the syntax as brief as proposed in that comment without breaking hot reload (e.g. if you change the number of hooks you're using, it's not clear how they could be kept stateful across a reload).

That said, I think the work @esDotDev and @TimWhiting show above is very interesting and could solve these problems. It's not as brief as Hooks, but it is more reliable. I think it would make perfect sense to package something like that up, it could even be a Flutter Favorite if it works well. I'm not sure it makes sense as a core framework feature because the improvement is not _that_ substantial once you take into account the complexity around building properties and the performance impact, and how different people would prefer different styles. At the end of the day, it should be fine for different people to use different styles, but we wouldn't want the core framework to have multiple styles, that's just misleading for new developers.

There's also an argument to be made that the right way to learn Flutter is to first understand widgets, and then learn the tools that abstract them away (Hooks or whatever), rather than jumping right to abstract syntax. Otherwise you are missing a key component of how the system works which is likely to lead you astray in terms of writing performant code.

I also can't see a good way to get the syntax as brief as proposed in that comment without breaking hot reload (e.g. if you change the number of hooks you're using, it's not clear how they could be kept stateful across a reload).

Hooks do work with hot-reload without issue.
The first hook with a non-matching runtimeType cause all the subsequent hooks to be destroyed.

This supports adding, removing and reordering.

I think there is an argument that full abstraction is preferable to partial which exists currently.

If I want to understand how Animator works in the context of a Property, I either ignore it totally, or jump in, and it is all right there, self contained and coherent.

If I want to understand how AnimatorController works in context of a StatefulWidget, I need (am forced) to understand the basic lifecycle hooks, but then spared from understanding how the underlying tick mechanism works. This is worst of both worlds in some sense. Not enough magic to make it 'just work', but just enough to confuse new users and force them to just trust blindly in some mixin (which in itself is a new concept for most) and a magical vsync property.

I'm not sure of other examples in the code base, but this would apply to any situation where some helper mixins have been provided for StatefulWidget, but there is still some other bootstrapping that must always be performed. Dev's will learn the bootstraping (the boring part) and ignore the Mixin (the interesting/complex bit)

That said, I think the work @esDotDev and @TimWhiting show above is very interesting and could solve these problems. It's not as brief as Hooks, but it is more reliable

How is this more reliable?

We still can't create/update properties conditionally or outside of its life-cycle as we could enter a bad state. For example, calling a property conditionally will not dispose of the property when the condition is false.
And all properties are still re-evaluated on every rebuild.

But it causes multiple issues, such as forcing users to use ! everywhere after NNBD or potentially allowing users to access a property before it is updated.

For example, what if someone reads a property inside didUpdateWidget?

  • Did initProperties execute before the life-cycle? But then that means we may have to update properties multiple times per build.
  • Did initProperties get executed after didUpdateWidget? Then using properties inside didUpdateWidget may lead to an outdated state

So in the end, we have all the issues of hooks but:

  • we can't use Properties inside `StatelessWidget. So the readability of StreamBuilder/ValueListenableBuilder/... is still an issue – which was the main concern.
  • there are numerous edge-cases
  • it's harder to create custom properties (we can't just extract a bunch of properties into a function)
  • it's harder to optimize rebuilds

In the end, the example given is no different in behavior from:

class Example extends StatelessWidget {
  @override
  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()),
    );
  }
}

But this syntax supports a lot more things, such as:

Early returns:

class Example extends StatelessWidget {
  @override
  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));

    ...
  }
}

which would dispose of value2 when condition switches to false

Extracting bundles of builders into a function:

Widget build(context) {
  final foo = keyword FooBuilder();
  final bar = keyword BarBuilder();

  return Text('$foo $bar');
}

can be changed into:

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

Optimize rebuilds

The child parameter is still feasible:

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

As part of the language, we could even have syntax sugar for this:

Widget build(context) {
  return Scaffold(
    body: {
      final value = keyword TweenAnimationBuilder();
      final value2 = keyword ValueListenableBuilder();

      return Text();
    },
  );
}

Bonus: As a language feature, conditional calls are supported

As part of the language, we can support such scenario:

Widget build(context) {
  String label;

  if (condition) {
    label = keyword LabelBuilder();
  } else {
    label = keyword AnotherBuilder();
  }

  final value2 = keyword WhateverBuilder();

  return ...
}

It's not very useful, but supported – as since the syntax is compiled, it is able to differentiate each usage of keyword by relying on metadata that is not available otherwise.

Regarding readability of builders, here is the previous example, but done with builders. It solves all of the reliability and code-use needs, but look what it has done to my poor widget tree :'(

class _ExampleSimpleBuilderState extends State<ExampleSimpleBuilder> {
  @override
  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),
                      );
                    });
              });
        });
  }
}

It's much much harder (for my eye at least) to spot the code that matters. Also, fwiw, I had to start over like 3 times when writing this, as I continually got confused as to which bracket belonged where, where my semi-colans should go etc. Nested builders are just no fun to write or work inside of. One wrong semicolon and dartfmt bails completely crushing the whole thing.

How is this more reliable?

This is a perfect example of why this _should_ be a core plugin imo. The domain knowledge required here is _deep_. I have the scripting knowledge to implement a simple caching system like this, I do not even have close to the domain knowledge to know every edge case that can happen, or bad states we can get into. Other than Remi, I think there is like 4 developers in the world outside the Flutter team that know this stuff! (exaggerating obviously).

The issue of supporting Stateless Widgets is a good one. On one hand I get it, StatefulWidgets's are oddly verbose. On the other hand, here we truly are talking about pure verbosity. There are no bugs that can happen from having to define 2 classes, there is no way you can mess it up, compiler doesn't let you, there is never anything interesting I want to do in the StatelessWidget. So I'm not sold on this being a major issue... CERTAINLY would be nice to have, but it's the last 5% imo, not something to get stuck on.

On the other other hand... that syntax from remi with keyword support is absolutely beautiful and insanely flexible/powerful. And if it gives you StatelessWidget support for free, then that's just extra 🔥

Supporting StatelessWidget is a big deal IMO. Optional, but still very cool.

While I agree that it is not critical, people are already fighting over using functions instead of StatelessWidget.
Requiring people to use a StatefulWidget to use Builders (as most Builders would likely have a Property equivalent) would only deepen the conflict.

Not only that but, in a world where we can create higher-order functions in dart (https://github.com/dart-lang/language/issues/418), we could get rid of classes altogether:

@StatelessWidget
Widget Example(BuildContext context, {Key key, String param}) {
  final value = keyword StreamBuilder();

  return Text('$value');
}

then used as:

Widget build(context) {
  // BuildContext and Key are automatically injected
  return Example(param: 'hello');
}

This is something that is supported by functional_widget – which is a code-generator where you write a function and it generates a class for you – which also supports HookWidget.

The difference being, having support for higher-order functions in Dart would remove the need for code-generation to support such syntax.

I'm guessing what @Hixie meant by more reliable, is it doesn't suffer from the order of operations / conditional issue that hooks has, as that is very 'unreliable' from an architectural POV (though I realize it's an easy rule to learn and not violate once learned).

But neither does your proposal with the keyword. I think the case for new keyword is quite strong:

  • More flexible and composable than grafting onto State
  • Even more succint syntax
  • Works in Stateless which is a very nice option to have

What I don't like about it, is we're worrying about the cost of setting properties on some simple object multiple times/build, but then advocating a solution that basically will create a million levels of context and a bunch of layout cost. Am I misunderstanding?

The other downside is this idea of magic. But if you're going to do something magical, a new keyword is an effective way to get it done I think, as it makes it easy to highlight and call out to the community, and explain what it is and how it works. It would basically be all anyone talks about for the next yr in Flutter and I'm sure we would see an explosion of cool plugins that spawn out of it.

I'm guessing what @Hixie meant by more reliable, is it doesn't suffer from the order of operations / conditional issue that hooks has, as that is very 'unreliable' from an architectural POV (though I realize it's an easy rule to learn and not violate once learned).

But hooks don't suffer from such issue either, as they are statically analyzable, and we can therefore have a compilation error when they are misused.

This is a non-problem

Similarly if custom errors are a no-go, then as I mentioned previously, Property suffers from the exact same problem.
We can't reasonably write:

Property property;

@override
void initProperties() {
  if (condition) {
    property = init(property, MyProperty());
  }
}

as switching condition from true to false will not dispose of the property.

We can't really call it in a loop either. It doesn't really make sense, since it's a single-time assignment. What is the use-case of running the property in a loop?

And the fact that we can read properties in any order sounds dangerous
For example we could write:

Property first;
Property second;

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

This is such a weird example. Are you sure AnimatedContainer can't already do this?

Of course. The example here is to utilize 3 animations in some widget to do "X". The X is intentionally simplified in the example to highlight the amount of boilerplate.

Don't focus on how I'm using them. In a real example, the widget "core" would be a hundred lines or something, the animated properties would not be so simple, and we'd have multiple handlers and other functions defined. Assume I'm doing something not handled by one of the implicit widgets (not hard since other than AnimatedContainer, they are extremely single-purpose).

The point is that when building something like this, builders don't work great, as they stick you in a readability (and writability) hole to begin with, as such they are well suited for simple use cases, they do no "compose" well. Compose being the composition of 2 or more more things.

Don't focus on how I'm using them. In a real example, ...

...and back to square one. Why don't you bring a real example?

You need a real example of using complex animations?
https://github.com/gskinnerTeam/flutter_vignettes

Showing some arbitrary complex animation would do nothing but obfuscate the example. Sufficed to say, there are plenty of use cases for using multiple animators (or any other stateful object you can imagine) inside some widget

Of course. The example here is to utilize 3 animations in some widget to do "X". The X is intentionally simplified in the example to highlight the amount of boilerplate.

the widget "core" would be a hundred lines or something

In another post you posted an example with boilerplate that obscures the "core", but now you tell us that the core would be hundreds of lines? So in reality, the boilerplate would be minuscule compared to the core? You can;t have it both ways.
You are constantly changing your arguments.

Don't focus on how I'm using them. In a real example, ...

...and back to square one. Why don't you bring a real example?

Probably because it takes a lot of time to create a real example when just playing around with various idea. The intent is for the reader to imagine how it could be used in a real situation, not mention that there are ways around it. Of course one can use an animated container, but what if they couldn't? What if it were too complex to make with just an animated container.

Now, that the writers do not use truly real examples, that can be shown to be either good or bad, I have no opinion on that, I'm only commenting on the tendency in this thread to bring up ameliorations to problems which do not fully solve the problem at hand. This seems to be a major source of confusion between hooks proponents and opponents, as each seem to be talking past another to some extent, so I support Hixie's proposal to create some real apps such that an opponent cannot say that a "real" example was not shown and a proponent cannot say that one should merely imagine a real world scenario.

I think I said was it would be silly to have class that is 100lines, where half of it is boilerplate. Which is exactly what I'm describing here. The core, however large, should not be obfuscated by a bunch of noise, which it certainly is when using multiple builders.

And the reason is scan-ability, readability and maintenance across a large codebase. Its not the writing of the lines, although writing in builders is a productivity loser imo due to the tendancy to get into curly bracket hell.

In another post you posted an example with boilerplate that obscures the "core", but now you tell us that the core would be hundreds of lines? So in reality, the boilerplate would be minuscule compared to the core? You can;t have it both ways.
You are constantly changing your arguments.

Again, this issue is not about boilerplate but readability and reusability.
It doesn't matter if we have 100 lines.
What matters is how readable/maintainable/reusable these lines are.

Even if the argument were about boilerplate, why should I, the user, tolerate such boilerplate in any case, given a sufficiently equivalent way of expressing the same thing? Programming is all about creating abstractions and automating labor, I don't really see the point of redoing the same thing over and over again in various classes and files.

You need a real example of using complex animations?
https://github.com/gskinnerTeam/flutter_vignettes

Surely, you can't expect me to dig through your whole project. Which file exactly should I look at?

Showing some arbitrary complex animation would do nothing but obfuscate the example.

The exact opposite. Showing some arbitrary complex animation that cannot be solved by any existing solution would be the example, and that's what Hixie keeps asking, I believe.

Just scan the gifs and begin to imagine how you might build some of that stuff. That repo is actually 17 standalone apps. You also can't expect me to write you some arbitrary animation just to prove to you that complex animations can exist. I've been building them for 20 years starting in Flash, every single one different than the last. And it's not specific to Animations anyways, they are just the simplest most familiar API to illustrate a larger point.

Like you know how, when you use an animator, there's like 6 things you need to do everytime, but it also needs lifecycle hooks?? Ok, so now extend that to ANYTHING that has 6 steps you have to do everytime... And you need to use it in 2 places. Or you need to use 3 of them at once. It's so obviously an issue on it's face, I don't know what else I can add to explain it.

Programming is all about creating abstractions and automating labor, I don't really see the point of redoing the same thing over and over again in various classes and files.

__All__? So performance, maintainability is not relevant?

There comes a point when "automating" a labor and "doing" a labor is the same.

Folks, it's fine if you don't have the time or inclination to create real examples, but please, if you are not interested in creating examples to explain the problem, you should also not expect people to then feel compelled to solve the problem (which is a lot more work than creating examples to show the problem). Nobody here is required to do anything for anyone, it's an open source project where we're all trying to help each other.

@TimWhiting would you mind putting a license file in your https://github.com/TimWhiting/local_widget_state_approaches repo? Some people are unable to contribute without an applicable license (BSD, MIT, or similar ideally).

why should I, the user, tolerate such boilerplate in any case, given a sufficiently equivalent way of expressing the same thing?

Yes, maintainability and performance do matter of course. I mean when there is an equivalent solution, we should pick the one that has less boilerplate, is easier to read, is more reusable, and so on. That is not to say that hooks are the answer, as I have not measured their performance for example, but they are more maintainable in my experience. I am still unsure about your argument as to how it affects your work if a hook-like construct were put into the Flutter core.

Just scan the gifs and begin to imagine how you might build some of that stuff.

I scanned the gifs. I wouldn't use builder widgets.
Many of the animations are so complex that if I knew you implemented them using the higher level builders, I probably wouldn't use your package.

Anyway this discussion seems to be getting out of hand with more personal disagreements. We should focus on the main task at hand. I am not sure how hook proponents can show smaller examples if opponents will, as I said earlier, find ameliorations that don't truly solve the problem posed. I think we should contribute to @TimWhiting's repository for now.

Showing some arbitrary complex animation that cannot be solved by any existing solution would be the example, and that's what Hixie keeps asking, I believe.

Showing examples of something that is not possible today is out of the scope of this issue.
This issue is about improving the syntax of what is already feasible, not unblocking some things that are not possible today.

Any request at providing something that is not possible today is off-topic.

@TimWhiting would you mind putting a license file in your https://github.com/TimWhiting/local_widget_state_approaches repo? Some people are unable to contribute without an applicable license (BSD, MIT, or similar ideally).

Done. Sorry, I haven't had much time to work on the examples, but I'll probably get to that sometime this week.

Showing some arbitrary complex animation that cannot be solved by any existing solution would be the example, and that's what Hixie keeps asking, I believe.

Showing examples of something that is not possible today is out of the scope of this issue.
This issue is about improving the syntax of what is already feasible, not unblocking some things that are not possible today.

Any request at providing something that is not possible today is off-topic.

Let me rephrase what I said.

Showing some arbitrary complex use case involving animations, states, etc that are difficult to write and that can be improved significanlty without impacting performance would be the example, and that's what Hixie keeps asking, I believe.

I understand the push for less boilerplate, more reusability, more magic. I like having to write less code too, and the language/framework doing more work is quite appetizing.
Until now none of the examples/solution combos presented here would drastically improve code. That is, if we care about more than just how many lines of codes we have to write.

Folks, it's fine if you don't have the time or inclination to create real examples, but please, if you are not interested in creating examples to explain the problem, you should also not expect people to then feel compelled to solve the problem (which is a lot more work than creating examples to show the problem). Nobody here is required to do anything for anyone, it's an open source project where we're all trying to help each other.

@TimWhiting would you mind putting a license file in your https://github.com/TimWhiting/local_widget_state_approaches repo? Some people are unable to contribute without an applicable license (BSD, MIT, or similar ideally).

I spent about 6 hours creating these various examples and code snippets. But I see really no point in providing concrete examples of complex animations just to prove that they can exist.

The request is to basically turn this into something that can not be handled by AnimatedContainer:

Container(margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30), color: Colors.red.withOpacity(value1));

This is so trivial to the point of being almost intentionally obtuse to the issue. Is it so hard to imagine I might have a couple pulsing buttons, some particles moving, maybe a few text fields that fade in while scaling, or some cards that flip? Maybe I'm making a soundbar with 15 independant bars, maybe I'm sliding a menu in but also need the ability to slide individual items back out. And on, and on, and on. And this is just for Animations. It applies to any use case that is burdensome in the context of a widget.

I think I provided excellent cananonical examples of the problem with both builders, and vanilla state-reuse:
https://github.com/flutter/flutter/issues/51752#issuecomment-671566814
https://github.com/flutter/flutter/issues/51752#issuecomment-671489384

You simply have to imagine many of these instances (pick your poison), spread far and wide across a project of 1000+ class files, and you get the perfect picture of the readability and maintainability issues we're trying to avoid.

Would the example images that @esDotDev provided, showing how the nesting makes code harder to read, not be sufficient for you, @Rudiksz ? What is lacking from them? I suppose there are no performance metrics there but @rrousselGit sure they're not less performant than builders.

@esDotDev I think the point is to have a singular canonical example from which all life cycle management solutions can be compared against (not just hooks but others in the future as well). It's the same principle as TodoMVC, you wouldn't necessarily point to various other implementations in React, Vue, Svelte, etc as showing the difference between them, you'd want them all to be implementing the same application, _then_ you can compare.

That makes sense to me, but I don't understand why it needs to be any bigger than a single page.

Managing multiple animations is the perfect example of something that is common, requires a bunch of boilerplate, is error prone, and has no good solution currently. If that's not making the point, if people are going to say that they don't even understand how animations can be complex, then clearly any use-case will get knocked down for the context of the use case, and not the architectural issue we're trying to illustrate.

Sure, the repository from @TimWhiting doesn't have a full blown app, it has singular pages as examples as you say, if you can make a canonical example of animation for that repository from which others could implement their solution, that would work.

I also don't think we need a huge app or anything, but there should be sufficient complexity similar to that of TodoMVC. Basically it needs to be enough such that your opponents couldn't be able to say "well I could do this better in such and such way".

@Hixie The request for real apps to compare approaches is flawed.

There are two flaws:

  • We don't yet agree on the problem, as you said yourself you do not understand it
  • We cannot implement examples in real production conditions, as we will have missing pieces.

For example, we can't write an application using:

final snapshot = keyword StreamBuilder();

as this is not implemented.

We can't judge performances either, as this is comparing a POC vs production code.

We can't evaluate if something like "hooks can't be called conditionally" is error-prone either, as there is no compiler integration to point out errors when there is a misuse.

Judging performance of designs, evaluating API usability, implementing things before we have implementations... those are all part of API design. Welcome to my job. :-) (Flutter trivia: did you know that the first few thousand lines of RenderObject and RenderBox et al were implemented before we created dart:ui?)

That does not change the fact that you are requesting the impossible.

Some of the proposals made here are part of the language or the analyzer. It is impossible for the community to implement that.

I'm not so sure, other frameworks and languages do API design all the time, I don't think it's too different here, or that Flutter has some overwhelming differences or difficulties for API design than other languages. As in, they do it without having compiler or analyzer support, they're just proofs of concept.

I've put together an example of a 'complex' animation scenario that makes good use of 3 animations and it fairly loaded with boilerplate and cruft.

Important to note that I could have just done any animation that needs a hard snap back to starting position (eliminating all implicit widgets), or rotation on z axis, or scale on a single axis, or any other use case not covered by IW's. I worried those might not be taken seriously (though my designers will hand me this stuff all day long) so I built something more 'real world'.

So here is a simple scaffold, it has 3 panels that slide open and closed. It uses 3 animators with discrete states. In this case I don't really need the full control of AnimatorController, TweenAnimationBuilder would do ok, but the resulting nesting in my tree would be very undesireable. I can't nest the TAB's down the tree, as the panels have dependancies on eachothers values. AnimatedContainer is no option here as each panel needs to slide off screen, they do not "squish".
https://i.imgur.com/BW6M3uM.gif
image

class _SlidingPanelViewState extends State<SlidingPanelView> with TickerProviderStateMixin {
  AnimationController leftMenuAnim;
  AnimationController btmMenuAnim;
  AnimationController rightMenuAnim;

  @override
  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.
  @override
  void dispose() {
    btmMenuAnim.dispose();
    leftMenuAnim.dispose();
    rightMenuAnim.dispose();
    super.dispose();
  }

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

  @override
  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));

So, of the 100 lines or so in that body, roughly 40% or so is pure boilerplate. 15 lines in particular, where anything missing or mis-typed, could cause hard-to-spot bugs.

If we use something like StatefulProperty, it would reduce the boilerplate to 15% or so (saves about 25 lines). Critically, this would completely resolve the issue of sneaky bugs and duplicate business logic, but it's still a little verbose especially as it requires StatefulWidget, which is a 10line hit right off the top.

If we use something like 'keyword' we reduce the boilerplate lines to essentially 0%. The entire focus of the class could be on the (unique) business logic and the visual tree elements. We make usage of StatefulWIdgets much more rare in general, the vast majority of the views become 10 or 20% less verbose and more focused.

Also, worth noting that the Panel scenario above is real world, and in real world obviously this approach is really not nice, so we didn't use it, and you would not see it in the code base. Nor would we use nested builders, cause those look gross so you won't see that either.

We built a dedicated SlidingPanel widget, which takes an IsOpen property, and opens and closes itself. This is generally the solution in every single one of these use cases where you need some specific behavior, you move the stateful logic down into some ultra-specific widget, and you use that. You basically write your own ImplicitlyAnimatedWidget.

This does generally work ok, but it's still time and effort, and literally the ONLY reason it exists is because using Animations is so hard (which exists cause re-using stateful components in general is so hard). In Unity or AIR for example, I would not create a dedicate class simply to move a panel on a single axis, it would just be a single line of code to open or close, there would be nothing for a dedicated-widget to do. In Flutter we have to create a dedicated widget, cause it's literally the only reasonable way to encapsulate the bootstraping and teardown of AnimatorController (unless we want to nest,nest,nest with TAB)

My main point here is this type of thing is why real-world examples are so hard to find. As developers we can't let these things exist in our codebases too much, so we work around them with less-than-ideal-but-effective workarounds. Then when you look at the codebase, you just see these workarounds in effect and everything seems fine, but it might not be what the team wanted to make, it may have taken them 20% longer to get there, it may have been a total pain to debug, none of this is evident glancing at code.

For completeness, here is the same use case, made with builders. Line count is heavily reduced, there is no chance for bugs, no need to learn foreign concept RE TickerProviderMixin...but that nesting is sadness, and the way the various variables are sprinkled throughout the tree (dynamic end values, value1, value2 etc) makes the business logic much harder to read than it needs to be.

```dart
class _SlidingPanelViewState extends State {
bool isLeftMenuOpen = true;
bool isRightMenuOpen = true;
bool isBtmMenuOpen = true;

@override
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: Tween(begin: 0, end: isLeftMenuOpen ? 1 : 0),
duration: widget.slideDuration,
builder: (_, leftAnimValue, __) {
return TweenAnimationBuilder(
tween: Tween(begin: 0, end: isRightMenuOpen ? 1 : 0),
duration: widget.slideDuration,
builder: (_, rightAnimValue, __) {
return TweenAnimationBuilder(
tween: Tween(begin: 0, end: isBtmMenuOpen ? 1 : 0),
duration: widget.slideDuration,
builder: (_, btmAnimValue, __) {
double leftPanelSize = 320;
double leftPanelPos = -leftPanelSize * (1 - leftAnimValue);
double rightPanelSize = 230;
double rightPanelPos = -rightPanelSize * (1 - rightAnimValue);
double bottomPanelSize = 80;
double bottomPanelPos = -bottomPanelSize * (1 - btmAnimValue);
return Stack(
children: [
//Bg
Container(color: Colors.white),
//Main content area
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", () => setState(() => isLeftMenuOpen = !isLeftMenuOpen)),
Button("btm", () => setState(() => isBtmMenuOpen = !isBtmMenuOpen)),
Button("right", () => setState(() => isRightMenuOpen = !isRightMenuOpen)),
],
)
],
);
},
);
},
);
},
);
}
}

That last one is interesting... I originally was going to suggest that the builders should be around the Positioned widgets rather than the Stack (and I would still suggest that for the left and right panels) but then I realised that the bottom one affects all three, and I realised that the builder just giving you one child argument actually isn't enough, because you really want to keep both the Container and the Row constant across builds. I suppose you can just create them above the first builder.

My main point here is this type of thing is why real-world examples are so hard to find. As developers we can't let these things exist in our codebases too much, so we work around them with less-than-ideal-but-effective workarounds. Then when you look at the codebase, you just see these workarounds in effect and everything seems fine, but it might not be what the team wanted to make, it may have taken them 20% longer to get there, it may have been a total pain to debug, none of this is evident glancing at code.

For the record, this is not an accident. This is very much by design. Having these widgets be their own widgets improves performance. We very much intended for this to be how people used Flutter. This is what I was referring to above when I mentioned "a big part of Flutter's design philosophy is to guide people towards the right choice".

I originally was going to suggest that the builders should be around the Positioned widgets rather than the Stack (and I would still suggest that for the left and right panels)

I actually ommitted it for the sake of succintness, but normally I would likely want a Content container here, and it would use the sizes from all 3 menus to define it's own position. Meaning all 3 need to be at the top of the tree, and if any rebuild, I need the entire view to rebuild, no getting around it. This is basically your classic desktop-style scaffold.

Of course we could begin tearing apart the tree, always an option, we use it a lot for larger widgets, but I've never once seen that actually improve grokability at a glance, there's an extra cognitive step the reader needs to do at that point. The second you break the tree apart, I am suddenly following a bread crumb trail of variable assignments to figure out what is being passed here and how the whole thing fits together becomes occluded. Presenting the tree as a digestible tree is always easier to reason about from my experience.

Probably a good use case for a dedicated RenderObject.

Or 3 easily managed AnimatorObjects that can hook themselves into the widget lifecycle :D

Having these widgets be their own widgets improves performance.

Since we getting concrete here: In this case because this is a scaffold, every sub-view is already it's own widget, this guy is responsible for just laying out and translating children. The childen would be BottomMenu(), ChannelMenu(), SettingsView(), MainContent() etc

In this case we are wrapping a bunch of self-contained widgets, with another layer of self-contained widgets, simply to manage the boilerplate around moving them. I don't believe this is a performance win? In this case we are being pushed into what the framework _thinks_ we want to do, and not what we actually want to do, which is to write an equally performant view in a more succint and coherent way.

[Edit] I'll update the examples to add this context

The reason I suggest a dedicated RenderObject is that it would make the layout better. I agree that the animation aspect would be in a stateful widget, it would just pass down the three 0..1 doubles (value1, value2, value3) down to the render object instead of having the Stack math. But that's mostly a sideshow for this discussion; at the point where you're doing this, you'd still want to do the simplification afforded by Hooks or something similar in that stateful widget.

On a more relevant note, I had a crack at creating a demo for @TimWhiting's project: https://github.com/TimWhiting/local_widget_state_approaches/pull/1
I'm curious what a Hooks version would look like. I'm not sure I can see a good way to make it simpler, especially maintaining the performance characteristics (or improving them; there's a comment in there showing a place where it's currently suboptimal).

(I'm also very curious if this is the kind of thing where if we found a way to simplify it, we would be done here, or if it's missing critical things that we would need to solve before we were done.)

Is the restoration stuff critical to this example? I'm having a hard time following because of it, and do not really know what RestorationMixin even does. I assume it is... going to take a bit to understand this for me. I'm sure Remi will crank out the hooks version in 4 seconds flat :)

Using the restoration API using HookWidget rather than StatefulHookWidget is not supported at the moment.

Ideally we should be able to change

final value = useState(42);

into:

final value = useRestorableInt(42);

But it needs some thinking, as the current restoration API wasn't really designed with hooks in mind.

As a side note, React hooks comes with a "key" feature, usually used like so:

int userId;

Future<User> user = useMemo(() => fetchUser(id), [id]);

where this code means "cache the result of the callback, and re-evaluate the callback whenever anything inside the array changes"

Flutter_hooks have made a 1to1 reimplementation of this (as it's just a port), but I don't think that's what we would want to do for a Flutter optimized code.

We'd probably want:

int userId;

Future<User> user = useMemo1(id, (id) => fetchUser(id));

which would do the same thing, but remove the memory pressure by avoiding a list allocation and using a function tear-off

It's not critical at this stage, but worth mentioning if we're planning to use flutter_hooks for examples.

@Hixie I ported your animation example to hooks

It was an interesting example, kudos for thinking about it!
It's a good example in that, by default, the implementation of "active" and "duration" is all over the place (and depend on each other at that).
To the point were there are numerous "if (active)" / "controller.repeat" calls

Whereas with hooks, all the logic is handled declaratively and concentrated in a single place, with no duplicate.

The example also shows how hooks can be used to easily cache objects — which fixed the issue of ExpensiveWidget rebuilding too often.
We get the benefits of const constructors, but it works with dynamic parameters.

We also get a better hot-reload. We can change the Timer.periodic duration for the background color and immediately see the changes in effect.

@rrousselGit do you have a link? I didn't see anything new in @TimWhiting's repository.

Using the restoration API using HookWidget rather than StatefulHookWidget is not supported at the moment.

Whatever solution we come up with, we need to make sure it doesn't need to know about every last mixin. If someone wants to use our solution in combination with some other package that introduces a mixin just like TickerProviderStateMixin or RestorationMixin, they should be able to do so.

https://github.com/TimWhiting/local_widget_state_approaches/pull/3

Agreed, but I'm not worried about that. useAnimationController doesn't require users to care about SingleTickerProvider for example.

AutomaritKeepAlive could benefit from the same treatment.
One of the thing I was thinking is to have a "useKeepAlive(bool)" hook

That avoids both the mixin and the "super.build(context)" (the latter being quite confusing)

Another interesting point is the changes required during refactoring.

For example, we can compare the diff between the changes necessary to implement TickerMode for the raw approach vs hooks:

Some other things are mixed up in the diff, but we can see from it that:

  • StatefulWidget required to move the logic to a completely different life-cycle
  • Hooks changes are purely additive. Existing lines were not edited/moved.

Imo this is very important and a key win from this style of self contained state objects. Having everything based on nested contexts in a tree is fundamentally harder and more convoluted to refactor and change, which over the course of a project has an invisible but definite effect on the end quality of the codebase.

Agreed!
That also makes code-reviews a lot easier to read:

final value = useSomething();
+ final value2 = useSomethingElse();

return Container(
  color: value.color,
-  child: Text('${value.name}'),
+  child: Text('${value.name} $value2'),
);

vs:

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

It's not clear in the second diff that Container is unchanged and that only the Text changed

@rrousselGit Do you think it makes sense to try and do a version representing how it could look with keyword support? Is it substantially similar to hooks with just a dedicated 'use' keyword, or does it become even easier to follow? Its hard to compare the hook approach on a even footing cause it has so many foreign concepts like useEffect, useMemo etc which I think make it look more magical than it is?

Another thing to note, is that where this all really would hit home, is if you had multiple widgets, that all needed to share this 'color-stepping' logic, but used the resulting color in totally different ways. With hooks-style approach, we just bundle up whatever logic makes sense to re-use, and we simply use it. There are no architectural corners we are forced into, it's truly agnostic and flexible.

In the Stateful approach, we are forced into

  • copy and pasting the logic (very un-maintainable)
  • using a builder (not super readable, especially when using nesting)
  • mixing (doesn't compose well, very easy for different mixins to conflict in their shared state)

The key thing I think is that in the latter, you are immediately have an architectural problem, where should I put this in my tree, how can I best encapsulate this, should it be a builder, or maybe a custom widget? With the former the only decision is in which file to save this chunk of re-used logic, there is no impact to your tree at all. This is very nice architecturally when you want to use a few of these logic encapsulations together, move them up and down in your hierarchy or move to a sibling widget, etc

I am, by no means, an expert developer. But this new style of reusing logic and write it in one place is really handy.

I haven't use Vue.js in a long time, but they even have their own API (inspired from Hooks) for their next version, it might be worth to take a look.

And CMIIW, I think the rules of react hooks (do not use conditional), does not apply with Composition API. So, you do not need to use linter to enforce the rules.

Again the motivation section strongly reinforces the OP here:

MOTIVATION
The code of complex components become harder to reason about as features grow over time. This happens particularly when developers are reading code they did not write themselves.
[There was a] Lack of a clean and cost-free mechanism for extracting and reusing logic between multiple components.

The key words there being "clean" and "cost free". Mixins are cost-free but they are not clean. Builders are clean, but they are not cost-free in the readability sense, nor in the widget architectural sense as they're harder to move around the tree and reason about in terms of hierarchy.

I also think this is important to note in the readability discussion: "_happens particularly when developers are reading code they did not write_". Of course _your_ nested builder might be easy to read, you know whats there and can comfortably skip over it, it's reading someone else code, like you do on any larger project, or your own code from weeks/mths ago, when it becomes quite annoying/hard to parse & refactor these things.

Some other especially relevant sections.

Why simply having components is not enough:

Creating ... components allows us to extract repeatable parts of the interface coupled with its functionality into reusable pieces of code. This alone can get our application pretty far in terms of maintainability and flexibility. However, our collective experience has proved that this alone might not be enough, especially when your application is getting really big – think several hundreds of components. When dealing with such large applications, sharing and reusing code becomes especially important.

On why reducing logical fragmentation and encapsulating things more strongly is a win:

fragmentation is what makes it difficult to understand and maintain a complex component. The separation of options obscures the underlying logical concerns. In addition, when working on a single logical concern, we have to constantly "jump" around option blocks for the relevant code. It would be much nicer if we could collocate code related to the same logical concern.

I'm curious if there are other proposals for canonical examples that we're trying to improve other than the one I submitted.
If it's the starting point that people want to use then that's great. However, I would say the biggest problem with it right now is verbosity; there's not much code to reuse in that example. So it's not clear to me if it's a good representation of the problem as described in the OP.

I've been thinking some more about how to express the characteristics that I personally would look for in a solution, and it made me realise one of the big problems that I see with the current Hooks proposal, which is one reason I wouldn't want to merge it into the Flutter framework: Locality and Encapsulation, or rather, the lack thereof. The design of Hooks uses global state (e.g. the static to track which widget is currently being built). IMHO this is a design characteristic that we should avoid. In general we try to make APIs self-contained, so if you call a function with one parameter, you should be confident that it won't be able to do anything with values outside of that parameter (this is why we pass the BuildContext around, rather than having the equivalent of useContext). I'm not saying this is a characteristic that everyone would necessarily want; and of course people can use Hooks if that's not a problem for them. Just that it's something I'd like to avoid doing more in Flutter. Every time we've had global state (e.g. in the bindings) we've ended up regretting it.

Hooks could probably be methods on context (I think they were in early version), but honestly, I don't see much value in merging them as they currently are. Merge would need to have some adventages to separate package, like increased performance or hook specific debugging tools. Otherwise they would only add to the confusion, for example you would have 3 official ways of listening a listenable: AnimatedBuilder, StatefulWidget & useListenable.

So to me, the way to go is improving code generation - I have proposed some changes: https://github.com/flutter/flutter/issues/63323

If these suggestions were actually implemented, people who wanted magical SwiftUI-like solutions in their app could just make a package and not bother anyone else.

Discussing the validity of hooks is kind of off-topic at this stage, as we still do not agree on the problem as far I as know.

As stated a few times in this issue, there are many other solutions, multiple of which are language features.
Hooks are merely a port of an existing solution from another tech that is relatively cheap to implement in its most basic form.

This feature could take a path completely different from hooks, such as what SwiftUI or Jetpack Compose do; the "named mixin" proposal, or the syntax sugar for Builders proposal.

I insist on the fact that this issue at its core is asking for a simplification of patterns like StreamBuilder:

  • StreamBuilder has a bad readability/writability due to its nesting
  • Mixins and functions are not a possible alternative to StreamBuilder
  • Copy-pasting the implementation of StreamBuilder in all StatefulWidgets all over is not reasonable

All the comments so far mentioned alternatives of StreamBuilder, both for different behaviors (creating a disposable object, making HTTP requests, ...) or proposing different syntaxes.

I'm not sure what else there is to say, so I don't see how we can progress any further.
What is this that you do not understand/disagree with in this statement @Hixie?

@rrousselGit Could you create a demo app that shows this? I tried to create a demo app that showed what I understood to be the issue, but apparently I did not get that right. (I'm not sure what the difference is between "patterns like StreamBuilder" and what I did in the demo app.)

  • StreamBuilder has a bad readability/writability due to its nesting

You already said verbosity is not the issue. Nesting is just another aspect of being verbose. If nesting is really an issue we should go after the Padding, Expanded, Flexible, Center, SizedBox and all other widgets that add nesting for no real reason. But nesting can be easily solved by splitting up monolithic widgets.

Copy-pasting the implementation of StreamBuilder in all StatefulWidgets all over is not reasonable

You mean copy-pasting the lines of code that create and dispose the streams that the StatefulWidgets need to create and dispose? Yes, it's absolutely reasonable.

If you have 10's or god forbid hundreds of _different_ custom StatefulWidgets that need to create/dispose their own streams - "use" in the hooks terminology-, you have bigger problems to worry about than ""logic" reuse or nesting. I would worry about why my app has to create so many different streams in the first place.

To be fair, I think it's fine for someone to think a particular pattern isn't reasonable in their app. (That doesn't necessarily mean that the framework has to support that natively, but it would be good to at least allow a package to solve it.) If someone doesn't want to either use stream.listen or StreamBuilder(stream), that's their right, and maybe as a result we can find a pattern that is better for everyone.

To be fair, I think it's fine for someone to think a particular pattern isn't reasonable in their app.

I am 100% on the same page as you.
Sure, people can do what whatever they want in their apps. What I'm trying to get at is that all the problems and difficulties described in this thread are result of bad programming habits, and in fact have very little to do with Dart or Flutter. That's like, just my opinion, but I'd say if anybody is writing an app that creates dozens of streams all over the place should maybe review their app design before asking the framework to be "improved".

For example, the hook implementation that made it in to the example repo.

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

I have a bad feeling about this, so I did check some of the internals, and added some debug prints to check what's going on.
You can see from the output below that the Listenable hook checks weather it has been updated on every animation tick. As in, was the widget which "uses" that listenable updated? Was the duration changed? Was the instance replaced?
The memoized hook,I don't even know what is the deal with that. It's probably meant to cache an object, yet on every single build the widget checks if the object changed? What? Why? Of course, because it's used inside a stateful widget and some other widget up the tree might change the value so we need to poll for changes. This is literal polling behaviour is the exact opposite of "reactive" programming.

What's worse, the "new" and "old" hooks both have the same instance type, both have the same values, and yet the function iterates over the values to check if they changed. On _every single animation tick_.

This is the output I get, ad infinitum.

/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

All this work is done on every single animation tick. If I add another hook like "final color = useAnimation(animationColor);", to animate the color too, now the widget checks _two_ times if it was updated.

I'm sitting here watching a text animate back and forth with no change in the app state or any of the widgets or the widget tree, and still the hooks are constantly checking if the tree/widgets were updated. Having every widget that "uses" these particular hooks do this polling behaviour is bad.

Handling initialization/update/disposal logic of state objects inside the build method is just bad design. No amount of improvements gained in reusability, hotreload or cognitive load, justifies the impact on performance.
Again, in my opinion. Hooks being a package, anybody can use them if they think the gains justify the overhead.

Also I don't think any amount of language features, compiler magic or abstraction can prevent such unnecessary checks, if we start trying to abstract away everything inside the build process. So we are left with alternatives like extending the StatefulWidget. Something that can already be done, and was dismissed countless times.

@Hixie you haven't answered the question. What is it that you do not understand/agree with in the bullet list listed above?

I can't make an example without knowing what you want me to demonstrate.

@Rudiksz For sure any solution we would actually consider would need to be profiled and benchmarked to make sure it's not making things worse. Part of the way I constructed the demo app I submitted to @TimWhiting is intended to cover exactly the kinds of patterns that can be easy to mess up. (And, as noted earlier, it does leave room for improvement, see the TODO in the code.)

@rrousselGit I didn't really want to get into the weeds on this because it's so subjective but since you ask:

  • First, I would avoid using Streams in general. ValueListenable is IMHO a much better pattern for more or less the same use cases.
  • I don't think StreamBuilder is particularly hard to read or write; but, as @satvikpendem commented earlier, my history is with deeply nested trees in HTML, and I've been staring at Flutter trees for 4 years now (I've said before that Flutter's core competency is how to efficiently walk giant trees), so I probably have a higher tolerance than most people, and my opinion here isn't really relevant.
  • As to whether mixins and functions might be a possible alternative to StreamBuilder, I think Hooks demonstrates pretty well that you can definitely use functions to listen to streams, and mixins can clearly do anything classes can do here so I don't see why they wouldn't be a solution either.
  • Finally, regarding copy-pasting implementations, that's a subjective matter. I don't personally copy and paste the logic in initState/didUpdateWidget/dispose/build, I write it anew each time, and it seems mostly fine. When it gets "out of control" I factor it out into a widget like StreamBuilder. So again my opinion probably isn't relevant here.

As a general rule, whether I experience the problem you're seeing or not isn't relevant. Your experience is valid regardless of my opinion. I'm happy to work on finding solutions to the problems you experience, even if I don't feel those problems. The only effect of my not experiencing the problem myself is that it is harder for me to understand the problem well, as we have seen in this discussion.

What I would like you to demonstrate is the coding pattern that you think is unacceptable, in a demo that is significant enough that when someone creates a solution, you would not dismiss it by saying that it may get difficult to use when in a different situation (i.e. include all the relevant situations, for example, make sure to include "update" parts or whatever other parts you think are important to handle), or saying that the solution works in one case but not in the general case (e.g. to take your earlier feedback, making sure that parameters are coming from multiple places and updating in multiple situations so that it's obvious that a solution like Property above wouldn't factor out otherwise common code).

The issue is, you are asking examples for something that is in the domain of "obvious" to me.

I do not mind making some examples, but I have no idea what you are expecting, since I don't understand what you don't understand.

I have already said everything I had to say.
The only thing I can do without understanding what you don't understand is to repeat myself.

I can make some of the snippets here run, but that's equivalent to repeating myself.
If the snippet wasn't useful, I don't see why being able to run it would change anything.

As to whether mixins and functions might be a possible alternative to StreamBuilder, I think Hooks demonstrates pretty well that you can definitely use functions to listen to streams, and mixins can clearly do anything classes can do here so I don't see why they wouldn't be a solution either.

Hooks should not be considered as functions.
They are a new language construct akin to Iterable/Stream

Functions cannot do what hooks do — the do not have a State or the capability to cause widgets to rebuild.

The problem with mixins is demonstrated in the OP. TL;DR: Name clash on variables and it is impossible to reuse the same mixin multiple times.

@rrousselGit Well, since you don't mind making some examples, and since the examples I'm asking for are obvious, let's start with some of these obvious examples and iterate from there.

I didn't say the examples are obvious, but the problem is.
What I meant is, I cannot create new examples. Everything I have to say is already in this thread:

I can't think of anything to add to these examples.

But FWIW I'm working on an open-source weather app using Riverpod. I'll link it here when it's done.


I've made a poll on twitter asking a few questions about Builders related to the problems discussed here:

https://twitter.com/remi_rousselet/status/1295453683640078336

The poll is still pending, but here are the current numbers:

Screenshot 2020-08-18 at 07 01 44

The fact that 86% of 200 people wish for a way to write Builders that do not involve nesting speaks for itself.

To be clear, I've never suggested that we should not address this issue. If I thought we should not address it, the issue would be closed.

I guess I'll try to make an example that uses the snippets you linked to.

I can help you make examples based on the snippets linked, but I need to know why these snippets were not good enough
Otherwise, the only thing I can do is make these snippets compile, but I doubt that's what you want.

For example, here's a gist about the numerous ValueListenableBuilder+TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

For example, here's a gist about the numerous ValueListenableBuilder+TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

FWIW, this one particular example can be more easily implemented in mobx.
It's actually shorter than your hooks implementation.

Mobx's observables are ValueNotifiers on steroids and its Observer widget is the evolution of Flutter's ValueListenableBuilder - it can listen to more than one ValueNotifier.
Being a drop-in replacement for ValueNotifier/ValueListenableBuilder combo, means that you still write idiomatic Flutter code, which is actually an important factor.

Since it still uses Flutter's built in Tween builder here's no need to learn/implement new widgets/hooks (in other words it needs no new features) and it has none of the performance hits of hooks.

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'counters.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home1(),
    );
  }
}

var counters = Counters();

class Home1 extends StatelessWidget {
  @override
  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 being as simple as...

part 'counters.g.dart';

class Counters = _Counters with _$Counters;
abstract class _Counters with Store {
  @observable
  int firstCounter = 0;

  @observable
  int secondCounter = 0;
}

Here's another implementation that doesn't even need animationbuilders. The widget's build method is as pure as it can be, almost like a semantic html file ... like a template.

https://gist.github.com/Rudiksz/cede1a5fe88e992b158ee3bf15858bd9

@Rudiksz The behavior of the "total" field is broken in your snippet. It doesn't match the example, where both counters may animate together but finish animating at different times and with different curves.
Similarly, I am not sure what this example adds to the ValueListenableBuilder variant.

As for your last gist, TickerProvider is broken as it does not support TickerMode – nor are listeners removed or controllers disposed of.

And Mobx is likely off-topic. We aren't discussing how to implement ambient state / ValueListenable vs Stores vs Streams, but rather how to deal with local state / nested Builders – which Mobx does not solve in any way

––––

Also, bear in mind that in the hooks example, useAnimatedInt could/should be extracted into a package and that there is no duplicate of the duration/curve between the individual text animation and the total.

As for performances, with Hooks we are rebuilding only a single Element, whereas with Builders with are rebuilding 2-4 Builders.
So Hooks could very well be faster.

The behavior of the "total" field is broken in your snippet. It doesn't match the example, where both counters may animate together but finish animating at different times and with different curves.

You clearly didn't even try to run the exmple. It behaves exactly like your code.

As for your last gist, TickerProvider is broken as it does not support TickerMode.

I don't know what you mean by this. I refactored _your_ example, which does not use TickerMode. You again are changing the requirements.

As for performances, with Hooks we are rebuilding only a single Element, whereas with Builders with are rebuilding 2-4 Builders. So Hooks could very well be faster.

No just no. Your hook widgets constantly poll for changes on every single build. Builders based on valuelistenables are "reactive".

Similarly, I am not sure what this example adds to the ValueListenableBuilder variant.

And Mobx is likely off-topic. We aren't discussing how to implement ambient state / ValueListenable vs Stores vs Streams, but rather how to deal with local state / nested Builders – which Mobx does not solve in any way

You must be kidding. I took your example and "dealt" with nested ValueListenableBuilders *and tween builders?! A point you specifically brought up as an issue.
But this sentence here, describes your whole attitude towards this discussion. If it's not hooks it's "off-topic", but you say that you don't care if it's hooks that will be used as a solution.
Give me a break.

You clearly didn't even try to run the exmple. It behaves exactly like your code.

It doesn't. The first counter animates over 5 seconds, and the second over 2 seconds – and both uses a different Curve too.

With both snippets I gave, you could increment both counters at the same time, and during every single frame of the animation, the "total" would be correct. Even when the second counter stops animating while the first counter is still animating

On the other hand, your implementation does not consider this case, because it merged the 2 TweenAnimationBuilders into one.
To fix it, we would have to write:

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

The two TweenAnimationBuilders are necessary to respect the fact that both counters can animate individually. And the two Observer are necessary because the first Observer cannot observe counters.secondCounter


No just no. Your hook widgets constantly poll for changes on every single build. Builders based on valuelistenables are "reactive".

You are ignoring what Element does, which happens to be the same thing than what hooks do: comparing runtimeType and keys, and deciding whether to create a new Element or update the existing one

I took your example and "dealt" with nested ValueListenableBuilders *and tween builders

Assuming that the issue with the total count is fixed, what nesting was removed?

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

is no different from:

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

in terms of nesting.

If you are referring to your gist, then as I mentioned before, this approach breaks TickerProvider/TickerMode. The vsync needs to be obtained using SingleTickerProviderClientStateMixin or it otherwise does not support the muting logic, which can cause performance issues.
I've explained this in an article of mine: https://dash-overflow.net/articles/why_vsync/

And with this approach, we have to reimplement the Tween logic in every location that would originally want a TweenAnimationBuilder. That leads to a significant duplicate, especially considering the logic is not so trivial

With both snippets I gave, you could increment both counters at the same time, and during every single frame of the animation, the "total" would be correct. Even when the second counter stops animating while the first counter is still animating

On the other hand, your implementation does not consider this case, because it merged the 2 TweenAnimationBuilders into one.

Yes, that's a trade-off I was willing to make. I could easily imagine a case where the animation would be just a visual feedback for change happening and the accurancy is not important. It all depends on the requirements.

I suspected you would object though, hence the second version which solves this exact problem, while making the code even cleaner.

If you are referring to your gist, then as I mentioned before, this approach breaks TickerProvider/TickerMode. The vsync needs to be obtained using SingleTickerProviderClientStateMixin or it otherwise does not support the muting logic, which can cause performance issues.

So you create the tickerprovider in the widget and pass it to the Counters. I also didn't dispose of the animation controllers either. These details are so trivial to implement I didn't feel like they would add anything to the example. But here we are nitpicking on them.

I implemented the Counter() class to do what your example does and nothing more.

And with this approach, we have to reimplement the Tween logic in every location that would originally want a TweenAnimationBuilder. That leads to a significant duplicate, especially considering the logic is not so trivial

What? why? Pleasae explain, why I couldn't create more than one instance of the Counter class and use them in various widgets?

@Rudiksz I am not sure your solutions are actually solving the problems set forth. You say

I implemented the Counter() class to do what your example does and nothing more.

and yet

Yes, that's a trade-off I was willing to make. I could easily imagine a case where the animation would be just a visual feedback for change happening and the accurancy is not important. It all depends on the requirements.

So you create the tickerprovider in the widget and pass it to the Counters. I also didn't dispose of the animation controllers either. These details are so trivial to implement I didn't feel like they would add anything to the example. But here we are nitpicking on them.

You provided code that is only ostensibly equivalent to @rrousselGit's hook version, yet it is not actually equivalent, as you leave out parts that the hook version includes. In that case, they are not really comparable, right? If you would like to compare your solution to the suggested one, it would be best to make it match the requirements exactly instead of talking about why you did not include them. This is something that is the reason for making @TimWhiting's repository. You can submit your solution there if you think you have covered all of the requirements.

You clearly didn't even try to run the exmple. It behaves exactly like your code.

No just no.

You must be kidding. I took your example and "dealt" with nested ValueListenableBuilders *and tween builders

Please refrain from accusations like these, they only serve to make this thread vitriolic without solving the underlying problems. You can make your points without being accusatory, derogatory to, or angry at the other party, which is the effect of the comments I see in this thread, I don't see such behavior from others towards you. I'm also not sure what the effect is to emoji-react to your own post.

@rrousselGit

I can help you make examples based on the snippets linked, but I need to know why these snippets were not good enough
Otherwise, the only thing I can do is make these snippets compile, but I doubt that's what you want.

The reason I'm asking for a more elaborate app than just a snippet is that when I posted an example that handled one of your snippets, you said that that wasn't good enough because it didn't also handle some other case that wasn't in your snippet (e.g. didn't handle didUpdateWidget, or didn't handle having two of these snippets side by side, or other perfectly reasonable things that you would like handled). I'm hoping with a more elaborate app we can get it elaborate enough that once we have a solution, there's no "gotcha" moment where some new problem is brought out that needs handling as well. Obviously it's still possible that we'll all miss something that needs handling, but the idea is to minimize the chance.

Regarding the recent less-than-entirely-welcoming posts, please folks, let's just focus on creating examples on @TimWhiting's repo rather than fighting back and forth with small examples. As we've already discussed, small examples will never be elaborate enough to be sufficiently representative to prove an alternative actually works well.

You can submit your solution there if you think you have covered all of the requirements.

The requirements were changed after the fact. I provided two solutions. One that makes a compromise (a very reasonable one) and one that implements the exact behavour the example provided.

You provided code that is only ostensibly equivalent to @rrousselGit's hook version, yet it is not actually equivalent,

I didn't implement the "hooks solution", I implemented the ValueListenableBuilder example, specifically focusing on the "nesting issue". It doesn't do every single thing hooks do, I simply showcased how one item in the bullet list of grievances can be simplified using an alternative solution.

If you are allowed to bring in external packages into the discussion, than so am I.

Reusability: take a look at the example in the repo below
https://github.com/Rudiksz/cbl_example

Note:

  • it's meant as a showcase of how you can encapsulate "logic" outside widgets and have widgets that are lean, almost html looking
  • it does not cover everything hooks cover. That's not the point. I thought we are discussing alternatives to the stock Flutter framework, and not the hooks package.
  • It does not cover _every_ use case every marketing team can come up
  • but, the Counter object itself is pretty flexible, it can be used standalone (see the AppBar title), as part of a complex widget that needs to calculate totals of different counters, be made reactive, or respond to user inputs.
  • It's up to the consuming widgets to customize the amount, initial value, duration, animation type of the counters they want to use.
  • the details of the way animations are handled may have downsides. If the Flutter team says that yes, using Animation controllers and tweens outside the widgets, somehow breaks the framework, I will reconsider. There are definitely things to improve. Replacing the custom tickerprovider I use with one created by the consuming widget's mixin is trivial. I haven't done it.
  • This is just one more alternative solution to the "Builder" pattern. If you say you need or want to use Builders, then none of this applies.
  • Still, fact is that there are ways to simplify code, without extra features. If you don't like it, don't buy it. I'm not advocating any of this to make it into the framework.

Edit: This example has a bug, if you initiate an "increment" while the change is animated, the counter is reset and is incremented from the current value. I didn't fix it on purpose, because I don't know the exact requirements you might have for these "counters". Again it's trivial to change the increment/decrement methods, to fix this issue.

That is all.

@Hixie Should I interpret your comment as a way of saying that my example (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/hooks/animated_counter.dart) is not good enough?

Also, could we have a Zoom/Google meet call?

I'm also not sure what the effect is to emoji-react to your own post.

Nothing. It's totally irrelevant to anything. Why did you bring it up?

@rrousselGit Only you can know if it's good enough. If we find a way to refactor that example so that it's clean and short and has no duplicate code, will you be satisfied? Or are there things you think we should support that aren't handled by that example that we need to handle to satisfy this bug?

Only you can know if it's good enough

I cannot be the judge of that. To begin with, I do not believe that we can capture the problem using a finite set of applications.

I don't mind working like you want me to, but I need guidance as I do not understand how that will make us progress.

I think we've provided a ton of code snippets that show the problem from our perspective. I really dont think will become more clear through more code examples, if the ones shown are not doing it.

For example, if seeing multiple nested builders that are horrible to read, or 50 lines of pure boilerplate that has many opportunities for bugs, does not demonstrate the issue strongly enough, there is nowhere to go.

It's very strange to proffer mixins and functions are a solution here, when the entire ask is encapsulated state, that is re-usable. Functions can not maintain state. Mixins are not encapsulated. This suggestion misses the entire point of all of the examples and rationale provided and still shows a deep misunderstanding of the ask.

To me, I think we've beaten 2 points into the earth, and I don't think can seriously argue with either.

  1. Nested builders are inherently hard to read
  2. Other than nested builders there is _no way_ to encapsulate and share state that has widget lifecycle hooks.

As stated many times, and even in Remi's poll, simply put: We want to capabilities of builder, without the verbosity and nested closures of builder. Does that not completely sum it up? I'm totally confused why is a further code example required to proceed here.

Remi's poll shows us that a ~80% of Flutter dev's would prefer some sort of ability to avoid nested builders in their code when possible. This really does speak for itself imo. You don't need to take it from us in this thread, when community sentiment is so clear.

From my perspective, the issues are clear, and they are made even more clear when you look at competing frameworks that dedicate paragraphs to describing the rationale here. Vue, React, Flutter they are all cousins, they all are derived from React, and they all face this issue with re-using state that has to tie into widget lifecycle. They all describe why they have implemented something like this in detail. It's all right there. It's all relevant.

@rrousselGit could you make an example of having many multiple hooks? For example, I am making an animation with possibly dozens of AnimationControllers. With regular Flutter, I can do:

List<AnimationController> controllers = [];
int numAnimationControllers = 50;

@override
void initState() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers.add(AnimationController(...));
}

@override
void dispose() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers[i].dispose();
}

But with hooks I cannot call useAnimationController in a loop. I suppose this is a trivial example but I couldn't really find the solution anywhere for this type of use case.

@satvikpendem

few examples from my apps that are in production (some of the hooks like sending request with pagination can merge/refactor into a single hook but that's irrelevant here):

simple data fetching with pagination:

    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 () {};
    }, []);

form logic (login form with phone number verification and resend timer):

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

for animation, I think @rrousselGit already provided enough example.

I don't want to talk about how composable nature of hooks can make refactoring above code much easier, reusable and cleaner but if you want I can post refactored versions too.

As stated many times, and even in Remi's poll, simply put: We want to capabilities of builder, without the verbosity and nested closures of builder. Does that not completely sum it up? I'm totally confused why is a further code example required to proceed here.

I literally provided examples of how you can reduce verbosity and avoid nestedness of builders using an example Remi provided.
I took his code stuck it into my app, ran it and rewrote it. The end result as far as functinality is concerned was nearly identical - as much as I could glean from just running the code, because the code didn't come with requirements. Sure we can discuss the edge cases and potential problems, but instead it was called off-topic.

For simple use cases I use Builders, for complex use cases I don't use Builders. The argument here is that without using Builders there's no easy way to write concise, reusable code. Implicitly, that's also means that Builders are a must have, and the only way to develop Flutter apps. That is demonstrably false.

I just showed a working, proof of concept code that demonstrates it. It didn't use Builders or hooks, and it does not cover 100% of the "infinite set of problems" that this particular github issue seems to want to solve. It was called off-topic.
Sidenote, it's also very efficient, even without any benchmark I would guess even beats the Builder widgets. I'm happy to change my mind if proven wrong, and if I ever find Mobx to become a performance bottleneck I will ditch Mobx and switch to vanilla builders in a heartbeat.

Hixie works for Google, he has to be patient and polite with you and can't call you out on your lack of engagment. The best he can do is to push for more examples.

I didn't call anybody names, or bring in personal attacks. I only ever reacted to the arguments presented here, shared my opinion
(which I know is unpopular and in the minority) and even tried to present actual counter examples with code. I could do more, I'm willing to discuss where my examples fall short and see ways we can improve them, but yeah, being called off-topic is kind of off-putting.

I have nothing to lose, other than maybe being banned, so I don't really care calling you out.
It is evident that you two are dead set that hooks are the only solution ("because React does it") to whatver the problems you experience and that unless an alternative fullfills 100% of the "infinite set of problems" that you are imagining, you won't even consider engaging.

That is not reasonable, and shows lack of desire to truly engage.


Of course, everything above "is just my opinion".

I see the usefulness of hooks in that example but I guess I don't understand how it'd work in my case where it seems like you would want to initialize many objects at once, in this case AnimationControllers but in reality it could be anything. How do hooks handle this case?

Basically is there a hooks way of turning this

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

Into

var xs = []
for (int i = 0; i < 3; i++)
     xs[i] = useState(i);

Without violating hook rules? Because I listed the equivalent in normal Flutter. I'm not too experienced with hooks in Flutter so bear with me there.

I want to simply create an array of hook objects (AnimationControllers for example) on demand with all of its initState and dispose already instantiated, I'm just unsure how it works in hooks.

@satvikpendem think about hooks like properties on a class. do you define them in a loop or manually naming them one by one?

in your example defining like this

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

is useful for this usecase:

var isLoading = useState(1);
var selectedTab = useState(2);
var username = useState(3); // text field

do you see how each useState is related to a named part of your state logic? (like isLoading's useState is connected to when app is in loading state)

in your second snippet you are calling useState in a loop. your are thinking of useState as a value holder not part of your state logic. is this list needed for showing bunch of items in a ListView? if yes then you should think of every item in the list as a state not individually.

final listData = useState([]);

this is just for the useState and I can see some use cases (which I think they are very rare) for calling some hooks in a condition or in a loop. for those kind of hooks there should be another hook for handling list of data instead of one. for example:

var single = useTest("data");
var list = useTests(["data1", "data2"]);
// which is equivalent to
var single1 = useTest("data1");
var single2 = useTest("data2");

I see, so with hooks it looks like we need to create a separate hook to handle cases with an array of items, such as multiple AnimationControllers.

This is what I had initially which doesn't seem to work:

  final animationControllers = useState<List<AnimationController>>([]);

  animationControllers.value = List<AnimationController>.generate(
    50,
    (_) => useAnimationController(),
  );

but I suppose if I write my own hook to handle multiple items, this should work, right?

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;

  @override
  _AnimationControllerHookState createState() =>
      _AnimationControllerHookState();
}

class _AnimationControllerHookState
    extends HookState<AnimationController, _AnimationControllerHook> {
  List<AnimationController> _multipleAnimationController; // return a list instead of a singular item

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

  @override
  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;
        }
      }
  }

  @override
  MultipleAnimationController build(BuildContext context) {
    return _multipleAnimationController;
  }

  @override
  void dispose() {
    _multipleAnimationController.map((e) => e.dispose());
  }
}

Does this mean that if we have a singular version of a hook, we simply can't use it for a version with multiple items and instead have to rewrite the logic? Or is there a better way of doing this?

If anyone also wants to give a non-hooks example I would like to know as well, I've been wondering about this piece of the reusability puzzle. Maybe there's a way to encapsulate this behavior in a class, which has its own AnimationController field, but if that's created inside a loop, then the hook would be too, breaking the rules. Perhaps we could consider how Vue does it, which is unaffected by conditionals and loops for its hooks implementation.

@satvikpendem

I don't think my statement is valid for AnimationController or useAnimationController

because although you may have more than one AnimationController but you don't necessarily store them in an array to use them in the class method. for example:

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

(you don't create a list and reference them like animation[0])

honestly in my experience in react and flutter with hooks I rarely needed calling some kind of hooks in a loop. even then the solution was straight forward and easy to implement. now I think about it, it definitely could be solved in a better way like creating a Component(widget) for each one of them which is IMO is the "cleaner" solution.

to answer your question if there is an easier way to handle multiple AnimationController, yes there is:

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

  • you can also use useState if AnimationControllers are dynamic.

(it also re syncs when ticker is changed)

@rrousselGit could you make an example of having many multiple hooks? For example, I am making an animation with possibly dozens of AnimationControllers. With regular Flutter, I can do:

List<AnimationController> controllers = [];
int numAnimationControllers = 50;

@override
void initState() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers.add(AnimationController(...));
}

@override
void dispose() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers[i].dispose();
}

But with hooks I cannot call useAnimationController in a loop. I suppose this is a trivial example but I couldn't really find the solution anywhere for this type of use case.

Hooks do that differently.

We don't create a List of controllers anymore, rather we move the controller logic down to the item:

Widget build(context) {
  return ListView(
    children: [
      for (var i = 0; i < 50; i++)
        HookBuilder(
          builder: (context) {
            final controller = useAnimationController();
          },
        ),
    ],
  );
}

We still made our 50 animation controllers, but they are owned by a different widget.

Maybe you could share an example of why you needed that, and we could try and convert to hooks and add it to Tim's repo?

Hixie works for Google, he has to be patient and polite with you and can't call you out on your lack of engagment. The best he can do is to push for more examples.

@Hixie, if that is how you feel, please say so (either here or contact me privately).

I literally provided examples of how you can reduce verbosity and avoid nestedness of builders using an example Remi provided.

Thanks, but it is unclear to me how you would extract a common pattern from this code as apply this logic to different use-cases.

In the OP, I mentioned that currently, we have 3 choices:

  • use Builders and have nested code
  • don't factorize the code whatsoever, which does not scale to more complex state logic (I would argue that StreamBuilder and its AsyncSnapshot is a complex state logic).
  • try and make up some architecture using mixins/oop/..., but end up with a solution too specific to the problem that it any use-case that is a _tiny_ bit different will require a rewrite.

It appears to me that you used the 3rd choice (which is in the same category as early iterations of the Property or addDispose proposals).

I previously made an evaluation grid to judge the pattern:

Could you run your variant on this? Especially the second comment about implementing all the features of StreamBuilder without code duplicate if used multiple times.

My plan at this point on this bug is:

  1. Take the examples from https://github.com/flutter/flutter/issues/51752#issuecomment-675285066 and create an app using pure Flutter that shows those various use cases together.
  2. Try to design a solution that enables code reuse for those examples that satisfies the various key requirements that have been discussed here and that fits within our design principles.

If anyone would like to help with either of these I'm definitely happy to have help. I'm unlikely to get to this soon because I'm working on the NNBD transition first.

@rrousselGit Sure, I'm making an app where many Widgets may move around the screen (let's call them Boxes), and they should be able to move independently of each other (so there needs to be at least one AnimationController for each Box). Here's one version I made with just one AnimationController shared among the multiple Widgets, but in the future I may animate each Widget independently, for example to do complicated Transforms such as to implement a CupertinoPicker, with its custom scroll wheel effect.

There are three boxes in a Stack that move up and down when you click a FloatingActionButton.

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

void main(List<String> args) => runApp(const App());

class App extends HookWidget {
  const App({Key key});

  static const Duration duration = Duration(milliseconds: 500);
  static const Curve curve = Curves.easeOutBack;

  @override
  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({
    @required this.numBoxes,
    @required this.animation,
  });

  final int numBoxes;
  final Animation<double> animation;

  @override
  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,
          ),
        ),
      ),
      // ],
    );
  }
}

In this case, each box moves in unison, but one can imagine a more complex scenario such as creating a visualization for a sorting function for example, or moving elements in an animated list around, where the parent widget knows the data about where each Box should be and it should be able to animate each one as it sees fit.

The problem appears to be that the AnimationControllers and the Boxes that use them to drive their movement are not in the same class, so one would either need to pass through the AnimationController by keeping an array of them to use in a Builder, or have each Box maintain its own AnimationController.

With hooks, given that the Boxes and the parent widget are not in the same class, how would I make a list of AnimationControllers for the first case where each Box is passed in an AnimationController? This seems not needed based on your answer above with HookBuilder, but then if I move down the state into the child Widget as you say, and choose to make each Box have its own AnimationController via useAnimationController, I run into another problem: how would I expose the created AnimationController to the parent class for it to coordinate and run the independent animations for each child?

In Vue you can emit an event back to the parent via the emit pattern, so in Flutter do I need some higher state management solution like Riverpod or Rx where the parent updates the global state and the child listens to the global state? It seems that I shouldn't, at least for a simple example like this. Thanks for clearing up my confusions.

@satvikpendem Sorry I wasn't clear. Could you show how you would do it without hooks, rather than the issue where you are blocking with hooks?

I want to have a clear understanding of what you are trying to do rather than where you are getting stuck

But as a quick guess, I think you are looking for the Interval curve instead, and have a single animation controller.

@rrousselGit Sure, here it is

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;

  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> with TickerProviderStateMixin {
  List<Animator> animators = [];
  bool isDown = false;
  int numBoxes = 3;

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

  @override
  void dispose() {
    for (int i = 0; i < numBoxes; i++) {
      animators[i].controller.dispose();
    }
    super.dispose();
  }

  @override
  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({
    @required this.animation,
    @required this.index,
  });

  final int index;
  final Animation<double> animation;

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

I actually do want multiple animation controllers, one for each widget, as they can move independently of each other, with their own durations, curves, etc. Note that the above code seems to have a bug that I couldn't figure out, where it should animate cleanly, but basically it should animate 3 boxes up and down on a button click. We can imagine a scenario where instead of them each having the same curve, I give each of them a different curve, or I make 100 boxes, each with a duration longer or shorter than the previous one, or I make the even ones go up and the odd ones go down, and so on.

With normal Flutter, initState and dispose can both have loops but not so it seems with hooks, so I'm just wondering how one can combat that. As well, I don't want to put the Box class inside the parent widget, as I don't want to tightly encapsulate them both; I should be able to keep the parent logic the same but swap out Box with Box2 for example.

Thanks!
I've pushed your example to @TimWhiting's repo, with a hook equivalent

TL;DR, with hooks (or builders), we think declaratively instead of imperatively. So rather than having a list of controllers on one widget, then driving them imperatively – which move the controller to the item and implement an implicit animation.

Thanks @rrousselGit! I was struggling with this type of implementation for a little while after starting to use hooks but I understand now how it works. I just opened a PR for a version with a different target for each animation controller as that might be more compelling to understand why hooks are useful as I had said above:

We can imagine a scenario where instead of them each having the same curve, I give each of them a different curve, or I make 100 boxes, each with a duration longer or shorter than the previous one, or I make the even ones go up and the odd ones go down, and so on.

I had been trying to make the declarative version but I suppose what I didn't understand was the didUpdateWidget/Hook lifecycle method, so I didn't know how to drive the animation when a child prop is changed from the parent, but your code cleared it up.

Came across a real-world example in my code base today, so thought I might as well share it.

So in this scenario, I'm working with Firestore, and have some boilerplate I want to perform with each StreamBuilder, so I made my own custom builder. I also need to work with a ValueListenable which allows the user to re-order the list. For monetary-cost reasons related to Firestore, this requires a very specific implementation (each item can't store it's own order, instead the list must save it as a field of concatenated id's), this is because firestore charges for each write, so you can potentially save a lot of money this way. It ends up reading something like this:

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

It feels like it would be much easier to reason about, if I could write it more like:

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

This does lose the optimization regarding granular rebuilds, but that wouldn't make any different IRL since all the visual elements are at the bottom-most leaf node, all the wrappers are pure state.

As with many real world scenario, the advise of "Just don't use X" is not realistic, as Firebase has just one method of connection which is Streams, and any time I want this socket-like behavior I have no choice but to use a Stream. C'est la vie.

This does lose the optimization regarding granular rebuilds, but that wouldn't make any different IRL since all the visual elements are at the bottom-most leaf node, all the wrappers are pure state.

It still makes a difference. Whether a node is visual or not does not affect whether it costs something to rebuild.

I would probably factor that example out into different widgets (the IDEs have one-click refactoring tools to make that really easy). _buildItemList should probably be a widget, as should the part rooted at FamilyStreamBuilder.

We don't really lose the granular rebuild.
In fact hooks improve that aspect, by allowing to easily cache the widget instance using useMemoized.

There are a few examples on Tim's repo that do that.

I would probably factor that example out into different widgets (the IDEs have one-click refactoring tools to make that really easy). _buildItemList should probably be a widget, as should the part rooted at FamilyStreamBuilder.

Thing is, I don't really want to do this, because I have no performance concerns at all in this view. So 100% of the time I will favor code locality and coherence over micro-optimization like this. This view only rebuilds when the user initiates action (~avg once per 10 seconds), or when the backend data changes and they are staring at an open list (almost never happens). It also happens to just be a simple view that is primarily a list, and the list has a ton of it's own optimizations going on internally. I realize build() can technically fire any time, but in practice any random rebuilds are quite rare.

imo it's significantly easier to work on and debug this view if all this logic is grouped in one widget, mainly as an effort to make my life easier when I come back to it in the future :)

Another thing to note is that the nesting basically "forced me out" of the build method, as there was no way I could begin to construct my tree inside of 3 closures and 16 spaces in the hole.

And yes, you could say it makes sense to then just move to a separate widget. But why not just stay in the build method? If we could reduce the boilerplate to what it really _needs_ to be, then there's no need to have the readability and maintenance hassle of of splitting things across 2 files. (Assuming performance is not a concern, which it often just isn't)

Remember in this scenario I already created a custom widget to handle my Stream Builder. Now I would need to make another one to handle the composition of these builders?? Seems a bit over the top.

because I have no performance concerns at all in this view

Oh I wouldn't refactor it into widgets for performance, the builders should take care of that already. I would refactor it for readability and reusability. I'm not saying that's the "right" way to do it, just saying how I would structure the code. Anyway, that's neither here nor there.

no way I could begin to construct my tree inside of 3 closures and 16 spaces in the hole

I may just have a wider monitor than you... :-/

then there's no need to have the readability and maintenance hassle of of splitting things across 2 files

I would put the widgets in the same file, FWIW.

Anyway, it's a fine example, and I believe you that you would rather use one widget with a different syntax than using more widgets.

I may just have a wider monitor than you... :-/

I have an ultrawide :D, but dartfmt obviously limits us all to 80. So losing 16 is significant. The main issue is the end of my statement is this },);});},),),); not really fun when something gets messed up. I have to be extremely careful anytime I edit this heirarchy, and common IDE helpers like swap with parent stop working.

I would put the widgets in the same file, FWIW.

100%, but I still find that jumping around vertically in a single file is tougher to maintain. Unavoidable of course, but we try to reduce when possible and 'keep things together'.

Crucially though, even if I do refactor the main list into it's own widget (which I agree, is more readable than a nested build method), it's still way more readable without the nesting in the parent widget. I can come in, understand all the logic at a quick glance, see the _MyListView() widget clearly, and jump right into it, confident I understand the surrounding context. I can also add/remove additional dependencies with relative ease, so it scales very well.

dartfmt obviously limits us all to 80

I mean, that's one reason I don't generally use dartfmt, and when I do I set it to 120 or 180 characters...

Your experience here is totally valid.

Me too actually, 120 all day :) But pub.dev actively down-rates plugins that are not formatted at 80, and I get the impression that I (we) are in the minority when we change this value.

Well that's absurd, we should fix that.

pub.dev doesn't not down-rate plugins that do not respect dartfmt. It only shows a comment in the score page, but the score is unimpacted
But arguably, there are more problems with dartfmt than just the line-length.

A too big line-length leads to things that are more readable in multiple lines to be in a single line, such as:

object
  ..method()
  ..method2();

which may become:

object..method()..method2();

I'm seeing this?
image
Package in question: https://pub.dev/packages/sized_context/score

Interesting – it definitely wasn't like that before, as provider didn't use dartfmt for a while.
I stand corrected.

Yup it definitely is new behavior, when I originally published last spring I made sure I was ticking all the boxes, and dartfmt was not required.

After all these discussions, I hope we see native support for hook like solution in flutter. either useHook or use Hook or anything that flutter team can feel their feature is not like React 😁🤷‍♂️

we use hooks in a way like final controller = useAnimationController(duration: Duration(milliseconds: 800));
Is it not better to use Darts new program feature _Extension_ copied from kotlin/swift to beautifully that syntax?

something like: final controller = AnimationController.use(duration: Duration(milliseconds: 800));
with this approach, when flutter/dart team decides to add use Hook instead of currently available syntax useHook, I think a Annotation to that extension function made it read to use as
final controller = use AnimationController(duration: Duration(milliseconds: 800));

also it's understandable/meaningful to have use keyword used like const and new:
new Something
const Something
use Something

as a bonus to that recommendation, I thing at last even constructor/generator functions can use/benefit from that proposed Annotation. then dart compiler with some customization converts it to support use keyword.

So beautiful and flutter/dart specific feature 😉

Am I correct in assuming that the examples in https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful are now representative of the issues people want resolved?

I'm not sure how everyone else feels, but I think that the problems are somewhat represented there (meaning I can't be sure because someone might point out something that isn't represented).

I have attempted a middle-ground solution in that repository. It is composable like hooks, but not dependent on ordering of function calls or not allowing for loops etc. It uses StatefulWidgets directly. It involves a mixin, as well as stateful properties that are uniquely identified by keys. I'm not trying to promote this as the ultimate solution, but as a middle ground between the two approaches.

I've called it the lifecycleMixin approach, it is very close to the LateProperty approach that was discussed here, but the main difference is it has more lifecycles implemented, and it can easily compose. (on the lifecycles part, I haven't used widget lifecycles other than initState and dispose much, so I might have totally messed up there).

I like this approach because:

  1. It has very little runtime penalty.
  2. There is no logic / functions creating or managing state in the build path (builds can be pure - only fetching state).
  3. Lifecycle management is more clear when optimizing rebuilds via a builder. (But you don't sacrifice on reusability and composability of small bits of state).
  4. Since you can reuse creation of bits of state, a library can be made of common bits of state that should be created and disposed of in certain manners, so there is less boilerplate in your own code.

I don't like this approach (compared to hooks) for the following reasons:

  1. I don't know if it covers everything hooks can do.
  2. You have to use keys for uniquely identifying the properties. (So when composing the pieces of logic that build up some state you have to append to the key to uniquely identify each portion of state -- making the key a required positional parameter helps, but I'd love a language level solution to accessing a unique id for a variable).
  3. It heavily uses extensions for creating reusable functions to create common bits of state. And extensions cannot be auto-imported by the IDEs.
  4. You can mess yourself up if you mix lifecycles of different widgets / access them between widgets without explicitly managing them correctly.
  5. The builder syntax is a bit weird so that the created state is in the scope of the build function, but leaving the build function pure.
  6. I haven't yet implemented all of the examples, so there might be a use-case that I cannot cover.

Simple counter example.
Animated counters example

framework
common bits of reusable state composing logic

I'm not sure how much time I have, graduate studies are always keeping me busy, but I'd love some feedback. @rrousselGit How close is this to hooks, can you see some obvious holes in reusability or composability?

I'm not trying to promote my solution, so much as encourage positive discussion on a middle ground. If we can agree about what is missing or what this solution gives us, I think we will be making good forward progress.

@TimWhiting The main issue I have with this approach is the lack of robustness. A big driver here is the need for the reliability of builders, in a succinct form. The magic id, and the ability to clash on lifecycle, both create new vectors for bugs to occur, and I would continue to recommend to my team they use builders, as despite being pretty nasty to read, at least we know that they are 100% bug-free.

Regarding examples, I still think the perfect example is simply using a AnimationController, with a duration value tied to the widget. Keeps it simple and familiar. There is no need to get more esoteric than that, it's a perfect little use-case for re-usable boilerplate, it needs lifecycle hooks, and all solutions could easily be judged by their ability to use several animations succinctly.

Everything else is just a variation of this same use 'Stateful Controller' use case. I want to do X in initState, and Y in dispose state, and update Z when my dependencies change. It doesn't matter what X, Y and Z are.

I wonder if @rrousselGit could provide some insight here, or has any data on which hooks are currently most used. I'm guessing it's 80% Stream and Animations, but it would be nice to actually know what people are using most.

Regarding rebuilding portions of the tree, builders are naturally suited to this task anyways, we should just let them do it. A stateful controller can easily be hooked into stateless renderers anyways if that is what you want (hello every Transition class).

Just like we might do:

var anim = get AnimationController();
return Column(
  _someExpensiveBuildMethod(),
  FadeTransition(opacity: anim, child: ...)
)

We could always do:

var foo = get ComplicatedThingController();
return Column(
  _someExpensiveBuildMethod(),
  ComplicatedThing(controller: foo, child: ...)
)

@esDotDev I agree, the keys, and builder syntax are the main drawback of the lifecycleMixin approach. I don't know if you can get around that except by using a hooks style approach with its associated restrictions, or a language change in being able to associate variable declarations with bits of state with lifecycles. This is why I will continue using hooks, and let others use stateful widgets, unless a better solution comes about. However, I do think it is an interesting alternative for those who don't like the restrictions of hooks, though it comes with restrictions of its own.

Am I correct in assuming that the examples in https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful are now representative of the issues people want resolved?

I'm honestly not sure.
I would say _yes_. But that really depends on how you will interpret these examples.

In this thread, we have a history of not understanding each other, so I can't guarantee this won't happen again.

That's partly why I dislike using code examples and suggested to extract a set of rules instead.
Examples are subjective and have multiple solutions, some of which may not solve the broader problem.

I wonder if @rrousselGit could provide some insight here, or has any data on which hooks are currently most used. I'm guessing it's 80% Stream and Animations, but it would be nice to actually know what people are using most.

I think it's very homogenous.

Although if anything, useStream and Animations are likely the least used:

  • useStream usually has a better equivalent depending on your architecture. Could use context.watch, useBloc, useProvider, ...
  • few people take the time to make animations. That's rarely the priority, and TweenAnimationBuilder other implicitly animated widgets cover a big part of the need.
    Maybe that would change if I added my useImplicitlyAnimatedInt hooks in flutter_hooks.

@esDotDev Just removed the need for keys/ids in the lifecycleMixin approach. It's still a bit awkward in the builder syntax. But possibly that could be helped eventually too. The one issue that I'm running into is with the type system. It tries to cast things in certain ways that aren't working. But it probably just needs some careful casting or type system mastery. As far as mixing lifecycles I think that could be improved by throwing some reasonable exceptions when a particular piece of state you try to access is not accessible by that widget's lifecycle. Or a lint that within a lifecyclebuilder you should only access the builder's lifecycle.

Thanks Remi, that surprises me, I would think people would useAnimation very frequently to drive the large collection of Transition widgets in the core, but I guess most people just use the various Implicit's, as they are quite nice to read and do not have nesting.

Still despite AnimatorController being very well served with a suite of Implicit and Explicit widgets, I still think it's a great example of a 'thing that needs to maintain state, and tie into widget params & lifecycle`. And serves as a perfect little example of the problem to be solved (the fact is is totally solved in Flutter w/ like a dozen widgets notwithstanding), that we can all discuss and stay focused on the architecture and not the content.

For example, consider how, if var anim = AnimationController.use(context, duration: widget.duration ?? _duration); were a first class citizen, virtually none of these implicit or explicit animations really need to exist. It renders them redundant since they are all created to manage the core problem: easily compositing a stateful thing (AnimationController) within the context of a widget. TAB becomes pretty close to pointless, since you can do the same thing with AnimatedBuilder + AnimatorController.use().

It really illustrates the need for the general use case if you look at the huge mass of widgets that have sprung up around animations. Precisely because it is so cumbersome/bug-prone to re-use the core setup/teardown logic, we have 15+ widgets all handling very specific things, but the majority of each of them are repeating the same animation boilerplate with only a handful of unique lines of code in many cases.

It serves to show, that yes we could also do this thing to re-use our own stateful logic: make a widget for every single permutation of usage. But what a hassle and maintenance head-ache! So much nicer to just have an easy way to compose little stateful objects, with lifceycle hooks, and if we want to make dedicated widgets for rendering, or a re-usable builder, we can easily just layer those on top.

For what it's worth, I use something like useAnimation heavily in my app rather than the normal animation widgets. This is because I'm using a SpringAnimation which isn't well supported with widgets like AnimatedContainer for example; they all assume a time-based animation, with curve and duration rather than simulation-based animation, which would accept a Simulation argument.

I made an abstraction over useAnimationbut with springs, so I called it useSpringAnimation. The wrapper widget I used this hook with is similar to an AnimatedContainer but it was much easier to make because I could reuse all the animation code as you say @esDotDev, as much of the logic is the same. I could even make my own version of all of the animated widgets by again using useSpringAnimation but I didn't necessarily need to for my project. This once again shows the power of life cycle logic reuse that hooks provide.

For example, consider how, if var anim = AnimationController.use(context, duration: widget.duration ?? _duration); were a first class citizen, virtually none of these implicit or explicit animations really need to exist. It renders them redundant since they are all created to manage the core problem: easily compositing a stateful thing (AnimationController) within the context of a widget. TAB becomes pretty close to pointless, since you can do the same thing with AnimatedBuilder + AnimatorController.use().

Reading my comments above, this seems to be basically exactly what I did with my spring animation hook. I encapsulated the logic and then simply used AnimatedBuilder. To make them implicit, so that when I changed the prop as one does on AnimatedContainer, it would animate, I just added the didUpdateWidget (called didUpdateHook in flutter_hooks) method to run the animation from the old value to the new value.

Am I correct in assuming that the examples in https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful are now representative of the issues people want resolved?

I'm honestly not sure.
I would say _yes_. But that really depends on how you will interpret these examples.

In this thread, we have a history of not understanding each other, so I can't guarantee this won't happen again.

That's partly why I dislike using code examples and suggested to extract a set of rules instead.
Examples are subjective and have multiple solutions, some of which may not solve the broader problem.

I would also say that we should include all of the code samples in this issue that were discussed, I think there's a list above somewhere that @rrousselGit made. I could make a PR adding them to the local_state repository but they aren't all complete code examples so they might not all actually compile and run. But they show the potential problems at least.

I could make a PR adding them to the local_state repository

That would be very useful.

I'd like to point out that this thread has not defined reuse or what reuse looks like. I think we should be painfully specific in defining that, lest the conversation lose focus.

We've only shown what reuse _isn't_ as it pertains to Flutter.

There has been quite a few usage examples, and hooks clearly provides a total example of widget-state re-use. I'm not sure where the confusion stems from as it seems straightforward on it's face.

Re-use can simply be defined as: _Anything a builder-widget can do._

The ask is for some stateful object that can exist inside any widget, that:

  • Encapsulates it's own state
  • Can setup/teardown itself according to initState/dispose calls
  • Can react when dependencies change in the widget

And does so in a nice succinct easy to ready, boilerplate-free way, like:
AnimationController anim = AnimationController.stateful(duration: widget.duration);
If this works in Stateless and Stateful widgets. If it rebuilds when widget.something changes, if it can run it's own init() and dispose(), then you basically have a winner and I'm sure everyone would appreciate it.

The main thing I'm struggling with is how to do this in an efficient way. For example, ValueListenableBuilder takes a child argument that can be used to measurably improve performance. I don't see a way to do that with the Property approach.

I'm pretty sure this is a non-issue. We would do this the same way that the XTransition widgets work now. If I have some complex state, and I wanted it to have some expensive child, I would just make a small wrapper Widget for it. Just like we might make:
FadeTransition(opacity: anim, child: someChild)

We can just as easily do that with any thing that we want rendered, by passing the 'thing' into a Widget to re-render it.
MyThingRenderer(value: thing, child: someChild)

  • This doesn't _require_ nesting like builder does, but it optionally supports it (.child could be a build fxn)
  • It retains the ability to be used directly without a wrapping widget
  • We can always make a builder and use this syntax within the builder to keep it cleaner. It also opens the door to multiple types of builder, built around the same core object, that does not involve copy pasted code all over the place.

Agreed with @esDotDev. As I mentioned previously, an alternate title for this would be "Syntax sugar for Builders".

The main thing I'm struggling with is how to do this in an efficient way. For example, ValueListenableBuilder takes a child argument that can be used to measurably improve performance. I don't see a way to do that with the Property approach.

I'm pretty sure this is a non-issue. We would do this the same way that the XTransition widgets work now. If I have some complex state, and I wanted it to have some expensive child, I would just make a small wrapper Widget for it. Just like we might make:

There is no need for that.
One of the benefits of this feature is, we can have a state-logic that is "cache the widget instance if its parameters didn't change".

With hooks, that would be useMemo in React:

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

With this code, myWidget will rebuild _only_ when value changes. Even if the widget that calls useMemo rebuilds for other reasons.

That's similar to a const constructor for widgets, but allows dynamic parameters.

There's an example doing that in Tim's repo.

The ask is for some stateful object that can exist inside any widget, that:

  • Encapsulates it's own state
  • Can setup/teardown itself according to initState/dispose calls
  • Can react when dependencies change in the widget

I guess I have a hard time seeing why by those parameters, StatefulWidget doesn't do the job better than it does. Which is why I've asked the question on what we're really after here in a solution. As someone that uses flutter_hooks I find them to be more fun to work with than StatefulWidget, but that's just to avoid verbosity-- not because I think in terms of hooks. I actually find reasoning about UI updates difficult with hooks compared to Widgets.

  • Can react when dependencies change in the widget

You mean a dependency that was created/acquired inside the widget? Or a dependency far below the widget in the tree?

I'm not denying that there is a problem that causes verbosity/confusion in Flutter, I'm just hesitant to rely on everyone actually having the same mental model of what "reuse" is. I'm very thankful for the explanation; and when people have different models, they create different solutions.

Because using a SW to do this is fine for a specific use case, but not good for abstracting the reusable logic of the use-case across many SW's. Take the setup/teardown for Animation as example. This is not a SW itself, it's something we want to use across them. Without first-class support for sharing encapsulated state, you end up having to make a builder, ie TweenAnimationBuilder, or make a ton of specific Widgets, ie AnimatedContainer etc. Really much more elegant if you can just bundle that logic up and re-use it any way you want inside a tree.

In terms of Widget dependency, I just mean if widget.foo changes, the stateful-thing gets an opportunity to do any updated it needs to do. In case of stateful AnimationController, it would check if duration changed, and if it did, update it's internal AnimatorController instance. This saves every implementer of the Animation from having to handle the property change.

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

With this code, myWidget will rebuild _only_ when value changes. Even if the widget that calls useMemo rebuilds for other reasons.

Ah I see, Memoized returns a Widget itself, and then you pass in [value] as the rebuild trigger, neat!

The key about AnimatedOpacity is neither the parent nor the child rebuild. In fact, when you trigger an animation using AnimatedOpacity literally nothing rebuilds after the first frame where you trigger the animation. We skip the build phase entirely and do it all in the render object (and in the render tree, it's only repaint, not relayout, and in fact it uses a Layer so even the paint is pretty minimal). It makes a significant difference to the performance and battery usage. Whatever solution we come up with here needs to be able to maintain that kind of performance if we are to build it into the core framework.

Unfortunately I haven't had time to collate the examples in this issue into the local state repo, my bad. I may not be able to get to it within the near term so if anyone else wants to pick that up I'd be fine with that.

With regards to performance of having hooks defined inside the build/render method (which I think someone mentioned earlier in this issue), I was reading through the React docs and saw this FAQ, might be useful. Basically it asks if hooks are slow due to creating functions in every render, and they say no due to a few reasons, one of which is being able to memoize functions using a hook like useMemo or useCallback.

https://reactjs.org/docs/hooks-faq.html#are-hooks-slow-because-of-creating-functions-in-render

Basically it asks if hooks are slow due to creating functions in every render, and they say no due to a few reasons, one of which is being able to memoize functions using a hook like useMemo or useCallback.

The worry isn't about the cost of creating closures, those are indeed relatively cheap. It's the difference between running any code at all and not running any code at all that is key to the performance Flutter exhibits in optimal cases today. We've spent a lot of effort making algorithms that literally avoid running certain code paths at all (e.g. the build phase being skipped entirely for AnimatedOpacity, or the way we avoid walking the tree to perform updates but instead just target the affected nodes).

I agree. I'm not too well versed on Flutter internals nor on hook internals but you're right that hooks will need to (if they don't already) figure out when they should run vs not, and performance must not regress.

It's the difference between running any code at all and not running any code at all that is key to the performance Flutter exhibits in optimal cases today

As mentioned previously a few times, hooks improve that.
The animated example on Tim's repo is proof of that. The hooks variant rebuilds less often than the StatefulWidget variant thanks to useMemo

Since it is being discussed about solutions for this issue somewhere in this thread, I'm labelling it as proposal as well.

I really would like to see hooks incorporated into flutter as was done with react. I look at state in flutter the same way I used to when I first used react. Since using hooks I would personally never go back.

It is so much more readable IMO. Currently you have to declare two classes with a stateful widget versus hooks where you just drop in usestate.

It would also bring some familiarity to flutter that react developers often don't have when they look at flutter code. Obviously comparing flutter with react is a dangerous path to go down, but I really think my developer experience with hooks is better than my experience without them.

I'm not hating on flutter btw, it's actually my favourite framework but I think this is a really good opportunity to increase readability and dev experience.

I think there is definitely an opportunity to improve the naming conventions and make them more flutter like.

Things like UseMemoized and UseEffect sound quite foreign, and it sounds like we want some way to not have to run the init() code in the build fxn.

Currently initializing with hooks is like this (I think?):

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

I appreciate the brevity of this code, but it's certainly far less than ideal from a readability and "self documenting code" standpoint. There's a lot of implicit magic going on here. Ideally we have something that is explicit about it's init/dispose hooks, and doesn't force itself into build when used with a Stateless Widget.

Things like useMemoized and useEffect could maybe be better named more explicitly hook ComputedValue() and 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);
}

I like that, not sure how I feel about the use of the hook keyword though, and I don't think it solves the issue of foreign concepts. Introducing new keywords doesn't feel like the best approach in my mind, withSideEffect or withComputedValue? I'm no language designer so my words are worthless.

I do feel like hook-like functionality in flutter will be a great help in smoothing the learning curve for React developers, which is really the target audience when companies are making the decision between ReactNative and Flutter.

Echoing @lemusthelroy , Flutter is by far my favourite framework and I'm beyond excited to see the directions it takes. But I feel functional-programming concepts could be a great help in growing the framework in an as yet relatively unexplored direction. I think some people are dismissing the idea in an aim to distance from React, which is unfortunate, but understandable.

Ya there's two sides to that coin I think. A new keyword is a major event, so knowledge propagation would be very swift, but the other side is certainly that it's now something new to _everyone_. If it's possible without that's cool too! Just not sure it is... at least not as elegantly.

Opinion: The community inclination to name hooks as the de-facto solution to this problem roots from a bias for functions. Functions are simpler to compose than objects, especially in a statically-typed language. I think the mental model of Widgets for many developers is effectively just the build method.

I think if you frame the problem in terms of the basics, you're more likely to design a solution that works well in the rest of the library.

As for the hook keyword in terms of the basics; one could look at it as both declaring and defining a function from some kind of template (a macro), and the hook prefix is really just calling out that the built-function has internal state (c-style statics.)

I wonder if there isn't some kind of prior-art in Swift FunctionBuilders.

While we're dreaming, I'll clarify my guess at what would be the necessary code:

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

Where Hook is a type-system level hack that helps statically analyze that the resulting hook was called in accordance with what hook-familiar developers know as hook laws. As that kind of a thing, the Hook-type can be documented as something that's a lot like a function, but has static internal mutable state.

I cringe a bit as I write this because it's such an oddity from a language perspective. Then again, Dart is the language born for writing user interfaces. If this kind of an oddity should exist anywhere, perhaps this is the place. Just not this oddity in particular.

Opinion: The community inclination to name hooks as the de-facto solution to this problem roots from a bias for functions. Functions are simpler to compose than objects, especially in a statically-typed language. I think the mental model of Widgets for many developers is effectively just the build method.

I'm not sure what you wanna say with that. The hook approach that I also use with my get_it_mixin just makes the widget tree easier to read than using a Builder.

Interesting article about React hooks

@nt4f04uNd All of your points were addressed previously, including performance, why it needs to be a core feature, functional vs class style widgets, and why things other than hooks don't seem to work. I suggest you read through the whole conversation to understand the various points.

I suggest you read through the whole conversation to understand the various points.

This is fair to say considering they didn't read the whole thread, but I'm not sure it makes things any more clear to read the rest of the thread. There are folks whose priority it is to keep Widgets as they are, and another group who wants to do something else entirely or make Widgets more modular.

While that may be true, this issue shows that there are problems which cannot be solved with widgets as they are currently, so if we want to solve the problems, we have no choice but to make something new. This is the same concept as having Futures and later introducing async/await syntax, the latter makes things possible that simply weren't possible without new syntax.

People _are_ suggesting that we make it part of the framework, however. React can't add new syntax to Javascript because it's not the only framework available (well, it can through Babel transforms), but Dart is specifically designed to work with Flutter (Dart 2 at least, not the original version) so we have a lot more of an ability to make hooks work together with the underlying language. React, for example, needs Babel for JSX, and it has to use a linter for useEffect errors, while we could make it a compile time error. Having a package makes adoption a lot harder, as you could imagine the traction that React hooks would(n't) have gotten had it been a third party package.

There would be no problem if there could be a third type of widget, i.e. HookWidget, besides current Stateless and Stateful widgets. Let the community decide which one to use. There is already a package from Remi but it has limitations inevitably. I tried it and it significantly reduced boilerplate but I had to drop it unfornutanetly due to limitations. I have to create stateful widgets for only to use init method. There could be additional great benefits if it is part of the core framework with the language support. Besides, a HookWidget can enable community to create more optimum and more performant apps.

I have to create stateful widgets for only to use init method.

You don't actually have to do this, useEffect() is capable of doing the initCall inside build. The docs make no effort at all to explain this tho, and basically assume you are a React dev who already knows how hooks work.

I was using that way but I had some other problems with the limitations of the package and I don't remember exactly what were them.

Was this page helpful?
0 / 5 - 0 ratings