dart-lang / language

Design of the Dart language
Other
2.65k stars 203 forks source link

StatefulWidget syntax in Flutter requires 2 classes #329

Open ghost opened 5 years ago

ghost commented 5 years ago

@iskakaushik commented on Apr 22, 2019, 8:22 PM UTC:

Currently when we create a StatefulWidget, we need to declare one class for the Widget and one class for the State. This issue is to explore ways in which we can do it with just one class.

Existing discussion:

@Hixie: Just thinking off the top of my head, it would be interesting to experiment with a way flutter could declare new syntax that allowed people to declare a "statefulwidget" rather than a "class" and have that desugar into the widget and state classes. maybe we just tell people to use codegen for this, though...

@munificent: @yjbanov has had similar ideas for a while and spent a bunch of time talking to me about them. I'm very interested in this, though figuring out how to do it in a way that doesn't directly couple the language to Flutter is challenging.

It would be really cool if the language offered some static metaprogramming facility where you could define your own "class-like" constructs and a way to define what vanilla Dart they desugar to without having to go full codegen. Something akin to https://herbsutter.com/2017/07/26/metaclasses-thoughts-on-generative-c/.

If we do this right, we might even be able to answer the requests for things like "data classes" and "immutable types" almost entirely at the library level.

My hope is that after non-nullable types, we can spend some real time thinking about metaprogramming.

@yjbanov: If we're allowed language changes, then my current favorite option is something like:

// stateless
widget Foo {
  build() {
    ... access widget props ...
  }
}

// want stateful?
widget Foo {
  // add a `state {}` block
  state {
    String bar;
  }

  build() {
    ... access widget and state props ...
  }
}

One problem with the status quo is that conceptually developers want to "add state to a widget". The current way of moving between stateless and stateful does not feel like adding or removing state.

I don't think metaprogramming is sufficient to solve "data classes" or "immutables". Those features require semantics not expressible in current Dart at all.

And without language changes, I was thinking of experimenting with the following idea:

class Foo extends WidgetWithState<int> {
  int initState( ) => 0;

  build(context, int state) => Button(
    onTap: (context, int state) {
      context.setState(state + 1);
    },
    child: Text('Counting $state'),
  );
}

This API also enabled react hooks-like capability.

This issue was moved by vsmenon from dart-lang/sdk#36700.

leafpetersen commented 5 years ago

Can you provide some more context and requirements for the feature request? An example of why two classes are currently required could be helpful too.

munificent commented 5 years ago

I can give you some more context in person if you like.

leafpetersen commented 5 years ago

I can give you some more context in person if you like.

This would be good, but unless this is something that can't be shared publicly, it would be better to have at least a quick summary here as well, for future reference. If this was already discussed in a different issue tracker, just a link would be fine?

munificent commented 5 years ago

This page is a good intro to stateful widgets in Flutter. You can skim it to get the gist.

The relevant part for our discussion is that if you want to define a tiny widget that is also stateful, you have to define two separate classes. A minimal stateful widget looks like:

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

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

class _FooState extends State<Foo> {
  bool _state = false; // <-- The actual state.

  void _handle() {
    setState(() {
      _state = !_state;
    });
  }

  Widget build(BuildContext context) {
    // Return the concrete widgets that get rendered...
    // Also, something in here has an event handler that calls `_handle()`...
  }
}

That's a lot of boilerplate just to make a widget look different when you toggle a single bool.

This exacerbates the "build methods are too big and hard to read" problem. Imagine you have a monolithic stateful widget whose build method is getting unwieldy. Ideally, you'd take some subtree of that and hoist it out to its own little standalone widget. The kind of refactoring you do every day at the method level when a method body gets to big — pull some of it out into a helper.

But to do that for a stateful widget requires declaring a new widget class and a new state class. You need to wire the two together. The bits of state become fields in the state class. They often need to be initialized, which means a constructor with parameters that forward to those fields...

It's a lot of boilerplate, so users often leave their widgets big and chunky instead.

I'm not sure if this is a problem that is best solved at the language level. Like @yjbanov suggests (and like the new React Hooks stuff which everyone is really excited about right now), it seems like you should be able to express the same thing without declaring an actual state class.

Hixie commented 5 years ago

My preference would be to experiment with codegen before we do anything with the language. Until a syntax becomes popular, I'd be reluctant to adopt anything here.

yjbanov commented 5 years ago

