Custom Constructors for StatefulWidgets?

6,679 views
Skip to first unread message

Nicholas Manning

unread,
Dec 8, 2017, 11:48:53 AM12/8/17
to Flutter Dev
Hello there,

I have a Widget that a) renders some text and b) holds its *own* state (one of the 3 state management approaches recommended here in the docs).  

Therefore, I have custom constructor for this widget that takes in a DateTime object, but I need to pass this value down to the Widget's State class:

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

class FoobarWidget extends StatefulWidget {
 
final DateTime someDate;

 
FoobarWidget(this.someDate);

 
@override
  _FoobarWidgetState createState() => new _FoobarWidgetState(this.someDate);
}

class _FoobarWidgetState extends State<FoobarWidget> {
 
final DateTime someDate;

  _FoobarWidgetState
(this.someDate);

 
@override
  Widget build(BuildContext context) {
   
return new Container(
      child
: new Column(children: <Widget>[
       
new Text("some date is ${someDate}"),
     
]),
   
);
 
}
}

So I have 2 questions, one re the code above and one re SDK design related to this code:

1.  Is there a better way to do this?  (I've read the docs on the StatefulWidget class and the guide on StatefulWidgets but I'm still not clear.  The doc on the StatefulWidget class is TBH quite terse and its Bird class example could be improved: maybe something more realistic to make it easy to grasp.

2.  Overall, coming to this documentation for the first time and having developed iOS apps for years (and now React, React Native), I'm still not clear why:

  a.  A object called "<Something>State", extending a State object, is *also* is doing the rendering?  (Just kind of irks me a bit but maybe I'm not grasping something yet.)  

  b.  Does the team plan to provide something more straightforward in the future?  Of course, it's great to separate the actual Widget from the Widget's state object(s), but it seems to me there is still room for a more simplified approach for 80% of the folks out there that just want a clean straightforward widget.  

  c.  Is it just the style/preferences of the SDK authors to create so many specifically named widgets?  For example, it kind of surprised me I need to use AspectRatio class when the SDK can simply offer a special field on a Container object.  For another example, it kind of was quite strange / overkill to even have StatelessWidget and StatefulWidgets.  A widget is a widget and if it wants, it can hold / work with state.  

(On the whole, I think the Flutter documentation is fantastic overall and if your team keeps at constantly improving it will really speed up adoption even more.  Thanks for being so prompt and engaging with the help thus far in the group.)

Thanks in advance.

Adam Barth

unread,
Dec 8, 2017, 12:34:00 PM12/8/17
to Nicholas Manning, Flutter Dev
On Fri, Dec 8, 2017 at 8:48 AM Nicholas Manning <nicholas...@gmail.com> wrote:
I have a Widget that a) renders some text and b) holds its *own* state (one of the 3 state management approaches recommended here in the docs).  

Therefore, I have custom constructor for this widget that takes in a DateTime object, but I need to pass this value down to the Widget's State class:

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

class FoobarWidget extends StatefulWidget {
 
final DateTime someDate;

 
FoobarWidget(this.someDate);

 
@override
  _FoobarWidgetState createState() => new _FoobarWidgetState(this.someDate);
}

class _FoobarWidgetState extends State<FoobarWidget> {
 
final DateTime someDate;

  _FoobarWidgetState
(this.someDate);

 
@override
  Widget build(BuildContext context) {
   
return new Container(
      child
: new Column(children: <Widget>[
       
new Text("some date is ${someDate}"),
     
]),
   
);
 
}
}

So I have 2 questions, one re the code above and one re SDK design related to this code:

