What is the difference between functions and classes to create reusable widgets?
Asked Answered
C

7

338

I have realized that it is possible to create widgets using plain functions instead of subclassing StatelessWidget. An example would be this:

Widget function({ String title, VoidCallback callback }) {
  return GestureDetector(
    onTap: callback,
    child: // some widget
  );
}

This is interesting because it requires far less code than a full-blown class. Example:

class SomeWidget extends StatelessWidget {
  final VoidCallback callback;
  final String title;

  const SomeWidget({Key key, this.callback, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
      return GestureDetector(
        onTap: callback,
        child: // some widget
      );
  }
}

So I've been wondering: Is there any difference besides syntax between functions and classes to create widgets? And is it a good practice to use functions?

Centroid answered 10/11, 2018 at 0:3 Comment(1)
I found this thread very useful for my understanding of the issue. reddit.com/r/FlutterDev/comments/avhvco/…Bible
C
464

Edit: The Flutter team has now taken an official stance on the matter and stated that classes are preferable. See https://www.youtube.com/watch?v=IOyq-eTRhvo


TL;DR: Prefer using classes over functions to make reusable widget-tree.

EDIT: To make up for some misunderstanding: This is not about functions causing problems, but classes solving some.

Flutter wouldn't have StatelessWidget if a function could do the same thing.

Similarly, it is mainly directed at public widgets, made to be reused. It doesn't matter as much for private functions made to be used only once – although being aware of this behavior is still good.


There is an important difference between using functions instead of classes, that is: The framework is unaware of functions, but can see classes.

Consider the following "widget" function:

Widget functionWidget({ Widget child}) {
  return Container(child: child);
}

used this way:

functionWidget(
  child: functionWidget(),
);

And it's class equivalent:

class ClassWidget extends StatelessWidget {
  final Widget child;

  const ClassWidget({Key key, this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: child,
    );
  }
}

used like that:

new ClassWidget(
  child: new ClassWidget(),
);

On paper, both seem to do exactly the same thing: Create 2 Container, with one nested into the other. But the reality is slightly different.

In the case of functions, the generated widget tree looks like this:

Container
  Container

While with classes, the widget tree is:

ClassWidget
  Container
    ClassWidget
      Container

This is important because it changes how the framework behaves when updating a widget.

Why that matters

By using functions to split your widget tree into multiple widgets, you expose yourself to bugs and miss on some performance optimizations.

There is no guarantee that you will have bugs by using functions, but by using classes, you are guaranteed to not face these issues.

Here are a few interactive examples on Dartpad that you can run yourself to better understand the issues:

Conclusion

Here's a curated list of the differences between using functions and classes:

  1. Classes:
  • allow performance optimization (const constructor, more granular rebuild)
  • ensure that switching between two different layouts correctly disposes of the resources (functions may reuse some previous state)
  • ensures that hot-reload works properly (using functions could break hot-reload for showDialogs & similar)
  • are integrated into the widget inspector.
    • We see ClassWidget in the widget-tree showed by the devtool, which helps understanding what is on screen
    • We can override debugFillProperties to print what the parameters passed to a widget are
  • better error messages
    If an exception happens (like ProviderNotFound), the framework will give you the name of the currently building widget. If you've split your widget tree only in functions + Builder, your errors won't have a helpful name
  • can define keys
  • can use the context API
  1. Functions:

Overall, it is considered a bad practice to use functions over classes for reusing widgets because of these reasons.
You can, but it may bite you in the future.

Centroid answered 10/11, 2018 at 0:3 Comment(5)
Comments are not for extended discussion; this conversation has been moved to chat.Reconsider
TLDR: Framework assumes Widgets are class-based and using functions prevents it from optimizing rebuilds and providing the debugging facilities such as Widget Inspector.Brinkley
It is not the case with managmentAlston
2 cents: you can re-use your widgets in other placesAnthracoid
Can you have a look at a Flutter navigation related question here : #75913731 ?Skein
A
26

I've been researching on this issue for the past 2 days. I came to the following conclusion: it is OKAY to break down pieces of the app into functions. It's just ideal that those functions return a StatelessWidget, so optimisations can be made, such as making the StatelessWidget const, so it doesn't rebuild if it doesn't have to. For example, this piece of code is perfectly valid:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            const MyWidgetClass(key: const Key('const')),
            MyWidgetClass(key: Key('non-const')),
            _buildSomeWidgets(_counter),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  Widget _buildSomeWidgets(int val) {
    print('${DateTime.now()} Rebuild _buildSomeWidgets');
    return const MyWidgetClass(key: Key('function'));

    // This is bad, because it would rebuild this every time
    // return Container(
    //   child: Text("hi"),
    // );
  }
}