I wrote a sketch for a stateful widget that does not require a second class: https://github.com/yjbanov/stateful (see usage example in the test).

In this design the state object is immutable, and just like Widget, provides configuration parameters to the build method.

PS: if I could I would declare the Stateful class like this:

abstract class Stateful<@immutable S> extends Widget

Unfortunately, annotations are not allowed in generic type parameters.

kasperpeulen commented 5 years ago

@yjbanov

I like how this looks:

class Counter extends Stateful<int> {
  Counter(this.greeting);

  final String greeting;

  int createInitialState() => 0;

  @override
  Widget build(StatefulBuildContext context, int state) {
    return Boilerplate(Column(children: [
      Text('$greeting, $state!'),
      GestureDetector(
        onTap: () {
          context.setState(state + 1);
        },
        child: Text('Increment'),
      ),
    ]));
  }
}

I'm not per se against two classes. But it feels mainly weird to me that the build method is in the state class, when I assumed that the StatefulWidget has the build method, and the State class only data.

kasperpeulen commented 5 years ago

This exacerbates the "build methods are too big and hard to read problem". Imagine you have a monolithic stateful widget whose build method is getting unwieldy. Ideally, you'd take some subtree of that and hoist it out to its own little standalone widget. The kind of refactoring you do every day at the method level when a method body gets to big — pull some of it out into a helper.

But to do that for a stateful widget requires declaring a new widget class and a new state class. You need to wire the two together. The bits of state become fields in the state class. They often need to be initialized, which means a constructor with parameters that forward to those fields...

It's a lot of boilerplate, so users often leave their widgets big and chunky instead.

I agree with your point @munificent, but I think even if we have what @yjbanov suggests, it still takes quite a lot of boilerplate to make a widget. I think it is too hard to make a small sweet class in Dart. Compare this with how this would look in Kotlin:

Dart:

class TodoItem extends StatelessWidget {
  Foo({@required this.body, @required this.completed, Key key}) : super(key: key);
  final String body;
  final bool completed;

  @override
  Widget build(BuildContext context) => ListItem(...);
}

Kotlin:

class TodoItem(val body: String, val completed: bool, key: Key): StatelessWidget(key) {
  override fun build(context: BuildContext): Widget = ListItem(...);
}

I have heard concerns around here that this is not scalable or something, but in Kotlin it is used all the time, at work we use it in a very large codebases, it just works very well. When the class grows it will look like this:

class TodoItem(
  val body: String, 
  val completed: bool,
  val labels: List<Label> = []
  val project: Project? = null,
  key: Key
): StatelessWidget(key) {
  override fun build(context: BuildContext): Widget = ListItem(...);
}
munificent commented 5 years ago

Yeah, I'm with you. The syntactic cost to move some values into the fields of a class in Dart is too damn high. However, I don't know if moving to something like a Scala/Kotlin-esque "primary constructor" notation would be a good fit for Dart. Dart has named constructors and factory constructors, which are useful features, but mean there's no real notion of an implicit unnamed "primary" constructor. It's entirely idiomatic in Dart to define a class with no unnamed constructor and only named ones. Kotlin's syntax doesn't play nicely with that.

A half-formed idea I've had instead is that if a class only has a single generative constructor, we could allow parameters to it to implicitly declare fields. Right now, constructor parameters can initialize fields using this. but the field still has to be declared too.

Instead, we could allow something like, I don't know:

class TodoItem extends StatelessWidget {
  TodoItem({
    @required final String this body,
    @required final bool this completed,
    Key key
  }) : super(key: key);

  @override
  Widget build(BuildContext context) => ListItem(...);
}

Where the this means "declare and initialize a field with that name and type. I don't know if the syntax is too confusing with this., but maybe there's something along these lines we could do.

Hixie commented 5 years ago

https://www.youtube.com/watch?v=dkyY9WCGMi0 explains why build is on State rather than on StatefulWidget.

Hixie commented 5 years ago

@munificent How about:

class Foo {
  Foo({ this.* });
  final int bar;
  final int baz;
}

...or some such, where this.* (or whatever) implicitly means "all the fields". Or some briefer-still syntax like:

class Foo {
  constructor;
  final int bar;
  final int baz;
}
kasperpeulen commented 5 years ago

@Hixie @tatumizer I like the idea of “data classes”/records like that, but Im not sure how that would look like in my widget above with inheritance. And also how do you give a final field a default value? How do you mark it as required in the constructor?

