flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond

Home Page:https://flutter.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Why should I ever use stateless widgets instead of functional widgets?

lukepighetti opened this issue · comments

Dart 2 shorthand makes writing functional widgets super nice, especially for a React/Javascript heathen like myself.

Why should I ever use Stateless Widgets?

screen shot 2018-07-11 at 12 24 06 pm

My simple answer would be.
It doesn't change its state, it only builds the widget. I'm not familiar with react native but I find Stateless widget very useful.
https://docs.flutter.io/flutter/widgets/StatelessWidget-class.html

Stateless Widget doesn't change its state either, right?

My best argument for using StatelessWidget is to create a widget that can be used to build tons of widgets that inherit its properties.

But this brings up another question. In react there are Functional components and Class based components. Functional components just build a component, like I have done with my functional widget above.

Class based components can be extended (iirc) and they have state.

So why would Flutter have a class based component for both Stateless and Stateful widgets? What benefit does this provide over Functional and Class based being the two methods of constructing widgets? (Where class based always has state)

Is a StatefulWidget more expensive to use than StatelessWidget?

There is nothing wrong with functions, but Widgets can be const and can have keys. Those are both very important features

commented

You also don't have the associated Element with the function. i.e. how would you put a button that navigates in the function if you don't have the BuildContext.

That's a good point. What stops someone from passing context as an argument, though?

commented

There's no Element associated with a function so you're passing in someone else's BuildContext. Then you have to manually make sure you get the right BuildContext. If you had:

class SomeWidget
  Widget build(BuildContext context) 
    a Navigator
      your function(context)

And your function tries to push onto that navigator, it'd fail or get the wrong one. Then you'd have to manually wrap your function in a Builder and you're more or less back to having a StatelessWidget.

So functional components for breaking down big trees but that's about it, it sounds like. Or fast prototyping.

commented

Ya, the main difference is in how it links to the Element tree (in that functions don't get references to Elements themselves and won't get triggered to rebuild themselves via the Element tree).

Some other examples:

  • If your subtree depends on an InheritedWidget, a StatelessWidget will get surgically be marked to rebuild itself. If you used a function, the widget that gave the function its BuildContext will get rebuilt.
  • A function itself won't get called to rebuild during hot reload. Someone else will have to bring it along. If you just passed a function into runApp at the root of your application, nothing will be rebuilt during hot reload.

A few other things :

  • Loose dart named and factory constructors
  • Loose refactoring options. Such as convert to stateful widget or Swap widget with child/parent

In the end your only gain is 2-3 lines of code. Lines which can be generated fairly easily with a code snippet.

For instance in vscode, just write stless and press tab.

ezgif com-crop 2

Oh and another one :

Widgets shouldn't have to care how other widgets are implemented. If you had to do new MyStatefulClass but then do myStatelessFunction() then it would be inconsistent

Which means that if you ever need to convert your stateless widget into a stateful or the opposite then it is a breaking change.

Thanks for your input Rémi, it is much appreciated. It seems like there are a lot of very valid reasons indeed to use Stateless & Stateful and not use a 'functional widget'.

I think the breaking change issue is very significant and I think I'm mostly convinced to always use a Stateless or a Stateful widget.

However, I think the number of lines problem is significant, writing code using a code snipet is not the problem here, it is reading those lines back. Also, if the 'functional widget' requires one parameter we go from five lines of code to one line of code, this is really significant when looking back through code and trying to work out what my code does (83% fewer lines of code, 54 chars vs 169, 68% fewer chars, again this is significant when looking back through code).

Widget MyFunctionalWidget(String text) => Text(text);

class MyWidgetComposition extends StatelessWidget {
  final String text;
  MyWidgetComposition(this.text);

  Widget build(BuildContext context) => Text('hello');
}

Flipping the question a little bit more (by the way I do love Flutter have dropped React Native for it!), is it ever ok to use a 'functional widget'? As a starter I would say it is ok privately inside a stateless or stateful widget.

