我已经意识到可以使用普通函数创建小部件,而不是继承StatelessWidget的子类。一个例子是:

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

这很有趣,因为它需要的代码比成熟的类要少得多。例子:

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
      );
  }
}

所以我一直在想:在创建小部件时,除了语法之外,函数和类之间还有什么不同吗?使用函数是一种好的实践吗?


编辑:Flutter团队现在已经对此事采取了官方立场,并表示更可取的是课程。参见https://www.youtube.com/watch?v=IOyq-eTRhvo


TL;DR:更倾向于使用类而不是函数来创建可重用的小部件树。

编辑:为了弥补一些误解: 这不是关于函数引起的问题,而是类解决了一些问题。

如果一个函数可以做同样的事情,Flutter就不会有StatelessWidget。

类似地,它主要针对公共小部件,以供重用。对于只使用一次的私有函数来说,这并不重要——尽管意识到这种行为仍然很好。


使用函数而不是类之间有一个重要的区别:框架不知道函数,但可以看到类。

考虑下面的“widget”函数:

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

这样用:

functionWidget(
  child: functionWidget(),
);

它是类等价的:

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,
    );
  }
}

这样用:

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

从表面上看,两者似乎做的是完全相同的事情:创建两个容器,其中一个嵌套到另一个容器中。但现实情况略有不同。

在函数的情况下,生成的小部件树看起来像这样:

Container
  Container

当使用类时,小部件树是:

ClassWidget
  Container
    ClassWidget
      Container

这很重要,因为它改变了框架在更新小部件时的行为。

为什么这很重要

通过使用函数将小部件树分割为多个小部件,您可能会遇到bug,并错过一些性能优化。

不能保证使用函数就一定会有bug,但是使用类就可以保证不会遇到这些问题。

以下是Dartpad上的一些互动示例,您可以自己运行以更好地理解问题:

https://dartpad.dev/?id=bcae5878ccced764b35dd9a659a593db This example showcases how by splitting your app into functions, you may accidentally break things like AnimatedSwitcher https://dartpad.dev/?id=481a2c301c2e4bed6c30ba651d01bacb This example showcases how classes allow more granular rebuilds of the widget tree, improving performances https://dartpad.dev/?id=8bcb85ba535102bed652e5bf1540ac3b This example showcases how, by using functions, you expose yourself to misusing BuildContext and facing bugs when using InheritedWidgets (such as Theme or providers)

结论

下面是使用函数和类之间的区别:

类:

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

功能:

代码更少(可以使用代码生成functional_widget解决)

总的来说,由于这些原因,使用函数而不是类来重用小部件被认为是一种糟糕的实践。 你可以,但将来可能会被它咬一口。

当您调用Flutter小部件时,请确保使用const关键字。例如const MyListWidget();

过去两天我一直在研究这个问题。我得出了以下结论:将应用程序的各个部分分解成函数是可以的。这些函数返回一个StatelessWidget是最理想的,这样就可以进行优化,比如使StatelessWidget为const,这样它就不会在不必要的情况下重新构建。 例如,这段代码是完全有效的:

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"),
    );
  }
}

function的使用完全没问题,因为它返回一个const StatelessWidget。如果我说错了,请指正。

正如Remi反复雄辩地指出的那样,并不是函数本身造成了问题,问题在于我们认为使用函数与使用新的小部件具有类似的好处。

不幸的是,这一建议正在演变成“仅仅使用一个函数的行为是低效的”,对其原因的猜测往往是错误的。

使用函数几乎等同于使用函数返回的内容来代替该函数。因此,如果您正在调用一个小部件构造函数并将其作为子部件赋予另一个小部件,那么将构造函数调用移到函数中并不会使代码效率降低。

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

在效率方面是否明显优于

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

Widget buildSomeWidget() => SomeWidget(); 

关于第二个问题,可以提出以下论点:

它是丑陋的 这是不必要的 我不喜欢它 功能未出现在颤振检查器中 有两个函数可能无法与AnimatedSwitcher等一起工作。 它不会创建一个新的上下文,所以您不能通过上下文到达它上面的Scaffold 如果在其中使用ChangeNotifier,则其重建不会包含在函数中

但这样说是不对的:

就性能而言,使用函数是低效的

创建一个新的小部件可以带来以下性能优势:

其中的ChangeNotifier不会在更改时重新构建其父对象 兄弟小部件受到保护,不受彼此重建的影响 使用const创建它(如果可能的话)可以保护它不受父节点的重新构建 如果可以将不断变化的子部件隔离到其他小部件,则更有可能保留const构造函数

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.

为方便起见,我重构了Remi的代码,以表明问题不只是使用函数,而是避免创建新的小部件。因此,如果您将在这些函数中创建小部件的代码放到调用函数的地方(refactor-inline),您将获得与使用函数完全相同的行为,但不使用函数!因此,问题不在于使用函数,而在于避免创建新的小部件类。

(记得关闭空安全,因为原始代码是2018年的)

这里有一些达特帕德的互动例子,你可以运行 让自己更好地理解问题: https://dartpad.dev/1870e726d7e04699bc8f9d78ba71da35这个例子 展示了如何通过将你的应用程序分成功能,你可以 不小心弄坏像AnimatedSwitcher这样的东西

非功能版本:https://dartpad.dev/?id=ae5686f3f760e7a37b682039f546a784

https://dartpad.dev/a869b21a2ebd2466b876a5997c9cf3f1这个例子 展示了类如何允许更细粒度的小部件树重建, 改善性能

非功能版本:https://dartpad.dev/?id=795f286791110e3abc1900e4dcd9150b

https://dartpad.dev/06842ae9e4b82fad917acb88da108eee这个例子 展示了如何通过使用函数来暴露自己被滥用的风险 BuildContext和在使用InheritedWidgets(例如 主题或提供者)

非功能版本:https://dartpad.dev/?id=65f753b633f68503262d5adc22ea27c0

您会发现,在函数中不使用它们会产生完全相同的行为。所以添加小部件会让你获胜。并不是添加函数会产生问题。

所以建议应该是:

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.

我希望这可以帮助提醒我们为什么更喜欢小部件而不是函数,并且简单地使用函数并不是一个大问题。

编辑:在整个讨论中有一点被忽略了:当你小部件化时,兄弟姐妹不再相互重建。这个达特帕德证明了这一点:https://dartpad.dartlang.org/?id=8d9b6d5bd53a23b441c117cd95524892

为了帮助大家理解这个问题,在我的Flutter概念模型中有一些东西是由这个问题发展而来的,并与Flutter一起工作(警告:我可能仍然对这些东西深感困惑和错误)。

Widget是您想要的,元素是您所拥有的。渲染引擎的工作就是尽可能有效地调和这两者。

使用钥匙,它们可以帮助很多。

BuildContext是一个元素。

任何Thing.of(context)都可能引入构建依赖。如果Thing发生变化,它将触发从context元素重新构建。

在build()中,如果您从嵌套的小部件访问BuildContext,那么您正在对子树顶部的Element进行操作。

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是一个狡猾的野兽-它必须能够区分它的孩子。如果函数返回不同类型,或者返回相同类型但键不同,则可以使用函数

如果您正在编写一个小部件,请使用类而不是函数,但可以自由地使用函数/方法重构您的1000行build()方法,结果是相同的*。

*但是重构到类中会更好