@munificent I could live with that. Couldn’t we use the same but with final Type this.property as parameter instead of this new syntax final Type this property? Or would that break too much?

rrousselGit commented 5 years ago

React hooks is technically doable already, there are multiple implementations available. Including flutter_hooks

This hook widget doesn't need to be defined in two classes.

Similarly, we can play around the StatefulWidget class a bit to express it as a single class. I made one a long time ago:

https://stackoverflow.com/questions/53019294/what-is-the-usefulness-of-immutable-statefulwidget-and-state-in-flutter-but-ca/53019505#53019505

The real issue is the bad support of immutable objects. But I trust that the WIP extension members will improve code-generators exponentially in that aspect.

munificent commented 5 years ago
class Foo {
  Foo({ this.* });
  final int bar;
  final int baz;
}

...or some such, where this.* (or whatever) implicitly means "all the fields". Or some briefer-still syntax like:

class Foo {
  constructor;
  final int bar;
  final int baz;
}

struct Foo { final int bar; final int baz; }

All of these work where you treat the fields as canonical and infer constructor parameters from them. The main problem I see with that approach is that constructor parameters have an extra bit of data that fields lack: whether or not they are named. By making the parameter canonical and inferring the fields from them, you have the freedom to choose which of those parameters you want to be named, positional, optional, etc.

Hixie commented 5 years ago

I'd be fine with requiring the use of named fields. But you could do something like:

class Foo {
  Foo({ this.* });
  final int bar;
  final int foo;
}

vs:

class Foo {
  Foo(this.*);
  final int bar;
  final int foo;
}

...to distinguish between all-named and all-positional.

I think it's fine to not support every use case with the syntactic sugar. After all, we can already do the complicated cases. It's just that the simple cases are verbose.

That said, this isn't a solution to the original problem in this bug.

temaivanoff commented 4 years ago

Hey. it would be nice to have elegant syntax without two classes. Now in the first grade we most often do static mapping, which could simply be avoided.

danaugrs commented 3 years ago

Any updates on this? Having to write two classes for every stateful widget is completely unergonomic. Why not have the properties of a stateful widget class comprise the state of that widget? Flutter shouldn't have to destroy the widget object and recreate it, but rather just execute the build method.

Hixie commented 3 years ago

I watched this presentation and some other videos on youtube, and I still have no idea why build method HAS TO be on State.

It doesn't have to be on State. I mean, it's all just design choices. We didn't have to have State at all, or even Widgets. :-) The video explains why we made the choice to have it on State.

In a current design, State object has a reference to the widget whose state it reflects.

It has a reference to the current configuration of the widget (widget property) and a reference to the Element whose state it reflects (context).

I assumed the State object has to be immutable. Does it really have to?

Widgets are immutable, but States are mutable. The whole point of a State, in fact, is to hold the mutable state of the widget.

danaugrs commented 3 years ago

I think having build on state and having to write two classes for stateful widgets are poor design choices. I think Flutter could allow stateful widgets to be created using a single class (close what the state class is) and create the widget behind the scenes when necessary. Both React and Jetpack Compose do this and are a lot more ergonomic, requiring the developer to write a lot less boilerplate code.

rrousselGit commented 3 years ago

Don't forget that dart has no built in solution for cloning objects yet

You'd had to use a code generator or manually write the "copyWith", which dramatically decrease the viability of the solution.

And you also have to consider state mixins, like with TickerProvider. They wouldn't work with such architecture

And in the end, whether "build" is on the widget or not, we'd still end up with two classes: The widget and the state.

On Fri, Mar 26, 2021, 22:12 Daniel Salvadori @.***> wrote:

I think having build on state and having to write two classes for stateful widgets are poor design choices. I think Flutter could easily allow stateful widgets to be created using a single class (close what the state class is) and create the widget behind the scenes when necessary. Both React and Jetpack Compose do this and are a lot more ergonomic, requiring the developer to write a lot less boilerplate code.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/dart-lang/language/issues/329#issuecomment-808554635, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEZ3I3JU3GYDJPSPQY7XAMDTFUBDLANCNFSM4HIGKXZA .

rrousselGit commented 3 years ago

Why? I just redirected one method from State to Widget. Do you mean that TickerProvider will somehow detect my method redirected and stop working in protest? I don't get this part.

TickerProviderStateClientMixin has some internal state

