Me gustaría crear un widget de BaseScreen como este para reutilizar en mi aplicación:

class BaseScreen extends StatelessWidget {
  final Widget child;

  BaseScreen({this.child});

  @override
  Widget build(BuildContext context) {
    var safePadding = MediaQuery.of(context).padding.top +
        MediaQuery.of(context).padding.bottom;

    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraint) {
          return SingleChildScrollView(
            child: SafeArea(
              child: ConstrainedBox(
                constraints: BoxConstraints(
                    minHeight: constraint.maxHeight - safePadding),
                child: IntrinsicHeight(
                  child: child,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

Pero el problema que veo es que también me gustaría reutilizar la propiedad constraint que LayoutBuilder proporciona en el hijo de esta clase.

Actualmente, necesito crear un nuevo LayoutBuilder en el niño, y eso suena como más procesamiento para el motor y más código repetitivo.

Si pudiera extender de alguna manera este Widget para que en el niño pudiera tener esto:

  @override
  Widget build(BuildContext context, BoxConstraints constraints) {
  }

Eso seria genial. Sé que Flutter también fomenta la composición sobre la herencia, así que si puedo resolverlo de otra manera, también lo agradecería.

¡Gracias!

2
Jan 29 abr. 2020 a las 22:32

2 respuestas

La mejor respuesta

TL; DR : No, use InheritedWidget para pasar variables / datos a widgets secundarios, lea más sobre esto en aquí y aquí


¿Por qué no?

En lenguaje Dart, solo es posible agregar parámetros opcionales / no conflictivos nombrados a los métodos anulados.

Por ejemplo:

class SuperClass {
  void someMethod(String parameter1) {}
}

class SubClass1 extends SuperClass {
  // adding optional parameter
  @override
  void someMethod(String paremeter1, [String paremter2]) {}
}

class SubClass2 extends SuperClass {
  // adding optional named parameter
  @override
  void someMethod(String paremeter1, {String paremter2}) {}
}

Nota: Dart no admite la sobrecarga de métodos, lo que significa que es un error de compilación tener dos métodos con el mismo nombre pero con parámetros diferentes.

Ahora si agrega BoxConstraints constraints en su método build() como este

@override
Widget build(BuildContext context, [BoxConstraints constraint]){
   /// Your code
}

Se compilará, pero ¿quién le dará ese parámetro [de restricción]?

Como desarrolladores nunca llamamos al método build() nosotros mismos, el marco de flutter nos llama a ese método.

Motivo de eso : Llamar el método build() nosotros mismos sería difícil porque requiere context, y proporcionar un valor de contexto correcto es algo que solo el marco de aleteo hace correctamente. La mayoría de los nuevos desarrolladores pasan la variable context pero no está garantizado si eso siempre funcionará, porque el lugar del widget en el árbol de widgets determina cuál es el valor correcto context para ese widget. Y durante la escritura del código, no hay una manera fácil de averiguar cuál es el lugar exacto del widget en el árbol de widgets. Incluso si de alguna manera pudiéramos descubrir el lugar, ¿cuál es el valor de context para ese lugar? Debido a que flutter proporciona ese valor, cómo se crea ese valor es para otra publicación.


Soluciones

Hay dos soluciones fáciles y muy comunes en flutter para pasar datos / variables a widgets secundarios,

  1. Usar variantes WidgetBuilder
  2. Usando InheritedWidget (Recomendado)

Solución 1. Usar variantes WidgetBuilder

WidgetBuilder es una función que toma BuildContext y devuelve un Widget, ¿suena familiar ?, es la definición de tipo del método build(). Pero ya tenemos el método build() disponible, ¿cuál es el punto de WidgetBuilder? El caso de uso más común es para determinar el alcance BuildContext.

Por ejemplo: si hace clic en "Mostrar snackbar" no funcionará y en su lugar arrojará un error diciendo "Scaffold.of () llamado con un contexto que no contiene un Scaffold".

Widget build(BuildContext context) {
return Scaffold(
      body: Center(
            child: FlatButton(
              onPressed: () {
                /// This will not work
                Scaffold.of(context)
                    .showSnackBar(SnackBar(content: Text('Hello')));
              },
              child: Text('Show snackbar'),
            ),
      )
);
}

Puede pensar, claramente hay un widget Scaffold presente, pero dice que no hay andamio. Esto se debe a que la siguiente línea está utilizando context proporcionado por un widget anterior el widget Scaffold (el método build ()).

Scaffold.of(context).showSnackBar(SnackBar(content: Text('Hello')));

Si envuelve el FlatButton con el Builder widget, funcionará pruébalo.

Al igual que muchos widgets de flutter, puede crear una variante de WidgetBuilder que proporcione parámetros adicionales mientras construye el widget como AsyncWidgetBuilder AsyncWidgetBuilder o como LayoutBuilder 's LayoutWidgetBuilder

Por ejemplo:

class BaseScreen extends StatelessWidget {
  /// Instead of [child], a builder is used here
  final LayoutWidgetBuilder builder;
  const BaseScreen({this.builder});

  @override
  Widget build(BuildContext context) {
    var safePadding = MediaQuery.of(context).padding.top +
        MediaQuery.of(context).padding.bottom;

    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraint) {
          return SingleChildScrollView(
            child: SafeArea(
              child: ConstrainedBox(
                constraints: BoxConstraints(
                  minHeight: constraint.maxHeight - safePadding,
                ),
                /// Here we forward the [constraint] to [builder], 
                /// so that it can forward it to child widget
                child: builder(context, constraint),
              ),
            ),
          );
        },
      ),
    );
  }
}

Y así es como lo usa (al igual que LayoutBuilder, pero el niño obtiene la restricción LayoutBuilder del widget principal y solo se requiere un LayoutBuilder

  @override
  Widget build(BuildContext context) {
    return BaseScreen(
      builder: (context, constraint) {
        // TODO: use the constraints as you wish
        return Container(
          color: Colors.blue,
          height: constraint.minHeight,
        );
      },
    );
  }

Solución 2. Usando InheritedWidget (Recomendado)

Muestra InheritedWidget

/// [InheritedWidget]s are very efficient, in fact they are used throughout
/// flutter's source code. Even the `MediaQuery.of(context)` and `Theme.of(context)`
/// is actually an [InheritedWidget]
class InheritedConstraint extends InheritedWidget {
  const InheritedConstraint({
    Key key,
    @required this.constraint,
    @required Widget child,
  })  : assert(constraint != null),
        assert(child != null),
        super(key: key, child: child);

  final BoxConstraints constraint;

  static InheritedConstraint of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<InheritedConstraint>();
  }

  @override
  bool updateShouldNotify(covariant InheritedConstraint old) =>
      constraint != old.constraint;
}

extension $InheritedConstraint on BuildContext {
  /// Get the constraints provided by parent widget
  BoxConstraints get constraints => InheritedConstraint.of(this).constraint;
}

Su widget secundario puede acceder al BoxConstraints proporcionado por este widget heredado como este

class ChildUsingInheritedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    /// Get the constrains provided by parent widget
    final constraint = context.constraints;
    // TODO: use the constraints as you wish
    return Container(
      color: Colors.green,
      height: constraint.minHeight,
    );
  }
}

Y así es como usas connect estos dos widgets

En su BaseScreen, envuelva el child con InheritedConstraint

class BaseScreen extends StatelessWidget {
  final Widget child;
  const BaseScreen({this.child});