Well, if you really dislike the class syntax; it should be fairly straightforward to fix most of the previous downsides using a code generator

You could write the following :

// foo.dart
part 'foo.g.dart';


@stateless
Widget _MyStateless({String foo}) => Text(foo);

which then generates the following

// foo.g.dart
part of 'foo.dart';

class MyStateless extends StatelessWidget {
  final String foo;

  MyStateless({this.foo, Key key}): super(key: key);


  @override
  Widget build(BuildContext context) {
    return _MyStateless(foo: foo);
  }
}

That would be a much more reasonable approach to functional widgets.

The downside is that IDE shortcuts such as Jump to definition would land on the generated code. And you'd still break convert to stateful widget. But you could add a support for it in the IDE

Would that be fine with you?

Thanks Remi, I'll have a play, that looks promising :-)

That would seem like a huge step in the right direction without going full JSX/DSX and is basically what I was trying to brainstorm in #19811

My current approach is to fit functional widgets only inside larger stateful/stateless class based widgets but even this isn't great for readability, and it makes my app nearly monolithic. If it never needs context or key, it stays functional, for example a _loading() => Center(child: CircularProgressIndicator()) widget should never be class based in my opinion, the boilerplate is not worth it.

The class format is really obtrusive in my opinion and if there is a way to reduce it without breaking anything else in an optional fashion would be ideal.

If I were writing a framework/package I would never use shorthand, but if I'm writing an app I am shorthand all the way.

This is a staple format for me

_title(String text) => Text(text, style: TextStyle(fontSize: 18.0));

I will NEVER

class _Title extends StatelessWidget {
  _Title(this.text);
  String text;

  @override
  Widget build(BuildContext context) {
    return Text(text, style: TextStyle(fontSize: 18.0),);
  }
}

If I could do this, I absolutely would, especially if it exposed context and key.

@stateless
_Title(String text) => Text(text, style: TextStyle(fontSize: 18.0));

And to be perfectly honest, I actually prefer that my widgets have a different syntax highlighting and style compared to built-in/pub widgets. It makes it so much easier to see what are widgets that I maintain, and what are widgets that someone else maintains.

@twistedinferno @rrousselGit did either of you get the @stateless / @stateful shorthand working? I would absolutely love to try it.

@stateful doesn't make sense.
But I can make a package for the @stateless.

Just out of curiosity, why do you say @stateful doesn't make sense? Just want to make sure I am understanding this fully.

And that would be fantastic and I would love to try it out and see what your solution is.

Sorry for the delay.
Here it is: https://github.com/rrousselGit/functional_widget

From

@widget
Widget foo(BuildContext context, { int value, int value2 }) {
  return Text('$value $value2');
}

it generates the following class for you to use:

class Foo extends StatelessWidget {
  final int value;
  final int value2;

  const Foo({Key key, this.value, this.value2}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return foo(context, value: value, value2: value2)
  }
}

I think this is solved and I'm going to close.
Please add a comment to have it reopened if you disagree.

There's no Element associated with a function so you're passing in someone else's BuildContext. Then you have to manually make sure you get the right BuildContext. If you had:

class SomeWidget
Widget build(BuildContext context)
a Navigator
your function(context)

And your function tries to push onto that navigator, it'd fail or get the wrong one. Then you'd have to manually wrap your function in a Builder and you're more or less back to having a StatelessWidget.

Could you elaborate further? I don't exactly understand either the principle or the pseudocode. I've tried pushing onto a navigator from a widget yielding function, using a BuildContext passed from the Widget, and it works as expected.

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: home(context),
    );
  }
}

Widget home(BuildContext context) {
  return Scaffold(
    body: Center(
      child: RaisedButton(
        onPressed: () => Navigator.of(context).pushNamed('/foo'),
        child: Text('bar'),
      ),
    ),
  );
}

This won't work. Because home doesn't create a new context, it reuses an existing one; which has a different location.

To solve this issue, you'd have to write:

