2

我想创建一个这样的 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,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

但是我看到的问题是我还想重用在这个类的孩子中提供的constraint属性。LayoutBuilder

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

如果我能以某种方式扩展这个小部件,以便在孩子身上我可以拥有这个:

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

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

谢谢!

4

2 回答 2

5

TL;DR:不,用于将变量/数据传递给子小部件,在此处此处InheritedWidget阅读有关它的更多信息


为什么不?

在 Dart 语言中,只能将可选/命名的非冲突参数添加到被覆盖的方法。

例如:

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 不支持方法重载,这意味着有两个同名但参数不同的方法会导致编译错误。

现在如果你像这样添加BoxConstraints constraints你的build()方法

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

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

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

原因:我们自己调用该build()方法会很困难,因为它需要context,并且提供正确的上下文值是只有 Flutter 框架才能正确完成的事情。大多数新开发人员都会传递context变量,但不能保证它是否总是有效,因为小部件在小部件树中的位置决定了该小部件的正确context值是什么。并且在编写代码期间,没有简单的方法可以确定小部件在小部件树中的确切位置。即使我们能以某种方式找出这个地方,context那个地方的价值是什么?因为 Flutter 提供了那个价值,所以这个价值是如何创造的,就另当别论了。


解决方案

Flutter 中有两种简单且非常常见的解决方案,用于将数据/变量传递给子小部件,

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

解决方案 1. 使用WidgetBuilder变体

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

例如:如果您单击“显示快餐栏”,它将不起作用,而是抛出错误并显示“使用不包含 Scaffold 的上下文调用的 Scaffold.of()”。

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小部件存在,但它说没有脚手架?这是因为下面的行正在使用context小部件上方Scaffold小部件(build() 方法)提供。

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

如果你FlatButtonBuilder小部件包装它,它会起作用

像许多颤振小部件一样,您可以创建一个 WidgetBuilder 变体,在构建类似FutureBuilder'sAsyncWidgetBuilder或 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,
    );
  }
}

这就是你如何使用连接这两个小部件

在你的 BaseScreenchild中用 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),
              ),
            ),
          );
        },
      ),
    );
  }
}

你可以在任何你喜欢的地方使用 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,
                  );
                },
              ),
            ),
          );
        },
      ),
    );
  }
}
于 2020-05-15T21:44:22.300 回答
2

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

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)是一种内部框架方法,您不能强制使用多个参数调用它(当然,如果您不想自己重写完整的颤振)。问题是您可以使用上面的方法,但将其添加BoxConstraints constraints为具有默认实现的基类中的一些getter,并在其子类中覆盖它(如果您愿意)。它看起来像这样:

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(). 我建议您调查它们是如何工作的。

您还可以为您的子小部件创建一个自定义构造函数,该构造函数BoxConstraints作为参数接收并将其存储为要在 aStateStatelessWidget构建器中成为用户的小部件。

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

希望能帮助到你。

于 2020-05-11T23:13:15.550 回答