With the single class syntax, this state needs to live somewhere. But a mixin on the Widget would be unable to add properties its associated state, because of how classes works.

There are still ways to implement it, but it's a bit inconvenient. Especially if we want to support setState(state.copyWith(foo: 42) (which IMO is very valuable).

what if your Widget depends on 2 State objects?

Have both states inside a different InheritedWidget/Provider/Listenable. Then widgets can listen to one or another or both

The state examples you gave are rarely local widget state and are instead global to the app, so it's not much of an issue.

Hixie commented 3 years ago

The parent widget will create a new WeatherReport object every time it builds.

Hixie commented 3 years ago

The parent will create a new WeatherReport each frame, which will mean a new WeatherReport will have its build method called each frame, so you will have a new WeatherService subscription each frame, and dispose is never called.

Hixie commented 3 years ago

So how does the second WeatherReport instance get access to the weather subscription?

Hixie commented 3 years ago

What if it has different properties? e.g. WeatherReport(zip: '94040') vs WeatherReport(zip: '94043')

Hixie commented 3 years ago

Best way to figure out what it's doing is to step through it in a debugger, or read the source. It's all open source. :-)

Hixie commented 3 years ago

What problem are we trying to solve here? It's not clear to me that the above is any better than:

class WeatherReport extends StatefulWidget {
  State<WeatherReport> createState() => _WeatherReportState();
}

class _WeatherReportState extends State<WeatherReport> {
  late final weather;
  // 2 methods to manage the state
  initState() {
    super.initState();
    weather = WeatherService.subscribe(this);
  }
  dispose() {
    weather.unsubscribe();
    super.dispose();
  }

  Widget build(BuildContext context) {
    return Row(
       children: [
         Icon(weather.isRaining? rainIcon: sunIcon)),
         Text(weather.temperature.toString()),      
       ]
    );
  }
}
Hixie commented 3 years ago

There's a couple of problems with StateInsidefulWidget, also:

The problem I'm trying to solve here (primarily) is: "StatefulWidget syntax in Flutter requires 2 classes".

My understanding of that problem is that the real problem isn't that there's two classes, it's that it's verbose. We could have three classes; if it was less verbose, that would be better.

For example, suppose one could do:

stateful widget WeatherReport {
  @autounsubscribe
  late final weather = WeatherService.subscribe(this);
  Widget build(BuildContext context) {
    return Row(
       children: [
         Icon(weather.isRaining? rainIcon: sunIcon)),
         Text(weather.temperature.toString()),      
       ]
    );
  }
}

...and it generated the exact same code as my example above at compile time. Would the fact that there are two classes involved still be a problem? Or would it solve the problem because now it's not verbose?

(This specific syntax wouldn't work, of course, since there's nowhere to put the widget configuration, etc.)

Hixie commented 3 years ago

Stateful widgets being const is very common and very useful. The state itself is not in the widget class.

Widgets have three parts:

  1. the configuration, which you can think of like a literal number or string (except it's a struct) that describes the current configuration of the widget. That's the Widget subclasses, they're all immutable (all fields final, all fields have values that themselves don't change unless they are subscribable).

  2. the long-lived core, which you can think of like the chassis or nervous system or inflated representation. This node is part of a tree that you can see in DevTools or with debugDumpApp. These nodes are called "Element"s. BuildContext objects are actually just these Elements (literally, context is Element returns true). These objects know how to be updated with new configurations, they know how to handle changing their child nodes, they know how to walk their tree and how to manage the inheritance logic from InheritedWidget. (Widget.createElement is how these come to exist.)

  3. the brains of each widget. StatelessWidgets don't have anything interesting here. StatefulWidgets have a State. RenderObjectWidgets have a RenderObject. These could have just been part of #2 but by splitting out the interesting logic that varies per widget from the machinery that applies to every widget we make it harder for people to break invariants or call sensitive APIs. These parts have internal state that mutates a lot, they hold the objects that have defined lifetimes like subscriptions and tickers and controllers and the like. (The Element creates these by calling StatefulWidget.createState or RenderObjectWidget.createRenderObject and so on on the first widget configuration they receive.)