Widget home() {
  return Builder(
    builder: (context) => Scaffold(
          body: Center(
            child: RaisedButton(
              onPressed: () => Navigator.of(context).pushNamed('/foo'),
              child: Text('bar'),
            ),
          ),
        ),
  );
}

But that Builder kinda defeats the point of functions.

I believe @rrousselGit is describing an edge case where the context doesn't have a navigator. The problem isn't passing context to a functional widget, the problem is passing a context that is unaware of a MaterialApp.

This would only be a problem in practice if you don't have any builder methods between MaterialApp and your functional widget. (I have often wondered why home: isn't a builder)

So the problem only arises if the passed context was created before the MaterialApp, which is supposed to configure the top level Navigator, correct?
On the other hand a Widget will always have a correctly configured Navigator on creation.

A good sums up:

Classes:

  • have hot-reload
  • are integrated into the widget inspector (debugFillProperties)
  • rebuild less often by overriding operator==
  • can define keys
  • ensure all widgets are used in the same way
  • ensure that switching between two different layouts correctly disposes of the associated resources
  • can use the context API
  • can be const

Functions have:

@Maldus512 you got it.

Oh and another one :

Widgets shouldn't have to care how other widgets are implemented. If you had to do new MyStatefulClass but then do myStatelessFunction() then it would be inconsistent

Which means that if you ever need to convert your stateless widget into a stateful or the opposite then it is a breaking change.

Again, I can't quite understand the situation you're depicting... Could someone give me a mode specific example?

PS: I'm curious about how Flutter & Dart work, hope my questions are not in the wrong place and/or annoying

Classes must start with an uppercase, functions with a lowercase.
And classes must be instantiated (with new or const), while functions can't.

This means that depending on how the widget is implemented, it changes how it's used. It's less impactful with new now being optional, but we may have to switch between foo() and Foo() all over the place,

Classes must start with an uppercase, functions with a lowercase.

This is just a lint, it is not required

@Maldus512 Like most languages there is a range of opinion on how valuable "standard practice" is. If you're coming from JS there is barely any standard practice, or it is always in flux. In Dart, standard practice is pretty well understood and doesn't change much. It is a feature to some and a bug to others. I have no opinion either way personally.

Does using stateful widget break functionality, because in my own opinion rather thinking if, either to use stateless or stateful, one can use stateful but expect additional lines of code

So I'm not trying to beat a dead horse or anything, but I'd just like to confirm that using functional widgets in all cases are bad practice?

Because I found this example, from the Flutter.dev layout tutorial example: on line 18 you'll find Widget buildHomePage(String title) {...} which is a functional widget which also contains widget as variables.

It's been very confusing to figure out what the best practices are =(

Because I found this example, from the Flutter.dev layout tutorial

There are some merged commits that did nothing but refactor these methods into widgets.

#26722

I don't think there's a broad announcement about this though. But that's definitely a bad idea

@rrousselGit thank you for clarifying this for me!

Guys, I get confused.

Should I really avoid to do something like this?

return ListView(
  children: <Widget>[
    _buildMyStatelessWidget(), // a simple Text widget
    _buildMyStatelessWidget2(context), // a widget that reads some properties from context.theme
    _buildMyStatefulWidget(),
   ]
);

@SaeedMasoumi can you show the rest of the code for _buildMyStatelessWidget and _buildMyStatefulWidget

@WantMoreCookies

_buildMyStatelessWidget() => ImageIcon(AssetImage("example.png"));
// or using const
_buildMyStatelessWidget() => const SizedBox(height: 20);

_buildMyStatelessWidget2(context) => Text(
      S.of(context).some_text,
      style: Theme.of(context).textTheme.title,
    );

_buildMyStatefulWidget() => MyScreen();

@SaeedMasoumi yea so this is exactly what you want to avoid doing. Instead you will want to do this:

class _Text extends StatelessWidget {
	const _Text(): super(key: key);