  @override
  Widget build(BuildContext context) {
    var safePadding = MediaQuery.of(context).padding.top +
        MediaQuery.of(context).padding.bottom;

    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraint) {
          return SingleChildScrollView(
            child: SafeArea(
              child: ConstrainedBox(
                constraints: BoxConstraints(
                  minHeight: constraint.maxHeight - safePadding,
                ),
                child:
                    InheritedConstraint(constraint: constraint, child: child),
              ),
            ),
          );
        },
      ),
    );
  }
}

Y puede usar BaseScreen en cualquier lugar que desee Por ejemplo:

  @override
  Widget build(BuildContext context) {
    return BaseScreen(child: ChildUsingInheritedWidget());
  }

Vea este ejemplo de DartPad de trabajo: https://dartpad.dev/9e35ba5c2dd938a267f0a1a0daf814


Nota: Noté esta línea en su código de ejemplo:

    var safePadding = MediaQuery.of(context).padding.top +
        MediaQuery.of(context).padding.bottom;

Si está tratando de obtener el relleno proporcionado por el widget SafeArea(), esa línea no le dará el relleno correcto, porque está usando context incorrecto, debería usar un contexto que esté debajo de SafeArea() para hacer eso, use el Builder widget.

Ejemplo:

class BaseScreen extends StatelessWidget {
  final Widget child;
  const BaseScreen({this.child});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraint) {
          return SingleChildScrollView(
            child: SafeArea(
              child: Builder(
                builder: (context) {
                  var safePadding = MediaQuery.of(context).padding.top +
                      MediaQuery.of(context).padding.bottom;
                  return ConstrainedBox(
                    constraints: BoxConstraints(
                      minHeight: constraint.maxHeight - safePadding,
                    ),
                    child: child,
                  );
                },
              ),
            ),
          );
        },
      ),
    );
  }
}
1
devkabiir 15 may. 2020 a las 21:52

