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

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?


Solution 1:

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:

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

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

  • 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)

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:
  • have less code (which can be solved using code-generation functional_widget)

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.

Solution 2:

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.

Solution 3:

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)