250

我已经意识到可以使用普通函数而不是继承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
      );
  }
}

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

4

5 回答 5

358

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


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

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

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

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


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

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

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

在纸面上,两者似乎都在做同样的事情:创建 2 Container,其中一个嵌套在另一个中。但实际情况略有不同。

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

Container
  Container

使用类时,小部件树是:

ClassWidget
  Container
    ClassWidget
      Container

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

为什么这很重要

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

无法保证使用函数出现错误,但使用类可以保证不会遇到这些问题。

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

结论

以下是使用函数和类之间差异的精选列表:

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

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

于 2018-11-10T00:03:56.500 回答
18

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

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

于 2019-04-28T10:40:13.850 回答
11

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

前任:

  • 下载网络映像
  • 从用户那里获取输入等。

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


2 - 使用小部件类,您可以创建许多其他类,而无需一次又一次地编写相同的代码(** Use Of Inheritance** (extends))。

并且还使用继承(扩展)和多态性(覆盖),您可以创建自己的自定义类。(在下面的示例中,我将通过扩展 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 - 函数不能为其参数添加条件,但使用类小部件的构造函数可以做到这一点。

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

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 - 您可以使用同一个类(类/对象的实例)创建任意数量的独立小部件,但函数不能创建独立的小部件(实例),但重用可以。

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


类比函数有很多优点..(以上只是几个用例)


我最后的想法

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

  • 使用函数来完成一小部分任务
  • 使用类作为应用程序的构建块(管理应用程序)
于 2020-04-24T05:13:36.377 回答
4

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

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

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

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

在效率方面并不比

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

Widget buildSomeWidget() => SomeWidget(); 

可以就第二个争论以下几点:

  • 它很丑
  • 这是不必要的
  • 我不喜欢
  • 功能未出现在 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此示例展示了如何通过使用函数,在使用 InheritedWidgets(例如主题或提供程序)时使自己暴露于滥用 BuildContext 和面临错误

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

您会发现在函数中没有它们会产生完全相同的行为。因此,它添加了可以让您获胜的小部件。不是添加功能会造成问题。

所以建议应该是:

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

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

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

于 2021-09-25T22:28:50.637 回答
3

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

于 2018-12-14T20:56:07.963 回答