Por supuesto que puede. Se verá algo como esto

abstract class BoxConstraintsWidget extends StatelessWidget {

  Widget build(BuildContext context, BoxConstraints constraints);

  @override
  Widget build(BuildContext context) {
    return build(context, BoxConstraints());
  }

}

Luego anularlo como

class BoxConstraintsWidgetChild extends BoxConstraintsWidget{

  @override
  Widget build(BuildContext context, BoxConstraints constraints) {
    return someStuff;
  }

}

Sin embargo, hay un pequeño problema: Widget build(BuildContext context) es un método de marco interno y no puede forzarlo a que se llame con más de un parámetro (por supuesto, si no desea reescribir el aleteo completo usted mismo). La cuestión es que puede usar el enfoque anterior, pero agregue esto BoxConstraints constraints como algunos getter en su clase base con la implementación predeterminada y anúlelo en su elemento secundario si lo desea. Se verá así:

abstract class BoxConstraintsWidget extends StatelessWidget {

  BoxConstraints get constraints => BoxConstraints();

  Widget build(BuildContext context, BoxConstraints constraints);

  @override
  Widget build(BuildContext context) {
    return build(context, constraints);
  }

}

Y úsalo como está o anúlalo como

class BoxConstraintsWidgetChild extends BoxConstraintsWidget{

  @override
  BoxConstraints get constraints => MyBoxConstraints();

  @override
  Widget build(BuildContext context, BoxConstraints constraints) {
    //here you will have you copy of constraints that = MyBoxConstraints()
    //without overriding method you would have had a parent constraints that = BoxConstraints()
    return someStuff;
  }

}

Este es solo un enfoque y quizás sea un poco redundante, pero puede experimentar. Puede usarlo sin una herencia.

También puede experimentar con Builders personalizado para su widget que funcionará como ListView.builder(), LayoutBuilder() o FutureBuilder(). Le recomendaría que investigue cómo funcionan.

También puede crear un constructor personalizado para su widget secundario que reciba BoxConstraints como parámetro y lo almacene como widget para ser usuario en constructores State o StatelessWidget.

Hay muchas más formas de hacerlo, la mayoría de las cuales serán implementaciones diferentes de composición simple, así que sí ... experimento))

Espero que ayude.

1
Pavlo Ostasha 12 may. 2020 a las 20:31