Configurations (#1) don't have context. Just like a raw string, you could place them in many parts of the tree. For example, you can pass the string 'Foo' to many Text widgets. Similarly, you can pass the widget const Text('foo') to many widgets. Since the configuration itself has no lifecycle, no logic, no meaning beyond just raw configuration, it can be in many places in the tree, it can be const, it can be created once and reused many times.

The Elements (#2) and their delegates (#3), on the other hand, have a very well-defined lifetime. They exist, they have identity. They come to be in a particular place in the tree (initState), they may move around the tree via GlobalKey reparenting (deactivate/reactivate), and eventually they go away (dispose). They never come back after that. They are always in the tree for every frame of their existence. They may over time receive different configurations — be red, be blue, be green — in the form of different configurations (#1, i.e. widgets, didUpdateWidget).

This setup means that configurations (Widgets) can be passed around without worrying about what the consumer will do with it. For example, you can pass a widget as the child of another, and that second widget can decide to pass it to two more widgets, or can decide to include it or exclude it from the tree on alternating frames, or can pass it to yet another widget. It also means that when updating the tree, if you find that the exact same instance of a widget is being used again as the configuration for an element, you don't have to update that element or its subtree. (This is why const helps performance so much, but it also works with "reprojection", where a widget was passed in from a parent and is being then passed down again to a child. See for example the child property of AnimatedBuilder.)

Long ago, Flutter had a single object that represented #1, #2, and #3. Conflating these concepts however doesn't work. In such a world, you can't have the same configuration twice in the tree at the same time. You have to keep a tight leash on what another widget does if you pass it a child, because if they include it in two places in their tree somehow then everything will break. You have to make sure they don't include it then remove it then include it again because that will break the lifecycle.

Anyway I don't know if that is helpful. Maybe this text could be tweaked and put in the widget library documentation somewhere, if it is.

Hixie commented 3 years ago

The question here is: how can Flutter know that the parent changed the configuration of the child widget?

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/framework.dart#L3343

If the configuration hasn't changed (i.e. the new configuration is the same Widget instance as the old configuration) then we go down the path that matches child.widget == newWidget and we may update the slot but otherwise don't do anything (this short-circuits any further descent into the tree, which is where we get performance gains).

If the configuration has changed (i.e. the new configuration is not the same Widget instance as the old configuration, but they are the same type and have the same key) then we go down the next code path, which calls update on the child.

Probably it has some way to compare the old configuration with the new one?

Yup, but it's not clever. It's just the == linked to above. If you create an otherwise-identical Widget instance, it's treated as a new configuration.

The reason we don't do a deep comparison is that it turns out that would be way less efficient. Consider the top widget in the tree. It has a child widget. So when comparing the configurations, you'd have to compare all the properties of that widget including its child. But then that child has a child too. So you'd have to compare that widget and its child as well. All the way down the tree (sort of, it's a bit more complicated because of builders).

So you do that, you find the last node in the tree is slightly different, so the top one must be considered different, you update it and continue to the next child... and then you have to do the whole comparison all over again. So you end up making widget updates O(N^2) (and N is very big) instead of O(1). It turns out that redundantly updating a few widgets is way cheaper than doing the deep comparison.

Hixie commented 3 years ago

But I thought (maybe wrongly) that StatelessWidgets with the child/children passed by the parent must be less common.

Pretty much every widget has a child property, builder property, or children property.

because in Stateless Widget, everything depends only on the configuration parameters

Not that it affects the argument here but for the record they can also be determined from data obtained from inherited widgets.

And I actually looked at the section of StatelessWidget docs describing == operator before posting the question, expecting it to recommend overriding this method to implement comparison, but the wording is pretty generic, there's nothing there indicating the importance of the operation. Not sure what this all means

That's the documentation from Object.==. It's overridden on Widget to specify @nonVirtual on operator == so that people get a lint warning them not to override == for widgets.

Hixie commented 3 years ago

So, the technique could probably be often useful, but that's only a part of my point.

If you're ok with just using identical for the grandchild, why not do it for the child too (like the framework does)? It's massively simpler (no need to override ==), less bug-prone (it's very easy to forget to include all properties in ==), and the performance is not very different (most of the time the grandchild won't be identical, by definition -- if it was usually identical, then the child would also usually be identical, and we wouldn't need to worry about performance in the first place).

To put it another way:

Now imagine a Stateless widget that does the following: creates a subscription to some service

By definition if it has a subscription it's not stateless.

Hixie commented 3 years ago

Assuming that by "the result is the same" you mean identical and assuming that by "behavior" you are being broad in your definition, then I would agree that it is either literally stateless or that any state it may have would be useless.

Hixie commented 3 years ago

The problem here is that you're trying to define stateless vs stateful by reference to their behaviour while the actual definition of stateless vs stateful is about their implementation. A widget "has state" literally if it has some form of data or logic that it allocates, mutates, or that otherwise has a lifecycle. A stateless widget is one where the entirety of the behaviour could be inlined at compile time without loss of generality.

So for example, a widget that always returns the same object out of its build method but that keeps track of how many times it was built, even if it doesn't do anything with that information, is stateful. A widget that is aware in some sense of whether it is currently "in the tree" or not is stateful. Another example: it's common for one to create a stateful widget that does complicated logic (e.g. talking to the network, a database, or some such) but always returns the same widget from its build, namely an InheritedWidget that provides access to the state of the StatefulWidget.

So we can say that for Stateless Widgets, all their observable behavior is a function (in a mathematical sense) of the configuration parameters (and nothing else). (The parameters coming from InheritedWidget are considered configuration parameters, too: the fact that they are not explicitly passed to the constructor is immaterial (*))

The same can be said for stateful widgets. In JetPack Compose, both stateless and stateful widgets (or rather, their equivalent thereof) in fact are literally functions.

Now let's define a notion of well-behaved Stateful Widgets. Suppose I write another MysteryWidget class, but this time I claim it's a Stateful widget. My conjecture is that my MysteryWidget can be called a well-behaved Stateful Widget if and only if its behavior is a function of configuration parameters and t (current time). In other words, if you create 2 instances of MysteryWidget with the same configuration parameters, their behavior is indistinguishable at any given moment t ("given moment" in dart's terms means: the same event-loop iteration).

Can we agree on this definition?

A stateful widget's behaviour can be a function of many more things than just time. For example, it could open a network connection. It could have a Random object. It could subscribe to other objects. It could track how many times it was built. Some of these would be bad form, and I agree that generally speaking you should expect two builds in sequence on the same State object to return more or less equivalent results (at least as far as the user is concerned), but that's mostly orthogonal to whether it's actually stateful or not.

(*) in a discussion of StatelessWidget, the parameters coming to MysteryWidget from Inherited Widget are deemed to be time-independent, otherwise, the MysteryWidget cannot be considered Stateless, according to the above definition ("Inherited Widget", in this case, turns into a carrier of State).

It's quite possible to have a stateless widget that returns a different result each time it is called, e.g. because it is fetching data from an InheritedWidget that itself returns different data each time. (I agree that it would be bad form for a stateless widget to return values based on DateTime.now(), Random().nextDouble(), global variables that it mutates, or other such behaviour, but technically speaking all those things could be done by a stateless widget and it would still be considered stateless.)

Hixie commented 3 years ago

I'm not really sure what you're proposing exactly, or how it exactly relates to this issue. I would recommend trying to implement it. Flutter was designed to allow the widgets layer to be forked and replaced, maybe you can provide the first such fork!

Levi-Lesches commented 2 years ago

I agree that states rarely need to be their own classes in the traditional sense -- you never make a new State with parameters that need to be distinguished from other States, and rarely ever do you end up subclassing a State. Naming and documenting is always a chore -- for a widget MyWidget, the class declaration almost always looks like this:

/// A state for [MyWidget]. 
class _MyWidgetState extends State<MyWidget> { }

However, with static metaprogramming (#1482) poised as the next big Dart feature, I think this issue can be solved fairly simply without the need for anonymous classes (as you can apply codegen to a "simpler" construct like a function or StatelessWidget).

cedvdb commented 2 years ago

How do you envision it being solved with static metaprogramming without breaking OOP readability ?

Levi-Lesches commented 2 years ago

I've seen proposals that suggest using a pattern similar to StatefulBuilder:

@FunctionalWidget(name: "CounterPage")
Widget counterPage(BuildContext context, StateSetter setState, {required int count}) => Scaffold(
  body: Text("You pressed the button $count times"),
  floatingActionButton: FloatingActionButton(
    onPressed: () => setState(() => count++),
  ),
);

Which would generate:

class CounterPage extends StatefulWidget {
  int count;
  const CounterPage({required this.count});
  _CounterPageState createState() => _CounterPage();
}

class _CounterPageState extends State<CounterPage> {
  @override
  Widget build(BuildContext context) => Scaffold(
    body: Text("You pressed the button $count times"),
    floatingActionButton: FloatingActionButton(
      onPressed: () => setState(() => count++),
    ),
  );
}