创建可重用小部件的函数和 类 有什么区别?

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

我意识到可以使用普通函数而不是 subclassing StatelessWidget 创建小部件。一个例子是这样的:

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

这很有趣,因为它比成熟的 class 需要 的代码。示例:

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

所以我一直在想:函数和 classes 在创建小部件时除了语法之外还有什么区别吗?使用函数是一种好习惯吗?

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


TL;DR:更喜欢使用 classes 而不是函数来使 可重用 小部件树。

编辑:为了弥补一些误解: 这不是关于导致问题的功能,而是 class 解决一些问题。

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

同样,它主要针对 public 小部件,可以重复使用。对于只使用一次的私有函数来说,这并不重要——尽管意识到这种行为仍然很好。


使用函数代替classes有一个重要的区别,那就是:框架不知道函数,但是可以看到classes.

考虑以下“小部件”功能:

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

这样使用:

functionWidget(
  child: functionWidget(),
);

它 class 等价于:

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

在理论上,两者似乎做的是完全相同的事情:创建 2 Container,其中一个嵌套在另一个中。但实际情况略有不同。

对于函数,生成的小部件树如下所示:

Container
  Container

使用 classes 时,小部件树是:

ClassWidget
  Container
    ClassWidget
      Container

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

为什么这很重要

通过使用函数将您的小部件树拆分为多个小部件,您将自己暴露在错误中并错过了一些性能优化。

不能保证您使用函数有错误,但是使用classes,您保证 不去面对这些问题。

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

结论

这是使用函数和 classes 之间的差异的精选列表:

  1. 类:
  • 允许性能优化(const 构造函数,更细粒度的重建)
  • 确保在两种不同的布局之间切换能够正确处理资源(函数可能会重用一些以前的状态)
  • 确保热重载正常工作(使用函数可能会破坏 showDialogs 和类似的热重载)
  • 已集成到小部件检查器中。
    • 我们在 devtool 显示的 widget-tree 中看到 ClassWidget, 有助于理解屏幕上的内容
    • 我们可以覆盖 debugFillProperties 来打印传递给小部件的参数是什么
  • 更好的错误消息
    如果发生异常(如 ProviderNotFound),框架将为您提供当前正在构建的小部件的名称。 如果您仅在函数 + Builder 中拆分小部件树,您的错误将没有有用的名称
  • 可以定义键
  • 可以使用上下文API
  1. 函数:

总的来说,由于这些原因,使用超过 classes 的函数来重用小部件被认为是一种不好的做法。
可以,但它以后可能会咬你。

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

我这两天一直在研究这个问题。我得出以下结论:将应用程序的各个部分分解为功能是可以的。那些函数 return 和 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"),
    );
  }
}

在那里使用函数非常好,因为它 return 是 const StatelessWidget。如有错误请指正

1 - 大部分时间构建方法(子小部件)调用 个同步和异步函数。

例如:

  • 下载网络图片
  • 从用户那里获取输入等

所以 build 方法需要保存在单独的 class 小部件中(因为 build() 方法调用的所有其他方法都可以保存在一个 class 中)


2 - 使用小部件 class,您可以创建许多其他 classes,而无需一次又一次地编写相同的代码(** 使用继承**(扩展))。