1.  Is there a better way to do this?  (I've read the docs on the StatefulWidget class and the guide on StatefulWidgets but I'm still not clear.  The doc on the StatefulWidget class is TBH quite terse and its Bird class example could be improved: maybe something more realistic to make it easy to grasp.

Generally, there isn't much reason to pass a value through the State constructor.  If you want to reference a property of the widget from the state, you can use the "widget" property of the state object.  In your example, the build() function can read "widget.someDate" directly.

2.  Overall, coming to this documentation for the first time and having developed iOS apps for years (and now React, React Native), I'm still not clear why:

  a.  A object called "<Something>State", extending a State object, is *also* is doing the rendering?  (Just kind of irks me a bit but maybe I'm not grasping something yet.)

By "doing the rendering," do you mean "implements the build() function"?  The reason has to do with the fact that closures in Dart implicitly capture "this".  If the build() function was on the widget, for example, then they would implicitly capture the current widget at that location in the tree.  By putting the build() function on the State object, closures implicitly capture the State object, which persists at that location in the tree even if the parent rebuilds with a new widget.

  b.  Does the team plan to provide something more straightforward in the future?  Of course, it's great to separate the actual Widget from the Widget's state object(s), but it seems to me there is still room for a more simplified approach for 80% of the folks out there that just want a clean straightforward widget.

I guess I'm not sure what you have in mind.  StatelessWidget is a more straightforward Widget.  Of course, StatelessWidget addresses fewer use cases than StatefulWidget, which is what lets it be simpler.

  c.  Is it just the style/preferences of the SDK authors to create so many specifically named widgets?  For example, it kind of surprised me I need to use AspectRatio class when the SDK can simply offer a special field on a Container object.

Yes.  The framework is built to emphasize composition.  Rather than baking many features into a single Container widget, each widget is focus on a single task, and the widgets can be easily composed into more complex widgets.  For example, Container itself is just a composition of a number of simpler widgets.  See the "design discussion" section in the documentation for the Padding widget for more information:

 
For another example, it kind of was quite strange / overkill to even have StatelessWidget and StatefulWidgets.  A widget is a widget and if it wants, it can hold / work with state.

It's true that StatefulWidgets solves a superset of the problems that StatelessWidget solves in the sense that you can always have an empty State object.  However, as you've mentioned above, there's more ceremony and syntax to implement a StatefulWidgets than a StatelessWidget.  We've included StatelessWidget in the framework so that simpler problems (those that do not require state) can be solved using a less verbose mechanism.

(On the whole, I think the Flutter documentation is fantastic overall and if your team keeps at constantly improving it will really speed up adoption even more.  Thanks for being so prompt and engaging with the help thus far in the group.)

Thanks!

Adam

michael.s.humphreys

unread,
Dec 8, 2017, 1:04:58 PM12/8/17
to Flutter Dev
The code above includes a final field at both the widget and state leveIs. Is there another pattern that avoids that?

Adam Barth

unread,
Dec 8, 2017, 1:31:25 PM12/8/17
to michael.s.humphreys, Flutter Dev
On Fri, Dec 8, 2017 at 10:05 AM michael.s.humphreys <michael.s...@gmail.com> wrote:
The code above includes a final field at both the widget and state leveIs.  Is there another pattern that avoids that?

Yes, the final field in the state object isn't needed.  Instead, the state object can refer to the widget's fields directly as "widget.someDate".

Adam

Nicholas Manning

unread,
Dec 9, 2017, 2:50:59 AM12/9/17
to Flutter Dev
Ok, thanks for the clarification but I'm not quite clear yet now because I realized "final" shouldn't be used as this value is mutable.  Therefore, as the docs say "StatefulWidget instances themselves are immutable and store their mutable state either in separate State objects that are created by the createState method, or in objects to which that State subscribes, for example Stream or ChangeNotifier objects, to which references are stored in final fields on the StatefulWidget itself."

So regarding the 3 options I have in the quote above:

1.  re "store their mutable state either in separate State objects" so I should move the "someDate" member then and *not* refer to widget.someDate in the "build" function, right?
2. storing state in "objects to which that State subscribes" seems like overkill for this situation, right?
3. storing state "ChangeNotifier objects" seems like overkill for this situation, right?

Therefore, since someDate is mutable, as the docs say I should store it not in the Widget but in the State object.  

So back to my original question: how can I initialize it with a value?  This is quite a common situation.  If I'm storing this in the State object which makes complete sense it seems like I'd need to pass this in via the "parent" Widget object no?

Thanks,

Adam Barth

unread,
Dec 9, 2017, 3:19:59 AM12/9/17
to Nicholas Manning, Flutter Dev
It's a bit hard for me to tell what you're trying to do, but here are some possibilities.

A) someDate changes because of something that happens to the parent widget. For example, if FoobarWidget's job is just to display the date in a fancy way, then when the parent rebuilds, it can create a new instance of FoobarWidget with the new state.  In this case, you can just use a simple StatelessWidget:

class FoobarWidgetA extends StatelessWidget {
  final DateTime someDate;

  FoobarWidget({ Key key, this.someDate }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new Text("the date is ${someDate}");
  }
}

B) someDate changes because of something that happens FoobarWidget and the parent needs to know about the new date. For example, if FoobarWidget's job is to let the user select a date, then the parent needs to be informed about which date the user has selected.  In this case, a good pattern to use is the "Checkbox" pattern (see https://docs.flutter.io/flutter/material/Checkbox-class.html).  In this pattern, the parent supplies both the someDate value and a callback that FoobarWidget should call when it wants to change the value.  The parent receives the new value, calls setState on itself, and rebuilds FoobarWidget with the new value, updating the UI:

class FoobarWidgetB extends StatelessWidget {
  final DateTime someDate;
  final ValueChanged<DateTime> onChanged;

  FoobarWidget({ Key key, this.someDate }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new Column(
      children: [
        new Text("the date is ${someDate}"),
        new RaisedButton(
          onPressed: () {
            onChanged(someDate.add(new Duration(day: 1)));
          },
          child: new Text("NEXT"),
        ),
      ],
    );
  }
}

