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

fiei3ece  于 2022-12-16  发布在  其他
关注(0)|答案(7)|浏览(87)

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


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

bweufnob

bweufnob1#

编辑:Flutter团队现在已经对这个问题采取了官方立场,并声明类更可取。

TL;DR:更喜欢使用类而不是函数来制作可重用的widget-tree。

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

如果一个函数可以做同样的事情,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(),
);

从理论上看,两者似乎做着完全相同的事情:创建2个Container,其中一个嵌套到另一个中,但实际情况略有不同。
对于函数,生成的小部件树如下所示:

Container
  Container

使用类时,小部件树为:

ClassWidget
  Container
    ClassWidget
      Container

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

为什么这很重要

通过使用函数将小部件树拆分为多个小部件,您会暴露在bug中,并错过一些性能优化。
不能保证您使用函数会有bug,但是通过使用类,您保证不会面临这些问题。
下面是Dartpad上的几个交互式示例,您可以自己运行这些示例来更好地理解问题:

此示例展示了如何通过将应用拆分为函数,意外地破坏AnimatedSwitcher等功能

结论

下面列出了使用函数和类之间的区别:
1.类别:

  • 允许性能优化(const构造函数,更细粒度的重建)
  • 确保两个不同布局之间的切换正确处置资源(函数可能重用某些先前状态)
  • 确保热重新加载正常工作(使用函数可能会中断showDialogs和类似的热重新加载)
  • 集成到小部件检查器中。
  • 我们在devtool显示的widget-tree中看到ClassWidget,这有助于理解屏幕上显示的内容
  • 我们可以重写debugFillProperties来打印传递给小部件的参数
  • 更好的错误消息

如果发生异常(如ProviderNotFound),框架将给予当前构建的小部件的名称。如果只在函数+Builder中拆分小部件树,则错误不会有有用的名称

  • 可以定义键
  • 可以使用上下文API

1.职能:

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

u5i3ibmn

u5i3ibmn2#

这两天我一直在研究这个问题,我得出了以下结论:将应用程序的各个部分分解为函数是可以的。理想情况下,这些函数返回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。如果我说错了,请纠正我。

eh57zj3b

eh57zj3b3#

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

  • 下载网络映像
  • 从用户获得输入等。