并且还可以使用继承(扩展)和多态(覆盖)创建您自己的自定义 class。 (在下面的示例中,我将在其中通过扩展 MaterialPageRoute 来自定义(覆盖)动画(因为我想自定义其默认过渡)。

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 - 函数不能为其参数添加条件,但使用 class 小部件的构造函数可以做到这一点。

下面的代码示例(此功能被框架小部件大量使用)

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 - 函数不能使用 const,Class 小部件可以将 const 用于其构造函数。 (影响主线程的性能)


5 - 您可以使用相同的 class(class/objects 的实例)创建任意数量的独立小部件 但是函数不能创建独立的小部件(实例),但是重用可以。

[每个实例都有自己的实例变量并且完全独立于其他小部件(对象),但是函数的局部变量是取决于每个函数调用*(这意味着,当您更改局部变量的值时,它会影响使用该函数的应用程序的所有其他部分)]


class 相对于函数有很多优点..(以上仅是一些用例)


我最后的想法

因此,不要将函数用作应用程序的构建块,仅将它们用于执行操作。 否则,当您的应用程序变得 可扩展 .

时,它会导致许多无法更改的问题
  • 使用函数完成一小部分任务
  • 使用class作为应用程序的构建块(管理应用程序)

正如 Remi 雄辩地指​​出 ,导致问题的不是函数本身,而是我们认为使用函数与使用新小部件具有相似的好处。

不幸的是,这个建议正在演变为“仅仅使用函数是低效的行为”,并且经常错误地推测为什么会这样。

使用函数与使用函数 returns 代替该函数几乎相同。因此,如果您正在调用一个小部件构造函数并将其作为子项提供给另一个小部件,那么通过将该构造函数调用移至函数中并不会降低您的代码效率。

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

在效率方面并没有明显优于

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

Widget buildSomeWidget() => SomeWidget(); 

关于第二个争论如下:

  • 很丑
  • 没必要
  • 我不喜欢
  • 函数没有出现在 Flutter Inspector 中
  • 两个函数可能不适用于 AnimatedSwitcher
  • 它不会创建新的上下文,因此您无法通过上下文
  • 到达它上面的Scaffold
  • 如果你在其中使用ChangeNotifier,它的重建不包含在函数中

但是这样争论是不正确的:

  • 使用函数在性能方面效率低下

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

  • ChangeNotifier在它里面不会让它的父级在改变时重建
  • 同级小部件相互之间的重建受到保护
  • 使用 const 创建它(如果可能)保护它免受父级的重建
  • 如果您可以将不断变化的子项与其他小部件隔离开来,您就更有可能保留 const 构造函数

然而,如果你没有任何这些情况,并且你的构建函数看起来越来越像pyramid of doom,最好将它的一部分重构为一个函数而不是保留金字塔.特别是如果您强制执行 80 个字符的限制,您可能会发现自己编写的代码大约有 20 个字符 space。我看到很多新手掉进这个陷阱。给那些新手的信息应该是“你真的应该在这里创建新的小部件。但如果你不能,至少创建一个函数。”,而不是“你必须创建一个小部件否则!”。这就是为什么我认为我们在推广小部件而不是功能时必须更加具体,并避免在效率方面出现事实错误。

为了您的方便,我重构了 以表明问题不只是使用函数,而是避免创建新的小部件。因此,如果您将这些函数中的小部件创建代码放置到调用函数的位置(重构内联),您将获得与使用函数完全相同的行为,但不使用函数!所以,问题不在于使用函数,而是避免创建新的小部件 classes.

(记得关闭空安全,因为原始代码来自 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

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

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

非功能版本: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)

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

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

所以建议应该是:

  • 不惜一切代价避开厄运金字塔!您需要水平 space 才能编码。不要卡在右边距。
  • 根据需要创建函数,但不要给它们提供参数,因为不可能通过 Flutter Inspector 找到调用该函数的行。
  • 考虑创建新的小部件 class是的,这是更好的方法!尝试重构-> 提取 Flutter 小部件。如果您的代码与当前的 class 过于耦合,您将无法做到这一点。下次你应该好好计划一下。
  • 尝试注释掉阻止您提取新小部件的内容。它们很可能是当前 class(setState 等)中的函数调用。然后提取您的小部件,并找到添加这些东西的方法。将函数传递给构造函数可能没问题(想想 onPressed)。使用状态管理系统可能会更好。

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

编辑: 整个讨论中遗漏了一点:当您使用 widgetize 时,兄弟姐妹不再相互重建。这个 Dartpad 演示了这一点:https://dartpad.dartlang.org/?id=8d9b6d5bd53a23b441c117cd95524892