是否可以扩展在其构建方法上提供额外参数的 StatefulWidget?

Is it possible to extend a StatefulWidget that provides an extra parameter on its build method?

我想创建一个像这样的 BaseScreen 小部件以在我的应用程序中重复使用:

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

但是我看到的问题是我还想重用 LayoutBuilder 在这个 class 的 child 中提供的 constraint 属性 .

目前,我需要在 child 中创建一个新的 LayoutBuilder,这听起来像是对引擎的更多处理,以及更多样板代码。

如果我能以某种方式扩展这个小部件,以便在 child 中我可以拥有这个:

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

那就太好了。我知道 Flutter 也鼓励组合而不是继承,所以如果我能用另一种方式解决它,我也会很感激。

谢谢!

当然可以。它看起来像这样

abstract class BoxConstraintsWidget extends StatelessWidget {

  Widget build(BuildContext context, BoxConstraints constraints);

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

}

然后像

一样覆盖它
class BoxConstraintsWidgetChild extends BoxConstraintsWidget{

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

}

虽然有一个小问题 - Widget build(BuildContext context) 是一种内部框架方法,您不能强制使用多个参数调用它(当然,如果您不想自己重写整个 flutter ).问题是你可以使用上面的方法,但是在你的基础 class 中添加这个 BoxConstraints constraints 作为一些 getter 和默认实现,如果你想在它的 child 中覆盖它.它看起来像这样:

abstract class BoxConstraintsWidget extends StatelessWidget {

  BoxConstraints get constraints => BoxConstraints();

  Widget build(BuildContext context, BoxConstraints constraints);

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

}

并按原样使用它或覆盖它

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

}

这只是一种方法,可能有点多余,但您可以尝试一下。您可以在没有继承的情况下使用它。

您也可以为您的小部件试验自定义 Builders,其工作方式类似于 ListView.builder()LayoutBuilder()FutureBuilder()。我建议您研究一下它们是如何工作的。

您还可以为 child 小部件创建一个自定义构造函数,它接收 BoxConstraints 作为参数并将其存储在 State 或 [= 中作为用户的小部件23=]建设者。

还有很多方法可以做到这一点,其中大部分都是简单组合的不同实现,所以是的......实验))

希望对您有所帮助。

TL;DR :不,使用 InheritedWidget 将 variables/data 传递给子部件,在 here and here[= 中阅读更多相关信息64=]


为什么不呢?

在 Dart 语言中,只能将 optional/named 个不冲突的参数添加到重写的方法中。

例如:

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

注意:Dart 不支持方法重载,这意味着具有相同名称但不同参数的两个方法是编译错误。

现在,如果您像这样在 build() 方法中添加 BoxConstraints constraints

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

它会编译,但谁会给你那个 [constraint] 参数?

作为开发人员,我们从不自己调用 build() 方法,flutter 框架会为我们调用该方法。

这样做的原因:我们自己调用build()方法会很困难,因为它需要context,并且提供正确的上下文值只会让人颤抖框架正确。大多数新开发人员都会传递 context 变量,但不能保证它是否始终有效,因为小部件在小部件树中的位置决定了该小部件的正确 context 值。并且在编写代码期间,没有简单的方法来确定小部件在小部件树中的确切位置。即使我们能以某种方式找出那个地方,那个地方的 context 的值是多少?因为 flutter 提供了该值,所以该值的创建方式是另一个 post.


解决方案

flutter 中有两种简单且非常常见的解决方案,用于将 data/variables 传递给子部件,

  1. 使用 WidgetBuilder 个变体
  2. 使用InheritedWidget(推荐)

解决方案 1. 使用 WidgetBuilder 变体

WidgetBuilder是一个函数,它接受BuildContext和returns一个Widget,听起来很熟悉吧?,它是build()方法的类型定义。但是我们已经有可用的 build() 方法,WidgetBuilder 有什么意义呢?。最常见的用例是 BuildContext.

的范围

例如: 如果你点击 "Show snackbar" 它将不起作用,而是抛出错误 "Scaffold.of() called with a context that does not contain a 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'),
            ),
      )
);
}

你可能会想,明明有一个Scaffold widget,却说没有脚手架?这是因为以下行使用 context 上面的 小部件提供 Scaffold 小部件(build() 方法)。

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

如果用 Builder widget, it will work try it.

包裹 FlatButton

像许多 flutter 小部件一样,您可以创建一个 WidgetBuilder 变体,它在构建小部件时提供额外的参数,例如 FutureBuilder's AsyncWidgetBuilder or like LayoutBuilder's LayoutWidgetBuilder

例如:

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

这就是你如何使用它(就像LayoutBuilder一样,但是子部件得到父部件的LayoutBuilder的约束并且只需要一个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,
        );
      },
    );
  }

解决方案 2. 使用 InheritedWidget(推荐)

样本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;
}

您的子小部件可以像这样访问此继承小部件提供的BoxConstraints

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

这就是您使用 connect 这两个小部件的方式

在你的 BaseScreen 中用 InheritedConstraint

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

而且您可以在任何您喜欢的地方使用 BaseScreen 例如:

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

查看此工作 DartPad 示例:https://dartpad.dev/9e35ba5c2dd938a267f0a1a0daf814a7


注意:我在您的示例代码中注意到这一行:

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

如果您尝试获取 SafeArea() 小部件提供的填充,那么该行不会为您提供正确的填充,因为它使用了错误的 context 它应该使用 下方 SafeArea() 要做到这一点,请使用 Builder 小部件。

示例:

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