【问题标题】:Is it possible to extend a StatefulWidget that provides an extra parameter on its build method?是否可以扩展在其构建方法上提供额外参数的 StatefulWidget?
【发布时间】:2020-08-14 00:49:58
【问题描述】:

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

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 在此类的子级中提供的 constraint 属性。

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

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

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

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

谢谢!

【问题讨论】:

    标签: flutter statefulwidget


    【解决方案1】:

    TL;DR:不,使用InheritedWidget 将变量/数据传递给子小部件,在herehere 中了解更多信息


    为什么不呢?

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

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

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

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

    如果您用Builder 小部件包装FlatButton,它将工作try it.

    像许多颤振小部件一样,您可以创建一个 WidgetBuilder 变体,在构建小部件时提供额外的参数,例如 FutureBuilderAsyncWidgetBuilderLayoutBuilderLayoutWidgetBuilder

    例如:

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

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

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

    【讨论】:

      【解决方案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 作为参数并将其作为用户小部件存储在StateStatelessWidget 构建器中。

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

      希望对你有帮助。

      【讨论】:

        猜你喜欢
        • 2010-12-24
        • 1970-01-01
        • 2015-06-09
        • 2019-12-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-06-11
        • 2012-03-05
        相关资源
        最近更新 更多