Responsive Widgets with Flutter
Learning Objectives
- You know of Flutter’s MediaQuery and LayoutBuilder.
- You know how to use MediaQuery and LayoutBuilder to create responsive widgets.
Flutter provides support for creating responsive widgets and layouts. Similar to media queries in CSS, Flutter has a widget called MediaQuery
that allows accessing the properties of the current media, and similar to container queries in CSS that allow working with the size of the parent widget, Flutter has a widget called LayoutBuilder
.
Here, we briefly look into using these, and then look into the possibility of creating a generic responsive widget that can be used to create responsive content. Similar to as one would use breakpoints when creating responsive layouts with CSS, we also use breakpoints in Flutter. For simplicity, we’ll use the sizes as used in TailwindCSS, and define a class called Breakpoints
that holds the breakpoints.
class Breakpoints {
static const sm = 640;
static const md = 768;
static const lg = 1024;
static const xl = 1280;
static const xl2 = 1536;
}
MediaQuery
The MediaQuery widget provides access to the properties of the current media. Using the method MediaQuery.of that takes the BuildContext
as a parameter, we receive an instance of MediaQueryData, which contains data about the present device and screen, including information such as the size of the screen, the orientation of the screen, areas of the screen that are obscured by system UI, and more.
The following example outlines the use of MediaQuery
to decide what to show based on the screen size. If the screen width is smaller than the breakpoint sm
, we show a container with a red background, otherwise, we show a container with a green background.
The above example demonstrates the use of the size property of MediaQueryData
. The property size contains the screen size, represented using an instance of a 2D Size class.
Widgets that use MediaQuery
are rebuilt whenever the values of the corresponding MediaQueryData
change. That is, if the user rotates the device, or if they adjust the size of the application window, the widgets that use MediaQuery
are rebuilt.
LayoutBuilder
Contrary to MediaQuery that has access to the size of the device, the LayoutBuilder widget has access to the size of the parent widget. The following example outlines the use of LayoutBuilder, creating a similar effect as the previous example with MediaQuery.
The constructor of LayoutBuilder
has an attribute builder
, which is given a two-parameter function. The parameters of the function are BuildContext
and an instance of BoxConstraints.
The BoxConstraints
has properties which can be used to determine the size of the parent widget; in the example above, we use the maxWidth
property to decide the widget based on the parent width. If the width of the parent is smaller than the breakpoint sm
pixels, we show a container with a red background, while otherwise, we show a container with a green background.
When asking for the maxWidth
property, we are receiving the maximum possible width for a widget within the constraints that the LayoutBuilder
receives from the parent widget. That is, the maxWidth
property is defined based on the parent widget.
The following example outlines this behavior. The HomeScreen
widget contains two Expanded
widgets, each of which contains a widget that uses LayoutBuilder
. The LayoutBuilder
widget shows the value of the maxWidth
property of the BoxConstraints
object within a Center
widget.
When you try out the above example locally, you’ll notice that the values change when you adjust the width of the screen. Similarly, the values change if you add an extra instance of a MyWidget to the application.
Using the maximum width of the parent widget can also be problematic, if we are not careful, given that the size of the parent widget is determined by Flutter’s layout algorithm.
As an example, below, Row
tries to maximize the width of the children, going beyond the actual width of the device screen. Try running the following example to see what happens.
The solution in this case is to wrap the widget in a way that leads to a specific width. This could be done, for example, by wrapping the widget in an Expanded
widget, i.e. either by defining the widget as an Expanded
widget, or by defining the widget so that it uses Expanded
, which allows setting the width of the widget; the latter is shown below.
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Expanded(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Center(
child: Text('To ${constraints.maxWidth} and beyond!'),
);
},
),
);
}
}
Alternatively, the widget could also be defined so that it has a specific width. Below, the widget is wrapped within a Container
widget that has a width of 200 pixels.
To summarize, MediaQuery allows access to information about the size of the media (i.e. the device), while LayoutBuilder allows access to the size of the parent widget. When asking for screen size with MediaQuery, we receive the size of the screen, while when asking for the constraints with LayoutBuilder, we receive the constraints imposed by the parent widget.
Generic Responsive Widget
Given a set of breakpoints and knowledge of LayoutBuilder
(or MediaQuery
), we can create a widget that decides what to show based on a set of given breakpoints. The following example outlines the key idea, although there is just one breakpoint.
// ...
class ResponsiveWidget extends StatelessWidget {
final Widget mobile;
final Widget desktop;
const ResponsiveWidget({required this.mobile, required this.desktop});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth < Breakpoints.sm) {
return mobile;
} else {
return desktop;
}
},
);
}
}
// ...
With a responsive widget, creating responsive content becomes a bit easier. The following example shows the use of the above ResponsiveWidget
for creating a simple screen where the shown widget changes based on the breakpoint.
// ...
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ResponsiveWidget(
mobile: Container(color: Colors.green),
desktop: Container(color: Colors.red),
);
}
}
// ...