¿Cuál es la diferencia entre funciones y clases para crear widgets reutilizables?

Resuelto Rémi Rousselet asked hace 5 años • 7 respuestas

Me di cuenta de que es posible crear widgets usando funciones simples en lugar de crear subclases de StatelessWidget . Un ejemplo sería este:

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

Esto es interesante porque requiere mucho menos código que una clase completa. Ejemplo:

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

Entonces me he estado preguntando: ¿Existe alguna diferencia además de la sintaxis entre funciones y clases para crear widgets? ¿Y es una buena práctica utilizar funciones?

Rémi Rousselet avatar Nov 10 '18 07:11 Rémi Rousselet
Aceptado

Editar : El equipo de Flutter ahora ha adoptado una postura oficial al respecto y ha declarado que las clases son preferibles. Ver https://www.youtube.com/watch?v=IOyq-eTRhvo


TL;DR: Prefiero usar clases en lugar de funciones para crear un árbol de widgets reutilizable .

EDITAR : Para compensar algunos malentendidos: no se trata de funciones que causan problemas, sino de clases que los resuelven.

Flutter no tendría StatelessWidget si una función pudiera hacer lo mismo.

Del mismo modo, está dirigido principalmente a widgets públicos, creados para ser reutilizados. No importa tanto para las funciones privadas diseñadas para usarse solo una vez, aunque sigue siendo bueno ser consciente de este comportamiento.


Existe una diferencia importante entre usar funciones en lugar de clases, es decir: el marco desconoce las funciones, pero puede ver las clases.

Considere la siguiente función de "widget":

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

usado de esta manera:

functionWidget(
  child: functionWidget(),
);

Y es su clase equivalente:

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

usado así:

new ClassWidget(
  child: new ClassWidget(),
);

Sobre el papel, ambos parecen hacer exactamente lo mismo: crear 2 Container, con uno anidado en el otro. Pero la realidad es ligeramente diferente.

En el caso de las funciones, el árbol de widgets generado tiene este aspecto:

Container
  Container

Mientras que con las clases, el árbol de widgets es:

ClassWidget
  Container
    ClassWidget
      Container

Esto es importante porque cambia el comportamiento del marco al actualizar un widget.

¿Por qué eso importa?

Al utilizar funciones para dividir su árbol de widgets en múltiples widgets, se expone a errores y pierde algunas optimizaciones de rendimiento.

No hay garantía de que tendrá errores al usar funciones, pero al usar clases, tiene la garantía de no enfrentar estos problemas.

Aquí hay algunos ejemplos interactivos en Dartpad que puede ejecutar usted mismo para comprender mejor los problemas:

  • https://dartpad.dev/?id=bcae5878ccced764b35dd9a659a593db
    Este ejemplo muestra cómo al dividir tu aplicación en funciones, puedes romper accidentalmente cosas comoAnimatedSwitcher

  • https://dartpad.dev/?id=481a2c301c2e4bed6c30ba651d01bacb Este ejemplo muestra cómo las clases permiten reconstrucciones más granulares del árbol de widgets, mejorando el rendimiento.

  • https://dartpad.dev/?id=8bcb85ba535102bed652e5bf1540ac3b Este ejemplo muestra cómo, al usar funciones, te expones a hacer un mal uso de BuildContext y a enfrentar errores al usar InheritedWidgets (como temas o proveedores)

Conclusión

Aquí hay una lista seleccionada de las diferencias entre el uso de funciones y clases:

  1. Clases:
  • permitir la optimización del rendimiento (constructor constante, reconstrucción más granular)
  • asegúrese de que al cambiar entre dos diseños diferentes se eliminen correctamente los recursos (las funciones pueden reutilizar algún estado anterior)
  • garantiza que la recarga en caliente funcione correctamente (el uso de funciones podría interrumpir la recarga en caliente showDialogsy similares)
  • están integrados en el inspector de widgets.
    • Vemos ClassWidgeten el árbol de widgets mostrado por devtool, que ayuda a comprender lo que hay en pantalla.
    • Podemos anular debugFillProperties para imprimir cuáles son los parámetros pasados ​​a un widget.
  • mejores mensajes de error
    Si ocurre una excepción (como ProviderNotFound), el marco le dará el nombre del widget que se está construyendo actualmente. Si ha dividido su árbol de widgets solo en funciones + Builder, sus errores no tendrán un nombre útil
  • puede definir claves
  • puede utilizar la API de contexto
  1. Funciones:
  • tener menos código (que se puede resolver usando el widget_funcional de generación de código )

