Search code examples
flutterstatefulwidget

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


I would like to create a BaseScreen Widget like this to reuse in my app:

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

But the problem I see is that I would also like to reuse the constraint property that LayoutBuilder provides in the child of this class.

Currently, I need to create yet a new LayoutBuilder in the child, and that just sounds like more processing for the engine, and more boilerplate code.

If I could extend somehow this Widget so that in the child I could then have this:

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

That would be great. I know Flutter encourages composition over inheritance as well, so if I can solve it in another way, I'd also appreciate that.

Thank you!


Solution

  • TL;DR : No, use InheritedWidget to pass variables/data to child widgets, read more about it in here and here


    Why not?

    In Dart language it is only possible to add optional/named non-conflicting parameters to overridden methods.

    For example:

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

    Note: Dart does not support method overloading which means is a compile error to have two methods with same name but different parameters.

    Now if you add BoxConstraints constraints in your build() method like this

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

    It will compile but who is going to give you that [constraint] parameter?

    As developers we never call the build() method ourselves, the flutter framework calls that method for us.

    Reason for that: Calling the build() method ourselves would be difficult because it requires context, and providing correct context value is something that only flutter framework does correctly. Most new developers pass around the context variable but it's not guaranteed if that will always work, because the place of the widget in the widget tree determines what is the correct context value for that widget. And during writing code, there is no easy way to figure out what is the exact place of the widget in the widget tree. Even if somehow we could figure out the place, what is the value of context for that place? Because flutter provides that value, how that value is created is for another post.


    Solutions

    There are two easy and very common solutions in flutter for passing data/variables to child widgets,

    1. Using WidgetBuilder variants
    2. Using InheritedWidget (Recommended)

    Solution 1. Using WidgetBuilder variants

    WidgetBuilder is a function that takes BuildContext and returns a Widget, sounds familiar?, it's the type definition of the build() method. But we already have build() method available, what's the point of WidgetBuilder?. The most common use case is for scoping the BuildContext.

    For example: If you click on "Show snackbar" it will not work and instead throw and error saying "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'),
                ),
          )
    );
    }
    

    You might think, there is clearly a Scaffold widget present, but it says there is no scaffold? This is because the following line is using context provided by a widget above the Scaffold widget (the build() method).

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

    If you wrap the FlatButton with the Builder widget, it will work try it.

    Like many flutter widgets you could create a WidgetBuilder variant that provides additional parameters while building the widget like FutureBuilder's AsyncWidgetBuilder or like LayoutBuilder's LayoutWidgetBuilder

    For example:

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

    And this is how you use it (Just like LayoutBuilder, but the child gets the parent widget's LayoutBuilder's constraint and only one LayoutBuilder is required

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

    Solution 2. Using InheritedWidget (Recommended)

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

    Your child widget can access the BoxConstraints provided by this inherited widget like this

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

    And this is how you use connect these two widgets

    In your BaseScreen wrap the child with 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),
                  ),
                ),
              );
            },
          ),
        );
      }
    }
    
    

    And you can use the BaseScreen anywhere you like For example:

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

    See this working DartPad example: https://dartpad.dev/9e35ba5c2dd938a267f0a1a0daf814a7


    Note: I noticed this line in your example code:

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

    If you are trying to get the padding provided by SafeArea() widget, then that line will not give you correct padding, because it's using wrong context it should use a context that is below SafeArea() to do that, use the Builder widget.

    Example:

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