创建小部件的函数和类之间有什么区别?

Rém*_*let 70 dart flutter

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

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

这很有趣,因为它需要的代码远远少于完整的类.例:

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
      );
  }
}
Run Code Online (Sandbox Code Playgroud)

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

Rém*_*let 96

TL; DR:永远不要使用类上的函数来制作可重用的小部件树.始终将这些提取到StatelessWidget中.


使用函数而不是类之间存在巨大差异,即:框架不知道函数,但可以看到类.

考虑以下"小部件"功能:

Widget functionWidget({ Widget child}) {
  return Container(child: child);
}
Run Code Online (Sandbox Code Playgroud)

用这种方式:

functionWidget(
  child: functionWidget(),
);
Run Code Online (Sandbox Code Playgroud)

它等同于类:

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,
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

像那样使用:

new ClassWidget(
  child: new ClassWidget(),
);
Run Code Online (Sandbox Code Playgroud)

在纸面上,两者似乎完全相同:创建2 Container,其中一个嵌套到另一个中.但现实略有不同.

在函数的情况下,生成的小部件树如下所示:

Container
  Container
Run Code Online (Sandbox Code Playgroud)

使用类时,窗口小部件树是:

ClassWidget
  Container
    ClassWidget
      Container
Run Code Online (Sandbox Code Playgroud)

非常重要,因为它从根本上改变了更新窗口小部件时框架的行为方式.以下是差异的精选列表:

  1. 类别:

    • 允许性能优化(const构造函数,operator == override,更精细的重建)
    • 有热重装
    • 被集成到小部件检查器(debugFillProperties)
    • 可以定义键
    • 可以使用上下文API
    • 确保所有小部件以相同的方式使用(始终是构造函数)
    • 确保在两个不同布局之间切换正确处理资源(函数可以重用某些先前的状态)
  2. 功能:

    • 拥有更少的代码(甚至在那里,我创建了一个代码生成器,使类像函数一样小:functional_widget)

结论应该很清楚:

不要使用函数来创建小部件.

  • 我建议您再看一遍。不用理会这些功能,而将注意力集中在每种情况下要创建的实例上。您将看到功能的使用是完全不相关的。 (5认同)
  • 您可以从“永远不要在类上使用函数来创建可重用的窗口小部件树”开始回答。这是错误的二分法。这是关于是否将小部件嵌套在StatelessWidget子类中。功能的使用完全无关。 (3认同)
  • @ sgon00我认为对于来自React的人(我怀疑其他人对此感到困惑),可以总结为Flutter中没有函数组件之类的东西。在Flutter中,组件始终是类。并且(就像在任何其他框架或lang中一样),您可以将函数用作帮助程序来创建类。因此,“应该使用函数或类”这个问题在Flutter中并没有什么意义。仅在React中,函数实际上直接用作组件。 (2认同)
  • TLDR:框架假设 Widget 是基于类的,并且使用函数会阻止它优化重建并提供调试工具(例如 Widget Inspector)。 (2认同)

Gaz*_*kus 17

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

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

使用函数几乎与使用函数返回值代替该函数相同。因此,如果您调用一个小部件构造函数并将其作为另一个小部件的子项,那么将该构造函数调用移至函数中并不会导致代码效率低下。

  //...
  child: SomeWidget(), 
  //...
Run Code Online (Sandbox Code Playgroud)

就效率而言并没有明显优于

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

Widget buildSomeWidget() => SomeWidget(); 
Run Code Online (Sandbox Code Playgroud)

关于第二个问题可以争论以下几点:

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

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

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

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

  • ChangeNotifier在它内部不会使其父级在更改时重建
  • 兄弟小部件受到保护,免受彼此重建的影响
  • 创建它const(如果可能的话)可以保护它免受父级重建的影响
  • const如果您可以将不断变化的子部件与其他小部件隔离,那么您更有可能保留构造函数

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

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

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

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

https://dartpad.dev/1870e726d7e04699bc8f9d78ba71da35此示例展示了如何通过将应用程序拆分为函数,您可能会意外地破坏 AnimatedSwitcher 等内容

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

https://dartpad.dev/a869b21a2ebd2466b876a5997c9cf3f1此示例展示了类如何允许更精细地重建小部件树,从而提高性能

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