En general, se considera una mala práctica utilizar funciones en lugar de clases para reutilizar widgets por estos motivos.
Puedes , pero puede que te afecte en el futuro .

Rémi Rousselet avatar Nov 10 '2018 00:11 Rémi Rousselet

He estado investigando sobre este tema durante los últimos 2 días. Llegué a la siguiente conclusión: está BIEN dividir partes de la aplicación en funciones. Es simplemente ideal que esas funciones devuelvan un StatelessWidget, para que se puedan realizar optimizaciones, como hacer el StatelessWidget const, para que no se reconstruya si no es necesario. Por ejemplo, este fragmento de código es perfectamente válido:

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

El uso de la función allí está perfectamente bien, ya que devuelve un archivo const StatelessWidget. Por favor corrígeme si estoy equivocado.

Sergiu Iacob avatar Apr 28 '2019 10:04 Sergiu Iacob

Como Remi ha dicho elocuentemente en repetidas ocasiones , no son las funciones por sí mismas las que causan un problema, el problema es que pensamos que usar una función tiene un beneficio similar al uso de un nuevo widget.

Desafortunadamente, este consejo está evolucionando hacia "el acto de simplemente usar una función es ineficiente", con especulaciones a menudo incorrectas sobre por qué podría ser así.

Usar una función es casi lo mismo que usar lo que devuelve la función en lugar de esa función. Entonces, si llama a un constructor de widget y se lo da como hijo a otro widget, no está haciendo que su código sea ineficiente al mover esa llamada al constructor a una función.

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

no es significativamente mejor en términos de eficiencia que

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

Widget buildSomeWidget() => SomeWidget(); 

Está bien argumentar lo siguiente sobre el segundo:

  • es feo
  • es innecesario
  • no me gusta
  • La función no aparece en Flutter Inspector
  • Es posible que dos funciones no funcionen con AnimatedSwitcheret al.
  • No crea un nuevo contexto, por lo que no puede llegar a lo Scaffoldanterior a través del contexto.
  • ChangeNotifierSi lo usa , su reconstrucción no está contenida dentro de la función

Pero no es correcto argumentar esto:

  • Usar una función es ineficiente en términos de rendimiento

La creación de un nuevo widget aporta estos beneficios de rendimiento:

  • ChangeNotifierdentro de él no hace que su padre se reconstruya tras los cambios
  • Los widgets hermanos están protegidos de las reconstrucciones de cada uno
  • Crearlo con const(si es posible) lo protege de las reconstrucciones de los padres.
  • Es más probable que conserve su constconstructor si puede aislar los elementos secundarios cambiantes de otros widgets.

Sin embargo, si no tiene ninguno de estos casos y su función de compilación se parece cada vez más a Pyramid of Doom , es mejor refactorizar una parte de ella en una función en lugar de mantener la pirámide. Especialmente si aplica un límite de 80 caracteres, es posible que se encuentre escribiendo código en un espacio de aproximadamente 20 caracteres. Veo que muchos novatos caen en esta trampa. El mensaje para esos novatos debería ser "Realmente deberías crear nuevos widgets aquí. Pero si no puedes, al menos crea una función", no "¡Tienes que crear un widget o si no!". Es por eso que creo que tenemos que ser más específicos cuando promocionamos los widgets sobre las funciones y evitar cometer errores en cuanto a la eficiencia.

Para su comodidad, he refactorizado el código de Remi para mostrar que el problema no es simplemente usar funciones, sino que el problema es evitar crear nuevos widgets. Entonces, si colocara el código de creación de widgets en esas funciones donde se llaman las funciones (refactor-inline), tendrá exactamente el mismo comportamiento que usar funciones, ¡pero sin usar funciones! Entonces, el problema no es el uso de funciones, sino evitar la creación de nuevas clases de widgets.

(recuerde desactivar la seguridad nula ya que el código original es de 2018)

Aquí hay algunos ejemplos interactivos en Dartpad que puede ejecutar usted mismo para comprender mejor los problemas:

https://dartpad.dev/1870e726d7e04699bc8f9d78ba71da35 Este ejemplo muestra cómo al dividir tu aplicación en funciones, puedes romper accidentalmente cosas como AnimatedSwitcher

Versión sin función: https://dartpad.dev/?id=ae5686f3f760e7a37b682039f546a784