	Widget build(BuildContext context){
		return Text(
				S.of(context).some_text,
				style: Theme.of(context).textTheme.title,
			);

	}
}

class MainWidget extends StatelessWidget {
	const MainWidget(): super(key: key);

	Widget build(BuildContext context){
		return ListView(
			children: <Widget>[
				ImageIcon(
					AssetImage("example.png")
				),
				_Text,
				MyScreen(),
			]
		);
	}
}

@WantMoreCookies I don't know what are the downsides of my approach and why I should use classes instead, I can even achieve hot reloading with functional widgets.

Could somebody answer @SaeedMasoumi 's last comment? I'm also wondering why it's a bad approach. It seems way too much to create a StatelessWidget just for a SizedBox

What if instead of the code above, the ListView were inside a StatelessWidget? Would it then be ok to use functions to build the "small parts" of the class?

class ListViewWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(children: <Widget>[
      _buildMyStatelessWidget(), // a simple Text widget
      _buildMyStatelessWidget2(
          context), // a widget that reads some properties from context.theme
      _buildMyStatefulWidget(),
    ]);
  }
}

like so

@SaeedMasoumi @sergiuiacob1 - This and this written by @rrousselGit should help you understand why.

tldr; "The framework [Flutter] is unaware of functions, but can see classes. Functions are not inserted in the Widget tree and therefore are never associated to an Element. This causes a whole new set of problems, including performance issues or state not properly disposed."

@WantMoreCookies

This causes a whole new set of problems, including performance issues or state not properly disposed.

Is there any example to show these performance issues?

For example, what are the performance issues of the below function?

SizedBox vGap4() => const SizedBox(height: 4);

Maybe @rrousselGit can help.

TD;DR: misused functions are a lot more dangerous than misused classes. So use classes


Honestly, the performance aspect barely matters.
The real deal-breaker is that using functions can have a very undesired behavior.

Consider:

bool condition;

Widget _foo();
Widget _bar();

Widget build(BuildContext context) {
  return condition
    ? _foo()
    : _bar();
}

With that code, flutter is unable to understand when we switch between _foo and _bar.
This can have heavy consequences that I described a bit in the previous links.

While classes require a bit more code, they are a lot safer.

I know I am coming late to the party. But, that question always pops up again. I think an important aspect is that Flutter stops rebuilding if encounters an equal widget at the same position in the tree.

Hence, by overriding operator== in a stateless widget you can make Flutter stop rebuilding. That's impossible with functions. Flutter will always "expand" them.

The Flutter docs read "same instance", but in reality they mean "equal instance". So, I opened a ticket to ask the Flutter team to clarify and emphasize that in the docs: #38740

I have often wished for a shouldRebuild() method a la react, but it sounds like overriding operator gets something similar?

While classes require a bit more code, they are a lot safer.

@rrousselGit Would you please provide equivalent code for the Class paradigm? I thought I had this concept cleared up until reading that last comment 😓

TD;DR: misused functions are a lot more dangerous than misused classes. So use classes

Honestly, the performance aspect barely matters.
The real deal-breaker is that using functions can have a very undesired behavior.

Consider:

bool condition;

Widget _foo();
Widget _bar();

Widget build(BuildContext context) {
  return condition
    ? _foo()
    : _bar();
}

With that code, flutter is unable to understand when we switch between _foo and _bar.
This can have heavy consequences that I described a bit in the previous links.

While classes require a bit more code, they are a lot safer.

I'm not sure this is the case. Wouldn't the same problem apply to classes as well? If that build function is used as the build method of any Widget Flutter will face the same uncertainty... The problem here is with dynamic behavior in general, and neither classes nor functions can fix that.

Classes will dispose the element and state when they are switched.

With functions, it might happen that state leaks between succeeding trees. A password might suddenly appear somewhere else as readable text.

So is there any difference between using a function that returns a Widget and just inlining the Widget? For (a very contrived) example:

class Toggler extends StatelessWidget {
  final VoidCallback onToggle;
  final VoidCallback onAdvancedToggle;
  const Toggler({Key key, this.onToggle, this.onAdvancedToggle}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        FlatButton(onPressed: onToggle, onLongPress: onAdvancedToggle, child: Text('On')),
        FlatButton(onPressed: onToggle, onLongPress: onAdvancedToggle, child: Text('Off')),
      ],
    );
  }
}

Here I'm using a couple of member variables while building the FlatButton, and it looks like a candidate for DRYing up. However making it a stateless widget doesn't help at all, since then I still lose the member variables and just have to pass the same values. So my instinct is:

  //...

  Widget _toggleButton(String text) =>
        FlatButton(onPressed: onToggle, onLongPress: onAdvancedToggle, child: Text(text));

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        _toggleButton('On'),
        _toggleButton('Off'),
      ],
    );
  }

Is that bad? For at least some of the points made elsewhere, I think that using a function that returns a Widget should be the same as inlining code? But then again @rrousselGit's comment that

The real deal-breaker is that using functions can have a very undesired behavior.

makes me question whether I actually understand it. So I get that there are important differences between extracting a StatelessWidget and using a function. But is using a function different from inlining the code directly in the build method?

@hmayer00 inlining and using a function is exactly the same.

The difference is between inlining/function and a proper widget-class.

In the first case, Flutter aggressively tries to reuse the elements and state. In the, second case Flutter will only reuse the element and state iff the runtimeType and the key match.

Consider two forms with text edits at the same location in the widget tree and switching between the forms. Do you want the edited text to spill over from the first to the second form or reset when you switch between the forms? The first is achieved with inlining/function, the later with proper subclasses or different keys.

@derolf is on point.

If that wasn't clear enough, here's a concrete example:

Widget functionA() => Container()

@override
Widget build() {
  return functionA()  
}

is equivalent to:

@override
Widget build() {
  return Container();  
}

whereas:

class ClassA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();  
  }
}

@override
Widget build() {
  return ClassA()  
}

is equivalent to:

@override
Widget build() {
  return KeyedSubtree(
    key: ObjectKey(ClassA),
    child: Container(),
  );
}

Thanks @derolf and @rrousselGit. That's what I had thought, but things seemed tricky enough that I wanted to make sure I hadn't missed something. And you and others have in previous comments certainly convinced me of the value of widget classes (also it helps that functional_widget_annotation takes away some of the boilerplate when convenient - thanks @rrousselGit!).

Btw, to @derolf's point about keys, I'm generally averse to learning by video, but I found the intro video about keys to be extremely helpful.

@hmayer00 To understand how Flutter works, it's quite helpful to:

  • Always consider that Flutter doesn't contain any black magic. It follows clear rules (like the frame-by-frame widget matching).
  • Even if you don't need to write your own RenderObjects, just write some out of curiosity. That way you learn a lot about Flutter's rendering pipeline.

For those who don't want to use code-generators/functional_widget, there are other alternatives.

We can use "higher order functions".

TD;DR instead of:

Widget myWidget(String title) {
  return Text(title);
}

Builder(
  key: ValueKey(42),
  builder: (context) {
    return myWidget('hello world');
  },
)

you do:

final myWidget = functionalWidget((BuildContext context, String title) {
  return Text(title);
});

myWidget('hello world', key: ValueKey(42));

Here's the functionalWidget function's source code:

Widget Function(T props, {Key key}) functionalWidget<T>(
    Widget value(BuildContext c, T props)) {
  final uniqueWidgetIdentifier = UniqueKey();
  return (props, {Key key}) {
    var child = Builder(
      key: uniqueWidgetIdentifier,
      builder: (context) {
        return value(context, props);
      },
    );
    if (key != null) {
      return KeyedSubtree(
        key: key,
        child: child,
      );
    }
    return child;
  };
}

The downside is, if the props of your function are anything more complex than a String, you have to write a class for them:

class MyProps {
  String title;
  int count;
}

