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

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

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

47238 次浏览

编辑:颤振团队现在已经对此事采取了官方立场,并表示类是可取的。看到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

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

为什么这很重要

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

不能保证你通过使用函数有bug,但是通过使用类,你保证就不会面临这些问题。

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

结论

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

  1. 类:
  • 允许性能优化(const构造函数,更细粒度的重建)
  • 确保在两种不同布局之间的切换正确地处理了资源(函数可能会重用以前的一些状态)
  • 确保热重新加载正常工作(使用函数可能会破坏showDialogs &的热重新加载;类似)
  • 集成到小部件检查器中。
      我们在devtool显示的小部件树中看到ClassWidget 帮助理解屏幕上的内容
    • 我们可以重写debugFillProperties来打印传递给小部件的参数是什么
  • 更好的错误消息
    . 如果发生异常(如ProviderNotFound),框架将为您提供当前构建小部件的名称。 如果你把小部件树分割成函数+ Builder,你的错误不会有一个有用的名字
  • 可以定义键
  • 可以使用上下文API吗
  1. 功能:
总的来说,由于这些原因,使用函数而不是类来重用小部件被认为是一种糟糕的实践。
可以,但它可能在未来咬你

当您调用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"),
);
}
}

函数的使用是完全正确的,因为它返回const StatelessWidget。如果我说错了,请指正。

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

函数不能为它们的参数添加条件,但是使用类小部件的构造函数你可以这样做。

下面是代码示例👇(该特性被框架小部件大量使用)

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

函数不能使用const, Class小部件可以使用const作为它们的构造函数。 (这会影响主线程的性能)


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

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


类比函数有很多优点。(以上只是一些用例)


我最后的想法

所以不要使用函数作为应用程序的构建块,只使用它们来做操作。 否则,当应用程序获得可伸缩的时,它会导致许多不可更改的问题
  • 使用函数来完成任务的一小部分
  • 使用类作为应用程序的构建块(管理应用程序)

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

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

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

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

在效率方面是否明显优于

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


Widget buildSomeWidget() => SomeWidget();

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

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

但这样说是不对的:

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

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

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

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

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

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

这里有一些关于达特帕德的互动例子,你可以运行

https://dartpad.dev/1870e726d7e04699bc8f9d78ba71da35此示例 展示了如何通过将你的应用程序分成功能,你可以 不小心破坏像AnimatedSwitcher

非函数版本:https://dartpad.dev/?id=ae5686f3f760e7a37b682039f546a784

https://dartpad.dev/a869b21a2ebd2466b876a5997c9cf3f1此示例 展示了类如何允许更细粒度的小部件树重建, 改善表演< / p >

非函数版本:https://dartpad.dev/?id=795f286791110e3abc1900e4dcd9150b

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

非函数版本:https://dartpad.dev/?id=65f753b633f68503262d5adc22ea27c0

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

所以建议应该是:

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

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

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

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

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

使用__abc,它们会有很大帮助。

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

如果你正在创建一个Widget,使用class而不是Function,但请随意用函数/方法重构你的1000行build()方法,结果是相同的*。

* 但是重构到类中会更好

功能:

函数用于创建可重用的小部件,方法是编写一个接受一些参数并返回小部件的函数。这允许您快速创建具有不同属性的小部件。

例子:

/// Function to create a reusable widget
Widget createReusableWidget(String title, Color color) {
return Container(
width: double.infinity,
height: 50.0,
color: color,
child: Text(title),
);
}


// Usage
Widget widget1 = createReusableWidget('Widget 1', Colors.red);
Widget widget2 = createReusableWidget('Widget 2', Colors.green);


类:

类用于创建可重用的小部件,方法是创建扩展基本小部件的类,如StatelessWidget或StatefulWidget。这允许您创建具有不同属性的小部件,也可以添加自定义行为

例子:


/// Class to create a reusable widget
class ReusableWidget extends StatelessWidget {
final String title;
final Color color;


const ReusableWidget({ this.title, this.color });


@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 50.0,
color: color,
child: Text(title),
);
}
}


/// Usage
Widget widget1 = ReusableWidget(title: 'Widget 1', color: Colors.red);
Widget widget2 = ReusableWidget(title: 'Widget 2', color: Colors.green);


差异:

函数是快速创建小部件而类允许您使用自定义行为创建更复杂的小部件的简单方法。此外,类的可扩展性更强,允许您创建修改小部件行为的子类。