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!
TL;DR : No, use InheritedWidget
to pass variables/data to child widgets, read more about it in here and here
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.
There are two easy and very common solutions in flutter for passing data/variables to child widgets,
WidgetBuilder
variantsInheritedWidget
(Recommended)WidgetBuilder
variantsWidgetBuilder
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,
);
},
);
}
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,
);
},
),
),
);
},
),
);
}
}