Device-Agnostic Design

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.

Run the program to see the output

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.

Loading Exercise...

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.

Run the program to see the output

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.

Loading Exercise...

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.

Run the program to see the output

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.

Run the program to see the output

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.

Run the program to see the output

MediaQuery and LayoutBuilder

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