final myComplexWidget = functionalWidget<MyProps>((context, props) {
  return Text('${props.title} ${props.count}');
});

myComplexWidget(
  MyProps()
    ..title = 'Hello world'
    ..count = 42,
)

It'd be perfect if Dart had structures/records though.

I just use on my app Stateless widgets. What I use is a global provider that I share within my pages.
This provider updates the UI if it's needed. For example, if I want to change some UI values I do something like this from my Stateless widget:
Provider.of<Game>(context).addTeams();
and inside my provider I have this:

class Game extends ChangeNotifier {
  List teams = [];
  String something = '';

  addTeams() {
    Map newTeam = {
       name: 'New Team'
    }
   this.teams.add(newTeam);
   // and now this is the trick. I if need to update the UI I call this method
   notifyListeners();
  }
}

I find the Stateful widget not too easy to use/understand so I prefer to have something like this.

There are some specific scenarios that require me to update the UI without modifying the global provider. For this I just use StateFulBuilder: https://api.flutter.dev/flutter/widgets/StatefulBuilder-class.html

Am I understanding it right if I say

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
      child: const Text('Hello world', style: TextStyle(color: Colors.blue)),
    );
  }

Can be refactored as

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
      child: const _MyText(),
    );
  }
// ...
class _MyText extends StatelessWidget {
  const _MyText({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Text('Hello world', style: TextStyle(color: Colors.blue));
  }
}

And never as

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
      child: _buildMyText(),
    );
  }

  Widget _buildMyText() {
    return const Text('Hello world', style: TextStyle(color: Colors.blue));
  }

And it is fine for

  final List<Fruit> fruits;
//...
  @override
  Widget build(BuildContext context) {
    return Column(
      children: fruits.map((f) => _FruitInfo(fruit: f)).toList(),
    );
  }

to be refactored to

  final List<Fruit> fruits;
//...
  @override
  Widget build(BuildContext context) {
    return Column(
      children: _buildChildren(),
    );
  }

  List<Widget> _buildChildren() {
    return fruits.map((f) => _FruitInfo(fruit: f)).toList();
  }

and it is fine because the inline and function implementations are no different. Or is there another way to build a List<Widget> that I don't know of.

why a normal widget without class is wrong? I think it depends if you need a different context

There is no hard rule. It’s just best practices! The reasons were already given multiple times in this thread.

Here is the hard and fast logic again:

@hmayer00 inlining and using a function is exactly the same.

The difference is between inlining/function and a proper widget-class.

In the first case, Flutter aggressively tries to reuse the elements and state. In the, second case Flutter will only reuse the element and state iff the runtimeType and the key match.

Consider two forms with text edits at the same location in the widget tree and switching between the forms. Do you want the edited text to spill over from the first to the second form or reset when you switch between the forms? The first is achieved with inlining/function, the later with proper subclasses or different keys.

Wish primary constructor landed on dart dart-lang/language#138 so I don't need to use functional widget to reduce LoC.

Here is the hard and fast logic again:

@hmayer00 inlining and using a function is exactly the same.
The difference is between inlining/function and a proper widget-class.
In the first case, Flutter aggressively tries to reuse the elements and state. In the, second case Flutter will only reuse the element and state iff the runtimeType and the key match.

I read whole topic and I can't understand WHY?! Why Flutter aggressively tries to reuse the elements and state?
Could somebody correct me:
if I write a function, for example:

Widget functionA() => Container()

And assign it to constructor parameter.
the first would created widget "Container() and then this Container would insert in widget Center and then inside widget Scaffold (for example):

Widget home(BuildContext context) {
  return Scaffold(
    body: Center(
         child: functionA(),
    ),
  );
}

The first of all compiler initialise all objects and executes all methods/functions and create all widgets and after that Flutter will check elements and states. Then will make a decision to reuse element and render objects.

And I think the same will be when I use StatelessWidget.

But you say NO. Not the same. Could you describe step by step what difference? May be Flutter start check runtimeType and the key in different order? But how?!

commented

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.