https://dartpad.dev/a869b21a2ebd2466b876a5997c9cf3f1 Este ejemplo muestra cómo las clases permiten reconstrucciones más granulares del árbol de widgets, mejorando el rendimiento.

Versión sin función: https://dartpad.dev/?id=795f286791110e3abc1900e4dcd9150b

https://dartpad.dev/06842ae9e4b82fad917acb88da108eee Este ejemplo muestra cómo, al usar funciones, te expones a un mal uso de BuildContext y a errores al usar InheritedWidgets (como temas o proveedores)

Versión sin función: https://dartpad.dev/?id=65f753b633f68503262d5adc22ea27c0

Descubrirá que no tenerlos en una función crea exactamente el mismo comportamiento. Entonces, agregar widgets es lo que te da la victoria. No es agregar funciones lo que crea un problema.

Entonces las sugerencias deberían ser:

  • ¡Evita la pirámide de la perdición a cualquier precio! Necesitas espacio horizontal para codificar. No te quedes estancado en el margen correcto.
  • Cree funciones si lo necesita, pero no les dé parámetros ya que es imposible encontrar la línea que llama a la función a través de Flutter Inspector.
  • Considere la posibilidad de crear nuevas clases de widgets, ¡es la mejor manera! Pruebe Refactor->Extraer widget Flutter. No podrá hacerlo si su código está demasiado acoplado con la clase actual. La próxima vez deberías planificar mejor.
  • Intente comentar las cosas que le impiden extraer un nuevo widget. Lo más probable es que sean llamadas a funciones en la clase actual ( setState, etc.). Luego, extraiga su widget y encuentre formas de agregar esas cosas. Pasar funciones al constructor puede estar bien (piense en OnPressed). Usar un sistema de gestión estatal puede ser incluso mejor.

Espero que esto pueda ayudar a recordar por qué preferimos los widgets a las funciones y que simplemente usar una función no es un gran problema.

Editar: un punto que se pasó por alto en toda esta discusión: cuando creas un widget, los hermanos ya no se reconstruyen entre sí. Este Dartpad demuestra esto: https://dartpad.dartlang.org/?id=8d9b6d5bd53a23b441c117cd95524892

Gazihan Alankus avatar Sep 25 '2021 22:09 Gazihan Alankus

1: la mayoría de las veces, el método de compilación (widget secundario) llama a varias funciones sincrónicas y asincrónicas.

Ex:

  • Para descargar la imagen de red
  • obtener información de los usuarios, etc.

por lo tanto, el método de compilación debe mantenerse en el widget de clase separado (porque todos los demás métodos llamados por el método build() pueden mantenerse en una clase)


2 - Usando la clase de widget puedes crear otras clases sin escribir el mismo código una y otra vez (** Uso de herencia ** (se extiende)).

Y también usando herencia (extender) y polimorfismo (anular) puede crear su propia clase personalizada. (A continuación, en el ejemplo, personalizaré (anularé) la animación extendiendo MaterialPageRoute (porque quiero personalizar su transición predeterminada).

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 - Las funciones no pueden agregar condiciones para sus parámetros, pero usando el constructor del widget de clase puedes hacerlo.

A continuación, ejemplo de código👇 (esta característica es muy utilizada por los widgets del marco)

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: las funciones no pueden usar const y el widget de clase puede usar const para sus constructores. (que afectan el rendimiento del hilo principal)


5 - Puede crear cualquier número de widgets independientes usando la misma clase (instancias de una clase/objetos) Pero la función no puede crear widgets independientes (instancia), pero la reutilización sí puede.

[Cada instancia tiene su propia variable de instancia y es completamente independiente de otros widgets (objeto), pero la variable local de la función depende de cada llamada a la función* (lo que significa que cuando cambias el valor de una variable local, afecta a todas las demás partes). de la aplicación que utiliza esta función)]


Había muchas ventajas en la clase sobre las funciones... (arriba solo se muestran algunos casos de uso)


Mi pensamiento final

Por lo tanto, no utilice funciones como componentes básicos de su aplicación, úselas sólo para realizar operaciones. De lo contrario, causará muchos problemas que no se pueden modificar cuando su aplicación se vuelva escalable .

  • Utilice funciones para realizar una pequeña parte de la tarea.
  • Utilice la clase como componente básico de una aplicación (Administración de aplicaciones)
dilshan avatar Apr 24 '2020 05:04 dilshan