We all know how to use the MediaQuery
to get the size of the whole screen's width and height. More often than not though, you don't want to adjust the size of a widget based on the dimensions of the whole screen. The most alarming of example of this need is a SafeArea
, where we want to adjust its children not based on the whole screen size, but rather on the "cropped" size of the safe area!
Without the SafeArea
Imagine we're building an app without an app bar. The purpose of the app is to display a message across one half of the available space. At first, we might develop the following code which, however, doesn't look right on the screen.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
// Builder for the ability to access the MediaQuery through an InheritedWidget
body: Builder(
builder: (context) {
return Container(
decoration: BoxDecoration(color: Colors.green.shade200),
alignment: Alignment.topCenter,
// Using media query is fine now, we're really dealing with the whole screen
height: MediaQuery.of(context).size.height / 2,
child: Column(
children: <Widget>[
Text(
"Let's get",
style: Theme.of(context).textTheme.display1,
),
Text(
"RESPONSIVE",
style: Theme.of(context).textTheme.display3,
),
],
),
);
},
),
),
);
}
}
Adding a SafeArea
Being battle-tested Flutter developers, we know how to make sure that no content is displayed behind a status bar. Just add a SafeArea
and call it a day, right?
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
// Builder for the ability to access the MediaQuery through an InheritedWidget
body: Builder(
builder: (context) {
return SafeArea(
child: Container(
...
),
);
},
),
),
);
}
}
Doing this will make the UI look much better, but we've just made ourselves another issue. By still using the MediaQuery
to get hold of the height, the Container
still takes up one half of the whole screen. That's not what we want! We want the container to take up only a half of the available space, which, in this case, is the space inside the SafeArea
.
I know, this may seem trivial, but when you're handed absolutely precise design specs from a designer, following them to the tee is a must! Unless you enjoy heated design-related arguments, of course ?
Solution - ResponsiveSafeArea
To sum up what we want to accomplish, we're basically trying to get the size of the available space inside the SafeArea
down to its children. There's a perfect way to accomplish this goal with a custom-made ResponsiveSafeArea
widget. It will be very similar to the "vanilla" SafeArea
, just instead of passing in a child widget, it's going to have a builder method. The final code utilizing such a widget will look like the following:
main.dart
ResponsiveSafeArea(
builder: (context, size) {
// The "size" parameter holds the available space
},
);
ResponsiveBuilder
Firstly, let's define the signature of the custom builder method. To keep it Flutter-compliant, we definitely want to pass down the BuildContext
and, of course, it has to have a Size
parameter.
responsive_safe_area.dart
import 'package:flutter/material.dart';
typedef ResponsiveBuilder = Widget Function(
BuildContext context,
Size size,
);
Now we have a way to pass around the safe area's size, how can we actually obtain it though?
Obtaining the available size
We now know that using a MediaQuery
is out of play, so are there other ways which allow us to get the size of a particular widget? Yes! There's a LayoutBuilder
which passes in an instance of Constraints
.
layout_builder_example.dart
LayoutBuilder(
builder: (context, constraints) {
// Constraints contain a lot of interesting data
},
);
While we can get a plethora of things out of the Constraints
object, we're interested in the biggest size available. For that, we can call constraints.biggest
.
Let's put it all into practice together with a SafeArea
and the ResponsiveBuilder
method:
responsive_safe_area.dart
class ResponsiveSafeArea extends StatelessWidget {
const ResponsiveSafeArea({
@required ResponsiveBuilder builder,
Key key,
}) : responsiveBuilder = builder,
super(key: key);
final ResponsiveBuilder responsiveBuilder;
@override
Widget build(BuildContext context) {
return SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return responsiveBuilder(
context,
constraints.biggest,
);
},
),
);
}
}
Using the ResponsiveSafeArea
Now that we have the widget, we're going to use it in our app to finally have the wonderful green container take up really just a half of the available space. The most important change in the code is using the size
parameter instead of the size coming from a MediaQuery
.
responsive_safe_area.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
// The Builder widget is no longer needed
body: ResponsiveSafeArea(
builder: (context, size) {
return Container(
decoration: BoxDecoration(color: Colors.green.shade200),
alignment: Alignment.topCenter,
height: size.height / 2,
child: Column(
children: <Widget>[
Text(
"Let's get",
style: Theme.of(context).textTheme.display1,
),
Text(
"RESPONSIVE",
style: Theme.of(context).textTheme.display3,
),
],
),
);
},
),
),
);
}
}
And now, the container truly does take up only a half of the screen ?
Wonderful tutorial!
I came across this, because I was irritated by the fact that I could not use a Column inside a Slider. I needed the slider for showing a keyboard without the yellow error lines…
I thought that SafeArea would be my key, as I had hoped that this would give me a box with fixed height and width. Alternative could be to give my app component fixed height, but that would have been annoying when going landscape (I think, anyway…)
I refactored your solution to let this function return a container that has the maximum size possible -/- appbar -/- statusbar. Then I can layout my widgets, for instance using Expanded or Flex. And when I get the keyboard on screen, the components just move up, in stead of re-measuring themselves. Hope this makes sense.
Frank