https://dartpad.dev/06842ae9e4b82fad917acb88da108eee此示例展示了如何通过使用函数,使自己在使用 InheritedWidget(例如主题或提供程序)时面临滥用 BuildContext 和面临错误的风险

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

您会发现,如果函数中没有它们,则会产生完全相同的行为。因此,添加小部件可以为您带来胜利。添加功能不会产生问题。

所以建议应该是:

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

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

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


小智 16

过去两天我一直在研究这个问题。我得出以下结论:将应用程序的各个部分分解为功能是可以的。这些函数返回 a 是理想的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"),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

在那里使用函数非常好,因为它返回一个const StatelessWidget. 如果我错了,请纠正我。

  • 我一直在犹豫要不要自己回答这个问题。公认的答案显然是错误的,但 Rémi 做了很多努力来尝试帮助 flutter 社区,因此人们可能不会像其他人那样仔细审查他的答案。从所有的赞成票中可以明显看出这一点。人们只想要他们的“单一真相来源”。:-) (4认同)
  • @DarkNeutron 这正是本主题的主题。我在问题和答案中都故意使用了“可重用”这个词。 (3认同)
  • 问题从来都不是功能本身。这是尝试使用函数使某些东西**可重用**的行为。 (2认同)

Dil*_*han 8

函数的作用和类的作用之间存在很大差异。


让我从头开始解释它。(仅关于命令式)

  • 编程历史,我们都知道从直接的基本命令开始(例如-:Assembly)。

  • Next 结构化编程带有流程控制(例如:if、switch、while、for 等)这种范式使程序员可以有效地控制程序流程,并通过循环最大限度地减少代码行数。

  • 下一个过程编程出现了,它将指令分组到过程(函数)中。这给程序员带来了两大好处。

1.Group 语句(操作)到单独的块中。

2.可以重用这些块。(功能)

但最重要的是,范式并没有给出管理应用程序的解决方案。过程式编程也只能用于小规模的应用程序。不能用于开发大型 Web 应用程序(例如:银行、谷歌、youtube、facebook、stackoverflow 等),不能创建像 android sdk、flutter sdk 等框架......

所以工程师们做了更多的研究来正确地管理程序。

  • 最后,面向对象编程提供了管理任何规模应用程序的所有解决方案。(从 hello world 到使用系统创建的万亿人,例如谷歌、亚马逊,以及今天 90% 的应用程序)。

  • 在oop中,所有应用程序都是围绕对象构建的。这意味着应用程序是这些对象的集合。

所以对象是任何应用程序的基本构建。

类(运行时的对象)对与这些变量(数据)相关的数据和函数进行分组。所以对象组合数据及其相关操作。

[这里我不打算解释 oop ]


好的现在让我们来看看颤振框架。

-Dart 支持过程式和 oop 但是,Flutter 框架完全使用类(oop)构建。(因为大型可管理框架无法使用程序创建)

在这里,我将创建他们使用类而不是函数来制作小部件的原因列表。


1 - 大多数时候构建方法(子小部件)调用同步和异步函数的数量。

前任:

  • 下载网络镜像
  • 从用户等获取输入

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


2 - 使用小部件类,您可以创建另一个类的数量,而无需一次又一次地编写相同的代码(** 使用继承**(扩展))。

并且还可以使用继承(扩展)和多态(覆盖)创建自己的自定义类。(在下面的示例中,我将通过扩展 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);
  }
}
Run Code Online (Sandbox Code Playgroud)

3 - 函数不能为其参数添加条件,但是使用类小部件的构造函数可以做到这一点。

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

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);
Run Code Online (Sandbox Code Playgroud)

4 - 函数不能使用 const,而类小部件可以使用 const 作为其构造函数。(影响主线程的性能)


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

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


类比函数有很多优点..(以上仅是少数用例)


我最后的想法

因此,不要将函数用作应用程序的构建块,而仅将它们用于执行操作。否则,当您的应用程序变得可扩展时,它会导致许多无法处理的问题。

  • 使用函数来完成一小部分任务
  • 使用类作为应用程序的构建块(管理应用程序)

谢谢阅读

  • 很多话,但这是题外话。使用无状态小部件相对于构建器功能有什么好处?除了意识形态。 (3认同)