Make SafeArea Responsive in Flutter – Responsive Widget Tutorial

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.

Content is behind the status bar

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.

Status bar is no longer covering with the content

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 🎉

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

>