因此构建方法需要保留在单独的类小部件中(因为通过build()方法调用的所有其他方法可以保留在一个类中)
2 -使用widget类,您可以创建许多其他类,而无需反复编写相同的代码(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,而类小部件可以使用const作为它们的构造函数。(这会影响主线程的性能)
5 -您可以使用同一个类(类/对象的示例)创建任意数量的独立小部件,但函数不能创建独立的小部件(示例),但重用可以。
[each示例有自己的 * 示例变量 *,完全独立于其他小部件(对象),但 * 函数的局部变量 * 依赖于每个函数调用 *(这意味着,当您更改局部变量的值时,它会影响使用此函数的应用程序的所有其他部分)]
类相对于函数有很多优点......(以上只是一些用例)
我最后的想法
因此,不要把函数作为应用程序的构建块,只把它们用于操作。否则,当你的应用程序变得可伸缩时,它会导致许多不可改变的问题。

  • 使用函数完成一小部分任务
  • 将类用作应用程序的构造块(管理应用程序)
6jjcrrmo

6jjcrrmo4#

正如Remi反复雄辩地指出的那样,问题并不是函数本身造成的,问题是我们认为使用函数与使用新的小部件有类似的好处。
不幸的是,这个建议正在演变成“仅仅使用函数的行为是低效的”,而对于为什么会这样,人们往往会做出不正确的猜测。
使用一个函数几乎等同于使用函数返回的值来代替该函数。因此,如果调用一个小部件构造函数并将其作为另一个小部件的子构造函数,那么将该构造函数调用移到函数中并不会使代码效率低下。

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

在效率方面并不比

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

Widget buildSomeWidget() => SomeWidget();

对第二个问题进行如下论证是可以的:

  • 真难看
  • 没这个必要
  • 我不喜欢
  • 功能未出现在Flutter检查器中
  • 有两个函数可能不适用于AnimatedSwitcher等。
  • 它不创建新的上下文,因此无法通过上下文到达它上面的Scaffold
  • 如果在其中使用ChangeNotifier,则其重新生成不包含在函数中

但这样说是不对的:

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

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

  • 其中的ChangeNotifier不会根据更改重新生成其父项
  • 保护同级微件不受彼此重建的影响
  • 使用const(如果可能)创建它可以防止父级的重建
  • 如果您可以将更改的子控件与其他小部件隔离开来,则更有可能保留const构造函数

然而,如果你没有这些情况,并且你的构建函数看起来越来越像pyramid of doom,那么最好将它的一部分重构为一个函数,而不是保留金字塔。你可能会发现自己在写代码时要占用大约20个字符的空间。我看到很多新手都落入了这个陷阱。给这些新手的信息应该是“你真的应该在这里创建新的小部件。但是如果你不能,至少创建一个函数。",而不是“你必须创建一个小部件或其他!"。这就是为什么我认为我们必须更具体地在我们促进小部件的功能,并避免在效率方面的事实错误。
为了方便起见,我对Remi's code进行了重构,以说明问题不在于简单地使用函数,而在于避免创建新的小部件。因此,如果您要将这些函数中的小部件创建代码放在调用这些函数的位置(重构内联)你有着和使用函数完全一样的行为,但是没有使用函数!所以,问题不在于使用函数,它避免了创建新的小部件类。
(记得关闭null safety,因为原始代码是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这个例子展示了如何通过使用函数,使自己暴露在滥用BuildContext和在使用InheritedWidget(例如主题或提供者)时面临bug的情况下
非功能版本:https://dartpad.dev/?id=65f753b633f68503262d5adc22ea27c0
你会发现在函数中没有小部件也会产生同样的行为,所以是添加小部件给你带来了好处,而不是添加函数带来了问题。
因此,建议应该是:

  • 不惜任何代价避免毁灭金字塔!你需要水平空间来编码。不要卡在右边距上。
  • 创建函数,如果你需要,但不要给予他们参数,因为它是不可能找到的行调用函数通过Flutter检查器。
  • 考虑创建新的小部件类,这是更好的方法!尝试重构-〉提取Flutter Widget。如果你的代码与当前类耦合太多,你将无法做到这一点。下次你应该计划得更好。
  • 试着注解掉那些阻止你提取一个新部件的东西。最有可能的是当前类中的函数调用(setState等)。然后提取你的部件,并找到添加这些东西的方法。将函数传递给构造函数可能是可以的(想想onPressed)。使用状态管理系统可能更好。

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

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

czfnxgou

czfnxgou5#

调用Flutter小部件时,请确保使用const关键字。

ioekq8ef

ioekq8ef6#

如果这有助于任何人通过这种方式,我在我的Flutter概念模型中的一些东西是从这个问题发展而来的,并与Flutter一般(警告:我仍然可能对这些东西感到深深的困惑和错误)。
一个Widget是你想要的,而Element是你拥有的,渲染引擎的工作就是尽可能有效地协调这两者。
使用Key s,他们可以帮助很大。
构建上下文一个元素。
任何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是一个狡猾的野兽--它必须能够区分它的子函数。如果函数返回不同的类型,或者返回相同的类型但Key不同,则可以使用函数
如果你正在编写一个Widget,使用class而不是Function,但是你可以随意地用函数/方法重构你的1000行build()方法,结果是相同的 *。

    • 但重构到类中会更好 *
1hdlvixo

1hdlvixo7#

功能:

通过编写一个接受一些参数并返回一个小部件的函数,函数可以用来创建可重用的小部件,这允许您快速创建具有不同属性的小部件。
示例:

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

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

相关问题