Language: StatefulWidget syntax in Flutter requires 2 classes

Created on 24 Apr 2019  Â·  17Comments  Â·  Source: dart-lang/language

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

Most helpful comment

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.

All 17 comments

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.

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

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?

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.

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.

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.

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

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

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.

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

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

How about

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

Struct is a new keyword, not constrained by legacy, so it can be automatically equipped with all necessary features (TBD) to represent an immutable state. (This was discussed eons ago, there was even an issue open, but ended with the verdict WontFix or ConsideredStale or something)

One interesting possibility: "struct" can accept different annotations controlling the code automatically generated by compiler (without the separate step of code generation).

@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?

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.

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.

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.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kevmoo picture kevmoo  Â·  3Comments

har79 picture har79  Â·  5Comments

moneer-muntazah picture moneer-muntazah  Â·  3Comments

jonasfj picture jonasfj  Â·  3Comments

eernstg picture eernstg  Â·  5Comments