-
Notifications
You must be signed in to change notification settings - Fork 213
StatefulWidget syntax in Flutter requires 2 classes #329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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. |
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 PS: if I could I would declare the abstract class Stateful<@immutable S> extends Widget Unfortunately, annotations are not allowed in generic type parameters. |
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. |
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 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 |
https://www.youtube.com/watch?v=dkyY9WCGMi0 explains why |
@munificent How about: class Foo {
Foo({ this.* });
final int bar;
final int baz;
} ...or some such, where class Foo {
constructor;
final int bar;
final int baz;
} |
@Hixie @tatumizer @munificent I could live with that. Couldn’t we use the same but with |
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 The real issue is the bad support of immutable objects. |
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. |
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 |
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.
It has a reference to the current configuration of the widget (
Widgets are immutable, but States are mutable. The whole point of a State, in fact, is to hold the mutable state of the widget. |
I think having |
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
<#329 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AEZ3I3JU3GYDJPSPQY7XAMDTFUBDLANCNFSM4HIGKXZA>
.
|
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
Have both states inside a different InheritedWidget/Provider/Listenable. 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. |
The parent widget will create a new |
The parent will create a new |
So how does the second |
What if it has different properties? e.g. |
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. :-) |
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()),
]
);
}
} |
There's a couple of problems with StateInsidefulWidget, also:
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.) |
Stateful widgets being const is very common and very useful. The state itself is not in the widget class. Widgets have three parts:
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 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 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 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. |
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 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
Yup, but it's not clever. It's just the 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. |
Pretty much every widget has a child property, builder property, or children property.
Not that it affects the argument here but for the record they can also be determined from data obtained from inherited widgets.
That's the documentation from |
If you're ok with just using To put it another way:
By definition if it has a subscription it's not stateless. |
Assuming that by "the result is the same" you mean |
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.
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.
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
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 |
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! |
I agree that states rarely need to be their own classes in the traditional sense -- you never make a new /// 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 |
How do you envision it being solved with static metaprogramming without breaking OOP readability ? |
I've seen proposals that suggest using a pattern similar to @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++),
),
);
} |
@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:
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:
This API also enabled react hooks-like capability.
This issue was moved by vsmenon from dart-lang/sdk#36700.
The text was updated successfully, but these errors were encountered: