Skip to content
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

Creating multiple classes from one structure #17

Open
Hixie opened this issue Jul 12, 2021 · 7 comments
Open

Creating multiple classes from one structure #17

Hixie opened this issue Jul 12, 2021 · 7 comments

Comments

@Hixie
Copy link
Collaborator

Hixie commented Jul 12, 2021

Consider a widget that receives a Listenable object, and wants to subscribe to it and rebuild each time it triggers. Currently this requires a lot of boilerplate:

class Foo extends StatefulWidget {
  Foo({ Key key, this.bar }) : super(key: key);
  final Bar bar;
  // ...
}

class _FooState extends State<Foo> {
  void initState() {
    super.initState();
    widget.bar?.addListener(_handleBar);
  }
  void didUpdateWidget(Foo oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.bar != oldWidget.bar) {
      widget.bar?.removeListener(_handleBar);
      oldWidget.bar?.addListener(_handleBar);
    }
  }
  void dispose() {
    widget.bar?.removeListener(_handleBar);
    super.dispose();
  }

  void _handleBar() {
    setState(() {
      // we use widget.bar in the build method
    });
  }

  // build...
}
  • You have to declare the argument in the Widget's constructor.
  • You have to declare the field in the Widget.
  • You have to listen to it in the State's initState.
  • You have to unlisten to it when it changes, and listen to the new one (State.didUpdateWidget).
  • You have to unlisten to it when the State is disposed.
  • You have to call setState when it triggers.

The current API makes this difficult because of the way this impacts two classes simultaneously. Ideally you'd only write something like:

stateful widget Foo { // creates both a Foo class and a _FooState class
  listenable field Bar bar; // "field" indicates that it should 
  // constructor and fields 
  Widget build(BuildContext context) {
    // ...something that uses bar...
  }
}

...and it would build everything else around you. However, in the current prototype, you have to hook everything to a class that will eventually be created, since we're just annotating to add code, we can't really change any of the code.

@jakemac53
Copy link
Owner

In #9 (comment) we started converging on what I think might be an interesting approach here.

Consider the following:

class ExampleState extends State<Example> {
  int count = 0;
  void increment() => setState(() => count++);
  
  @statefulWidget
  Widget _build(BuildContext context, String title) {
    return Scaffold(
      body: Text('$title ${count}'),
      floatingButton: FloatingButton(
        onPressed: () => increment())
      ),
    );
  }
}

Which would generate the widget class, something like the following:

class Example extends StatefulWidget {
  final String title;
  Example(this.title) : super();

  createState() => ExampleState();
}

And then also generates the build method in the state class, which would look like:

class ExampleState extends State<Example> {
  Widget build(BuildContext context) => _build(context, widget.title);
}

I believe you could similarly handle all of the other lifecycle methods as mentioned in your example.

@Hixie
Copy link
Collaborator Author

Hixie commented Jul 29, 2021

What is Example in the original code here?

@jakemac53
Copy link
Owner

jakemac53 commented Jul 29, 2021

What is Example in the original code here?

It doesn't exist in the original code - the user only writes the state class (so there is only one "structure", ExampleState).

The Example widget class is generated based on the annotated _build method - and the desired widget fields are extracted from the parameters to that function (so in this case the String title parameter becomes a field on the widget).

The actual build method on the state class is also code generated, and it calls your annotated build method with the requested parameters by grabbing them off the widget (in this case it would be Widget build(BuildContext context) => _build(context, widget.title);).

@Hixie
Copy link
Collaborator Author

Hixie commented Jul 30, 2021

I think creating the Example class (and having _ExampleState be implied) would be much more intuitive, since Example is how people think of these widgets, while _ExampleState is an internal implementation detail of the widget.

@jakemac53
Copy link
Owner

jakemac53 commented Aug 2, 2021

I think creating the Example class (and having _ExampleState be implied) would be much more intuitive

I agree with that statement - I just don't see how it would work without ripping that class apart which I think comes with worse downsides (imo), and is overall more confusing/magic. The widget classes aren't interesting, the state classes are really where are all the logic lives. So this is the best solution I have come up with, given the trade-offs (and capabilities currently available).

@munificent
Copy link
Collaborator

which I think comes with worse downsides (imo), and is overall more confusing/magic.

Since this is only a prototype anyway, maybe it's worth adding support for these capabilities and try it out. That would let us get a better feel for the user experience. I'm worried about things being too magical too, but at the same time, it would make the stateful widget macro a lot nicer to use in practice. Maybe we should at least try it out and see without committing to supporting those capabilities?

@jakemac53
Copy link
Owner

Since this is only a prototype anyway, maybe it's worth adding support for these capabilities and try it out.

There would be a bunch to hash out here, we could maybe use a bit of our upcoming meeting time to discuss some of the details?

I am not sure that the current prototype is good for identifying any usability concerns around either approach because ultimately the user in this case sees the fully generated file so it won't look any different. Although we could evaluate the author side some.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants