4

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!

Jan
  • 2,462
  • 3
  • 31
  • 56

2 Answers2

8

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,
                  );
                },
              ),
            ),
          );
        },
      ),
    );
  }
}
user6552940
  • 926
  • 8
  • 13
2

Of course you can. It will look something like this

abstract class BoxConstraintsWidget extends StatelessWidget {

  Widget build(BuildContext context, BoxConstraints constraints);

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

}

Then override it like

class BoxConstraintsWidgetChild extends BoxConstraintsWidget{

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

}

There is one tiny problem though - Widget build(BuildContext context) is an inner framework method and you cannot force it to be called with more that one parameter(of course if you do not want to rewrite the complete flutter by yourself). The thing is you may use the approach above but add this BoxConstraints constraints as some getter in your base class with the default implementation and override it in its child if you want to. It will look like this:

abstract class BoxConstraintsWidget extends StatelessWidget {

  BoxConstraints get constraints => BoxConstraints();

  Widget build(BuildContext context, BoxConstraints constraints);

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

}

And use it as it is or override it as

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

}

This is only one approach and it maybe a little redundant one but you may experiment. You may use it without an inheritance.

Also you may experiment with custom Builders for your widget that will work like ListView.builder(), LayoutBuilder() or FutureBuilder(). I would recommend you to investigate how do they work.

Also you can create a custom constructor for your child widget that receives BoxConstraints as a parameter and stores it is the widget to be user either in a State or StatelessWidget builders.

There are many more ways to do it most of which will be a different implementations of simple composition so yeah... experiment))

Hope it helps.

Pavlo Ostasha
  • 14,527
  • 11
  • 35