C) someDate changes because of something that happens FoobarWidget and the parent does not need to know about the new date.  For example, if FoobarWidget's job is to let the use explore the phases of the moon on various dates.  In this case, a good pattern to use is the "initial value pattern".  In this pattern, the parent supplies an initial value, which FoobarWidget copies into a mutable field in its State object when the state object initializes.  As the user interacts with FoobarWidget, the field in the State object is mutated and setState causes FoobarWidget to rebuild with the new value.  The parent is never informed of the new value and if the parent rebuilds with a new initial value, the FoobarWidget ignores the new initial value because its state machine has advanced beyond initialization:

class FoobarWidgetC extends StatefulWidget {
  final DateTime initialDate;

  FoobarWidget({ Key key, this.initialDate }) : super(key: key);

  @override
  _FoobarWidgetState createState() => new _FoobarWidgetState();
}

class _FoobarWidgetCState extends State<FoobarWidgetC> {
  DateTime someDate;

  @override
  void initState() {
    super.initState();
    someDate = widget.initialDate;
  }

  void _advanceDate() {
    setState(() {
      someDate = someDate.add(new Duration(day: 1));
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Column(
      children: [
        new Text("the date is ${someDate}"),
        new RaisedButton(
          onPressed: _advanceDate,
          child: new Text("NEXT"),
        ),
      ],
    );
  }
}

Hope that helps.

Adam


--
You received this message because you are subscribed to the Google Groups "Flutter Dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to flutter-dev...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Nicholas Manning

unread,
Dec 10, 2017, 3:30:25 AM12/10/17
to Flutter Dev
Adam, these are indeed 3 common patterns and exactly the example I was looking for - so this was spot on and incredibly helpful.  Maybe forward this answer to your documentation team so they can put this on the documentation page re this topic?  I'll probably do a post on this topic soon either way.

Thank you!

Seth Ladd

unread,
Dec 11, 2017, 11:49:00 AM12/11/17
to Nicholas Manning, Flutter Dev
I've added a note to https://github.com/flutter/flutter/issues/7044 , where we are collecting "Effective Flutter" ideas :)

To unsubscribe from this group and stop receiving emails from it, send an email to flutter-dev+unsubscribe@googlegroups.com.

Nicholas Manning

unread,
Dec 11, 2017, 2:11:35 PM12/11/17
to Seth Ladd, Flutter Dev
Cool.  This for sure will also be converted to a tutorial on my side soon.


Mit freundlichen Grüßen,
Nicholas

Seth Ladd

unread,
Dec 11, 2017, 2:15:09 PM12/11/17
to Nicholas Manning, Flutter Dev
On Mon, Dec 11, 2017 at 11:11 AM, Nicholas Manning <nicholas...@gmail.com> wrote:
Cool.  This for sure will also be converted to a tutorial on my side soon.

+1, thanks for writing it up for everyone!

And, of course, thanks to Adam for the thorough answer :)

Ben Priebe

unread,
Aug 16, 2018, 6:36:44 PM8/16/18
to Flutter Dev
What about option D) which is the same as Option C) 

BUT...

When the parent rebuilds with a new initial value, the FoobarWidget DOES NOT ignores the new initial value but resets the state machine and starts the process again.

I've managed to do this by removing the 'final' from the initialDate but this goes against the docs stating that these values should be 'final'

class FoobarWidgetD extends StatefulWidget {
  DateTime initialDate;

  FoobarWidget({ Key key, this.initialDate }) : super(key: key);

  @override
  _FoobarWidgetState createState() => new _FoobarWidgetState();
}

class _FoobarWidgetDState extends State<FoobarWidgetC> {

  void _advanceDate() {
    setState(() {
      widget.initialDate = widget.initialDate.add(new Duration(day: 1));
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Column(
      children: [
        new Text("the date is ${widget.someDate}"),
        new RaisedButton(
          onPressed: _advanceDate,
          child: new Text("NEXT"),
        ),
      ],
    );
  }
}

What is the best practice here?

Ben Priebe

unread,
Aug 17, 2018, 2:05:43 AM8/17/18
to Flutter Dev
Ok. I ended up going with keeping track of the previous initialDate value and if it changes I then set a local state value that I use internally.

class FoobarWidgetD extends StatefulWidget {
  final DateTime initialDate;

  FoobarWidget({ Key key, this.initialDate }) : super(key: key);

  @override
  _FoobarWidgetState createState() => new _FoobarWidgetState();
}

class _FoobarWidgetDState extends State<FoobarWidgetC> {

   DateTime _previousInitialDate;
   DateTime _localDatelDate;

  @override
  void initState() {
    super.initState();
    _localDate = widget.initialDate;
  }

  void _advanceDate() {
    setState(() {
      _localDate = _localDate.add(new Duration(day: 1));
    });
  }

  @override
  Widget build(BuildContext context) {

   if (widget.initialDate != _previousInitialDate) {
       _previousInitialDate = _localDate = widgit.initialDate;
   }

    return new Column(
      children: [
        new Text("the date is ${_localDate}"),
Reply all
Reply to author
Forward
0 new messages