class MyWidgetClass extends StatelessWidget {
  const MyWidgetClass({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('${DateTime.now()} Rebuild MyWidgetClass $key');

    return Container(
      child: Text("hi"),
    );
  }
}

The use of function there is perfectly fine, as it returns a const StatelessWidget. Please correct me if I'm wrong.

Attemper answered 28/4, 2019 at 10:40 Comment(10)
Can somebody explain why what I said is wrong? I mean, I suppose it's wrong given the downvotes.Attemper
I actually agree with you. I've been intending to write a much more detailed breakdown of the differences, but haven't gotten around to it. Feel free to flesh out your argument as I think it's important to understand the pros and cons of widgets v methods.Woadwaxen
@SergiuIacob Can we use const in front of the stateless class for every case? Or does it have to be certain cases? If yes, what are they?Grief
@Grief I don't think you can use const everywhere. For example, if you have a StatelessWidget class that returns a Text containing the value of a variable, and that variable changes somewhere, than your StatelessWidget should be rebuilt, so it can show that different value, therefore it can't be const. I think the safe way to put it is this: wherever you can, use const, if it is safe to do so.Attemper
I've been debating whether to answer this question myself. The accepted answer is plain wrong, but Rémi has done a lot to try and help the flutter community, so people probably don't scrutinize his answers as much as someone else's. That might be evident from all the upvotes. People just want their "single source of truth". :-)Hafner
That's what @Ixx was trying to say in the comments under Remi's answer. You can use functions for code readability, it's not a problem, as long as you return a ClassWidget instance with build(BuildContext context)Anthracoid
is there any preference on whether to use methods or classes when it comes to either Golden Image or Unit testing these bits of code? I would rather not make private methods public just because of allowing unit testability, but with class-way that comes sort of as a bonus? then again, not all parts of statefullWidget can be made into statelessWidgets, all the time....Aquavit
The problem has never been the function in itself. It is the act of trying to use functions to make something reusable.Cuirass
@RémiRousselet But beside your recent edit, thats not what your answer addresses. It talks about performance and nesting. If you wanted to talk reusability, then obviously a class is more appropriate. But using a helper function to instantiate a class is fine (as most commenters have established).Hafner
@DarkNeutron that is exactly what this topic is about. I used the word "reusable" in both the question and answer on purpose.Cuirass
R
17

As Remi has eloquently put repeatedly, it's not the functions by themselves that cause a problem, the problem is us thinking that using a function has a similar benefit to using a new widget.

Unfortunately this advice is evolving into "the act of merely using a function is inefficient", with often incorrect speculations into why this might be.

Using a function is almost the same as using what the function returns in place of that function. So, if you are calling a widget constructor and giving it as a child to another widget, you are not making your code inefficient by moving that constructor call into a function.

  //...
  child: SomeWidget(), 
  //...

is not significantly better in terms of efficiency than

  //...
  child: buildSomeWidget();
  //...

Widget buildSomeWidget() => SomeWidget(); 

It is fine to argue the following about the second one:

  • It's ugly
  • It's unnecessary
  • I don't like it
  • Function does not appear in Flutter Inspector
  • Two functions may not work with AnimatedSwitcher et al.
  • It does not create a new context, so you can't reach the Scaffold above it through context
  • If you use ChangeNotifier in it, its rebuild is not contained within the function

But it's not correct to argue this:

  • Using a function is inefficient in terms of performance

Creating a new widget brings these performance benefits:

  • ChangeNotifier within it does not make its parent rebuild upon changes
  • Sibling widgets are protected from each other's rebuilds
  • Creating it with const (if possible) protects it from parent's rebuilds
  • You are more likely to keep your const constructor if you can isolate the changing children to other widgets

However, if you do not have any of these cases, and your build function is looking more and more like pyramid of doom, it is better to refactor a part of it to a function rather than keeping the pyramid. Especially if you are enforcing 80 character limit, you may find yourself writing code in about 20 character-wide space. I see a lot of newbies falling into this trap. The message to those newbies should be "You should really be creating new widgets here. But if you can't, at least create a function.", not "You have to create a widget or else!". Which is why I think we have to be more specific when we promote widgets over functions and avoid being factually incorrect about efficiency.

For your convenience, I have refactored Remi's code to show that the problem is not simply using functions, but the problem is avoiding creating new widgets. So, if you were to place the widget-creating code in those functions into where the functions are called (refactor-inline) you have the exact same behavior as using functions, but without using functions! So, it's not using functions that's the problem, it's the avoidance of creating new widget classes.

(remember to turn off null safety as the original code is from 2018)

Here are a few interactive examples on Dartpad that you can run yourself to better understand the issues:

https://dartpad.dev/1870e726d7e04699bc8f9d78ba71da35 This example showcases how by splitting your app into functions, you may accidentally break things like AnimatedSwitcher

Non-function version: https://dartpad.dev/?id=ae5686f3f760e7a37b682039f546a784

https://dartpad.dev/a869b21a2ebd2466b876a5997c9cf3f1 This example showcases how classes allow more granular rebuilds of the widget tree, improving performances

Non-function version: https://dartpad.dev/?id=795f286791110e3abc1900e4dcd9150b

https://dartpad.dev/06842ae9e4b82fad917acb88da108eee This example showcases how, by using functions, you expose yourself to misusing BuildContext and facing bugs when using InheritedWidgets (such as Theme or providers)

Non-function version: https://dartpad.dev/?id=65f753b633f68503262d5adc22ea27c0

You will find that not having them in a function creates the exact same behavior. So it's adding widgets that gives you the win. It's not adding functions that creates a problem.

So the suggestions should be:

  • Avoid the pyramid of doom at any cost! You need horizontal space to code. Don't get stuck at the right margin.
  • Create functions if you need, but do not give parameters to them as it's impossible to find the line that calls the function through Flutter Inspector.
  • Consider creating new widget classes, it's the better way! Try Refactor->Extract Flutter Widget. You won't be able to if your code is too coupled with the current class. Next time you should plan better.
  • Try to comment out things that prevent you from extracting a new widget. Most likely they are function calls in the current class (setState, etc.). Extract your widget then, and find ways of adding that stuff in. Passing functions to the constructor may be ok (think onPressed). Using a state management system may be even better.

I hope this can help remind why we prefer widgets over functions and that simply using a function is not a huge problem.

Edit: one point that was missed in this whole discussion: when you widgetize, siblings don't rebuild each other anymore. This Dartpad demonstrates this: https://dartpad.dartlang.org/?id=8d9b6d5bd53a23b441c117cd95524892

Reachmedown answered 25/9, 2021 at 22:28 Comment(0)
A
14

1 - Most of the time build method (child widget) calls number of synchronous and asynchronous functions.

Ex:

  • To download network image
  • get input from users etc.

so build method need to keep in the separate class widget (because all other methods call by build() method can keep in one class)


2 - Using the widget class you can create a number of other classes without writing the same code again and again (** Use Of Inheritance** (extends)).

And also using inheritance(extend) and polymorphism (override) you can create your own custom class. (Down below example, In there I will customize (Override) the animation by extending MaterialPageRoute (because its default transition I want to customize).👇

class MyCustomRoute<T> extends MaterialPageRoute<T> {
  MyCustomRoute({ WidgetBuilder builder, RouteSettings settings })
      : super(builder: builder, settings: settings);

  @override                                      //Customize transition
  Widget buildTransitions(BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child) {
    if (settings.isInitialRoute)
      return child;
    // Fades between routes. (If you don't want any animation, 
    // just return child.)
    return new FadeTransition(opacity: animation, child: child);
  }
}

3 - Functions cannot add conditions for their parameters, But using the class widget's constructor You can do this.

Down below Code example👇 (this feature is heavily used by framework widgets)

const Scaffold({
    Key key,
    this.bottomNavigationBar,
    this.bottomSheet,
    this.backgroundColor,
    this.resizeToAvoidBottomPadding,
    this.resizeToAvoidBottomInset,
    this.primary = true,
    this.drawerDragStartBehavior = DragStartBehavior.start,
    this.extendBody = false,
    this.extendBodyBehindAppBar = false,
    this.drawerScrimColor,
    this.drawerEdgeDragWidth,
  }) : assert(primary != null),
       assert(extendBody != null),
       assert(extendBodyBehindAppBar != null),
       assert(drawerDragStartBehavior != null),
       super(key: key);

4 - Functions cannot use const and the Class widget can use the const for their constructors. (that affect the performance of the main thread)


5 - You can create any number of independent widgets using the same class (instances of a class/objects) But function cannot create independant widgets(instance), but reusing can.

[each instance has its own instance variable and that is completely independant from other widgets(object), But function's local variable is dependant on each function call* (which means, when you change a value of a local variable it affect for all other parts of the application which use this function)]


There were many Advantages in class over functions..(above are few use cases only)


My Final Thought

So don't use Functions as building blocks of your application, use them only for doing Operations. Otherwise, it causes many unchangeable problems when your application gets scalable.

  • Use functions for doing a small portion of the task
  • Use class as a building block of an application(Managing application)
Asthma answered 24/4, 2020 at 5:13 Comment(6)
Welcome to Stackoverflow! I'm not really sure what you're trying to express with your answer. You can use a function just fine for building widgets. shrinkHelper() { return const SizedBox.shrink(); } is the same as using const SizedBox.shrink() inline in your widget tree, and by using helper functions you can limit the amount of nesting in one place.Hafner
@Hafner Thanks for sharing. I will try to use helper functions.Asthma
Many words but it is offtopic speech.. What benefits of using stateless widget over builder function? Except ideological.Piloting
@NickolaySavchenko Rémi Rousselet's updated answer show benefits of using stateless widget over builder function. Look his answer's final part (Conclusion section)Asthma
@TDM I don't see any reason to use widgets over functions. If I need to build simple block on the screen, that not reused in other places.Piloting
if this block is really simple, why then extract it from build method? like, everything could be mostly done with ternary and collection operators nowadays, and code will still be real neat. the only reason to use helper functions is when they rebuild somewhat large part of subtree, but then using class will be better.Litt
H
4

When you are calling the Flutter widget make sure you use the const keyword. For example const MyListWidget();

Harv answered 14/12, 2018 at 20:56 Comment(0)
H
3

In case this helps anyone passing this way, some things I have in my conceptual model of Flutter developed from this question and working with Flutter in general (caveat: I could still be deeply confused and wrong about this stuff).

A Widget is what you want and the Elements are what you have. It is the job of the rendering engine to reconcile the two as efficiently as possible.

Use Keys, they can help a lot.

A BuildContext is an Element.

Any Thing.of(context) is likely to introduce a build dependency. If Thing changes it will trigger a rebuild from the context element.

In your build() if you access a BuildContext from a nested widget you are acting on the Element at the top of your subtree.

Widget build(BuildContext rootElement) {
  return Container(
    child:Container(
      child:Container(
        child:Text(
          "Depends on rootElement", 
          // This introduces a build trigger
          // If ThemeData changes a rebuild is triggered
          // on rootElement not this Text()-induced element
          style:Theme.of(rootElement).textTheme.caption,
        ),
      ),
    ),
  );
}

AnimatedSwitcher is a slippery beast - it has to be able to distinguish its children. You can use functions if they return different types or return the same type but with different Keys

If you are authoring a Widget use a class not a Function but feel free to refactor your 1000 line build() method with functions/methods, the outcome is identical*.

* but could be even better to refactor into classes

Hippocampus answered 20/8, 2022 at 11:3 Comment(0)
W
0

Sometimes, functions are the only way possible.

In my case I have, for example, button components that need to read several values for button behavior, and set several others, and call setState to rebuild the containing widget.

While this may sound horrible to a purist, for self-contained widgets, I prefer this dirty and efficient code over bending backwards to handle inter-widget communications.

With functions I get readable code, keep the build method visually smaller, and not have my code go too far right.

Wellesz answered 17/11, 2023 at 4:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.