0  comments

The biggest appeal of Flutter is being able to create apps that can run on multiple devices with just a single codebase. With the stable release of Flutter for the web, the apps you create become even more accessible.

Even though the apps you create will run on all compatible devices, we are faced with the challenge of displaying the optimal UI on a huge variety of screen sizes. That is why it is more important than ever to make your apps responsive.

In this tutorial you will learn how to use the Responsive Framework package to easily make your app UI adjust to different screen sizes.

The Finished App

In this tutorial we are going to transform an existing simplified e-learning app UI from non-responsive to responsive. The finished app will adjust in various ways depending on the breakpoints we define for different screen sizes. The final project will be able to do the following for different breakpoint conditions:

  • Scale and resize
  • Change the course tiles section from Column to Row and vice versa
  • Hide or display certain elements of the app bar
  • Change the font size value of the page header text

Getting Started

In this tutorial we’ll be working with a starter project which you can grab along with the finished project from the links below.

Once you’ve got the starter project, you can go ahead and add the only external dependency we will be using in this lesson - the Responsive Framework package. We’ll be using version 0.1.4. If you are following this lesson in the future, be mindful of any potential breaking changes when using an updated version of this package.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  responsive_framework: ^0.1.4

Starter Project Overview

The starter project we’re working with has a built-out UI that we’ll transform throughout this lesson to become responsive. The project consists of several files in the lib folder and an assets folder. Here’s the breakdown of all the relevant app files/folders:

  • lib/main.dart
    Here we’ve got a simple AppWidget which returns a MaterialApp that has the CoursesPage as the home argument.
  • lib/courses_data.dart
    This file contains a Course class that we use to provide mock data for the course tiles in the app.
  • lib/courses_page.dart
    This is the file where pretty much the entire UI structure lives. This file is responsible for displaying the only page in our app. It's made up of a Scaffold with an AppBar that has a title and some action buttons. The Scaffold body contains a ListView which displays the PageHeader widget, Column with CourseTile widgets and a SubscribeBlock widget.
  • lib/widgets.dart
    This file contains some of the custom widgets that are used in the courses_page.dart file to keep our code neater.
  • assets
    The starter project also contains an assets folder which has 3 images used in the app for the header image and course tiles. 

Understanding Scaling vs. Resizing

Before we begin, let’s see how the starter project looks at different screen sizes. Run the starter project in a browser. Then, enlarge and shrink the browser window and see how the current UI reacts to the screen changes.

Flutter Default Behavior (Resizing)

When testing the starter project, you’ll notice that some things do change as the screen size changes. For instance, the AppBar keeps stretching no matter how large the screen gets.

Also, the header image shrinks when the screen is small and enlarges up to a maximum of 800 pixels when the screen size goes up. Once the widgets reach their maximum possible size, they stay at that size no matter how large the screen gets. So, by default Flutter does try to a certain degree to adjust the UI to changes in screen size by resizing the widgets. Let’s understand what’s happening better with some examples:

App bar - The AppBar has a width of double.infinity, so it will stretch to fill the available width no matter how large the screen gets.

Header image - In the widgets.dart file we specified that the image we have in the header should have a width of 800 pixels. However, you’ll see that it does get smaller when the screen size decreases, but does not exceed 800 pixels when the screen size increases. This is due to ongoing negotiations between the parent and child widgets regarding how much space the child can take up. The image width changes based on the constraints defined by the parent widget.

In this tutorial we won’t be diving into the depths of how Flutter layouts work. If you want to understand how constraints, sizing and widget positioning work, an incredibly detailed guide to this can be found in the Flutter documentation section about understanding constraints.

You might think that the layout we have now doesn’t look too bad and can be shipped as is with the default resizing behavior. You might be right, as the UI is still usable, but just because something works doesn’t mean we can’t make it better.

Responsive Framework Difference (Scaling)

As discussed above, by default Flutter resizes the widgets on the screen. While this is nice, for certain devices and screen sizes, it would be beneficial if the UI could scale proportionally as well. This is the core offering of the Responsive Framework package.

Let’s think back to the AppBar and how it behaves when resized. The width of the AppBar increases to fill the maximum available space, but the height stays the same no matter how large the screen gets. The text and icons inside the AppBar don’t change either. When the screen size gets really big, this could become a problem in terms of readability and general appearance.

The Responsive Framework package can help us proportionally scale the height of the AppBar as the width increases. This doesn’t only apply to the AppBar either. When we activate scaling, all of the widgets on the screen will scale. Making them more suitable for a specific screen size as well as making the text more readable.

Setting Up Responsive Breakpoints

When to Scale and When to Resize?

Now that we understand the difference between scaling and resizing, let’s add scaling behavior to our app. First, though, we need to plan out at which screen sizes our app UI should scale. To do that we need to define breakpoints for the app and specify whether we want our app to scale or resize at those specific breakpoints. If you’re wondering why we wouldn't just have our UI scale continuously, let’s see what happens if we do that.

As you can see, if the app scales continuously, it will quickly get far too big. So, instead, we only want scaling to happen between certain breakpoints where it makes the most sense. For example, our standard UI looks perfect on small screens like cell phones. So, for smaller screens it is better to leave the default, resizing behavior and not add scaling. However, on tablets and very large screens, it may make sense to add scaling behavior. If we set our app to resize on cell phone screens and scale on tablets, it would look something like this.

This is a clip of the finished project. Here the app resizes when the screen width is less than 600px, then from 600px until 800px it scales. So, hopefully, you understand now why we would benefit from scaling in some cases and not in others.

Adding Breakpoints to the App

Knowing what we know now, let’s configure the breakpoints for the app. Head over to the main.dart file, import the Responsive Framework package, and add the following code to the builder argument of the MaterialApp.

main.dart

return MaterialApp(
  builder: (context, widget) => ResponsiveWrapper.builder(
    ClampingScrollWrapper.builder(context, widget!),
    breakpoints: const [
      ResponsiveBreakpoint.resize(350, name: MOBILE),
      ResponsiveBreakpoint.autoScale(600, name: TABLET),
      ResponsiveBreakpoint.resize(800, name: DESKTOP),
      ResponsiveBreakpoint.autoScale(1700, name: 'XL'),
    ],
  ),
...

Let’s discuss what’s happening here.

  • To configure the breakpoints and scaling behavior globally, we are returning a ResponsiveWrapper.builder from the callback function in the builder argument of the MaterialApp.
  • For the positional argument of the ResponsiveWrapper.builder we need to provide the widget that we get from the callback parameter. We are providing that widget here wrapped in a CalmpingScrollWrapper.builder. The primary reason for that is to disable the overscroll glow, which is a default effect on Android. You can also use the BouncingScrollWrapper.builder here if you prefer bouncing over clamping behavior.
  • Here we also provided a List of ResponsiveBreakpoint objects to the breakpoints argument. We used resize and autoScale named constructors depending on which behavior we want for the specific breakpoint. Our app will resize when the screen width is between 350px and 600px, and between 800px and 1700px. The app will scale when the screen width is between 600px and 800px as well as when the screen is larger than 1700px. It is completely up to you which breakpoints you set and the behavior you specify. As a general rule of thumb, though, you will likely want to have some scaling for tablets and very large screens to make sure the UI doesn’t appear too small. You can play around with the different behaviors and breakpoint values to achieve the most suitable effect.

We also specified a name for every breakpoint. These names can be used throughout the app to reference these breakpoints when configuring other features of the Responsive Framework package.

You’ll also notice that some breakpoints use constants for the names, while the last one uses a String. These constants are provided to us by the package for easy referencing, but you can also use your own String values.

Lastly, you can also set up the scaleFactor argument for the breakpoints if you want your app to be scaled by a custom amount.

When creating breakpoints, you can also use the .autoScaleDown and .tag named constructors. The .autoScaleDown constructor will have the same behavior as .autoScale, but it will also be able to scale down. The .tag constructor can be used when you don’t want to set a specific behavior and just want to create a breakpoint you can reference by name elsewhere in the app.

This is everything we are going to do inside of the main.dart file for our app. There are lots of other arguments we haven’t used here that you can customize for the ResponsiveWrapper. Here are some of the most prominent ones.

  • breakpointsLandscape - You can use this argument to specify breakpoints for when a device is in a landscape orientation.
  • landscapePlatforms - By default landscape breakpoints will only be active when the app is running on Android, iOS, or Fuchsia. To change this, you can pass in additional platforms in a List to the landscapePlatforms argument.
  • minWidth & maxWidth - You can use these arguments to set the maximum and minimum width for the entire app.
  • defaultName, defaultScale & defaultScaleFactor - You may have noticed that these arguments are similar to the ResponsiveBreakpoint arguments. That’s because they will be used by default for screen sizes which don’t have a set breakpoint. In our app we don’t have a defaultName set, but you can set one. Since we didn’t specify any custom values for the other two arguments, the defaultScale is set to false and the defaultScaleFactor is set to 1. We don’t have a breakpoint set before 350px, so before that breakpoint is reached the app’s behavior is determined by these default arguments. So, in our case the app will resize on screens smaller than 350px and not scale.
  • background & backgroundColor - You can use these arguments to set things like a background image and background color for the app. In our app we aren’t using these and even if we did it wouldn’t be visible. That’s because our Scaffold has a white background and it stretches to fill the entire screen.

Run the app in the browser now to see how these changes affect our UI. Try resizing the browser window to see when the app scales and when it resizes.

You’ll notice that when the app switches from scaling to resize, the UI snaps back from scaled to the original size. This will happen when a new breakpoint is reached. The behavior specified for that next breakpoint will start from those dimensions.

You can use this in your favor if, for example, you want the app to scale for a larger breakpoint range. Let’s say we want the app to scale from 600px to 1000px. If we only have one breakpoint for 600px set to scale and one breakpoint for 1000px set to resize, scaling will cause the app to get too large due to such a big range. To mitigate this, we can set another breakpoint at 800px and set it to scale. This will cause the UI to snap back to its original size when the screen width reaches 800px and then scale from there up to the 1000px breakpoint.

Note that when running the app in the browser, hot reload is not available. Hot restart may not always work either. So, when you are running and testing your app in the browser, you will likely have to stop the app and run it again to see the changes you made in your code take place.

Responsive Row/Column

Our app is already more responsive than it was before, but wouldn’t it be nice if the course tile Column could change to a Row on larger screens? The Responsive Framework package has a handy widget that can do just that. Head over to the courses_page.dart, import the Responsive Framework package and let’s convert the Column containing CourseTile widgets to a ResponsiveRowColumn widget.

courses_page.dart


...
ResponsiveRowColumn(
  rowMainAxisAlignment: MainAxisAlignment.center,
  rowPadding: const EdgeInsets.all(30),
  columnPadding: const EdgeInsets.all(30),
  layout: ResponsiveWrapper.of(context).isSmallerThan(DESKTOP)
      ? ResponsiveRowColumnType.COLUMN
      : ResponsiveRowColumnType.ROW,
  children: [
    ResponsiveRowColumnItem(
      rowFlex: 1,
      child: CourseTile(course: courses[0]),
    ),
    ResponsiveRowColumnItem(
      rowFlex: 1,
      child: CourseTile(course: courses[1]),
    ),
  ],
),
...

Let’s go over what we’ve done here step by step.

  • First, we are setting the rowMainAxisAlignment to center our course tiles when they are displayed in a Row.
  • Then, we are using the rowPadding and columnPadding arguments to specify padding for both states.
  • We use a ternary operator to set the layout argument dynamically depending on the screen size. We do this by checking if the current width is smaller than the DESKTOP breakpoint (800px) and if it is, then we set the layout argument to ResponsiveRowColumnType.COLUMN. If the current width is instead 800px and larger, the layout will be set to ResponsiveRowColumnType.ROW.
  • For the children argument, we provide a List of our CourseTile widgets wrapped inside of the ResponsiveRowColumnItem widgets. We also set the rowFlex argument for these widgets to to prevent overflow errors.

Go ahead and run the app again. You should now see the column change to a row when the screen size reaches 800px.

Responsive Visibility

There's another neat widget the Responsive Framework package provides us with that can help us conditionally display widgets in our app based on the breakpoint conditions we specify. To demonstrate this, let’s make certain parts of our AppBar become visible or hidden depending on the screen size. For this app we are going to show the MenuTextButton widgets when the screen is larger than the TABLET breakpoint and hide these buttons on smaller screens. When the MenuTextButton widgets are hidden, we will display a leading menu icon button instead.

First, add a leading IconButton to the AppBar. Then, wrap the IconButton with a ResponsiveVisibility widget and configure it in the following way.

courses_page.dart


...
leading: ResponsiveVisibility(
  hiddenWhen: const [
    Condition.largerThan(name: TABLET),
  ],
  child: IconButton(
    onPressed: () {},
    icon: const Icon(Icons.menu),
  ),
),
...

The ResponsiveVisibility widget has several arguments you can configure. Here we are just providing the IconButton as the child and a List with a single condition to the hiddenWhen argument. We use hiddenWhen to specify our conditions because by default the visible argument of this widget is set to true. So, this way the widget will be visible except when the screen is larger than the TABLET breakpoint. When creating conditions you can also use .equals and .smallerThan constructors instead of the .largerThan one. What you choose really depends on your personal use case.

Before we see how this looks, let’s wrap our MenuTextButton action buttons in ResponsiveVisibility widgets as well.

courses_page.dart


...
actions: [
  const ResponsiveVisibility(
    visible: false,
    visibleWhen: [
      Condition.largerThan(name: TABLET),
    ],
    child: MenuTextButton(text: 'Courses'),
  ),
  const ResponsiveVisibility(
    visible: false,
    visibleWhen: [
      Condition.largerThan(name: TABLET),
    ],
    child: MenuTextButton(text: 'About'),
  ),
...

What’s different here is that we are setting the visible argument to false and using visibleWhen to set our conditions. This makes these buttons hidden by default and only visible when the screen size is larger than the TABLET breakpoint.

Now, let’s run the app once again and see how it looks. You should now see the AppBar change based on the conditions we set.

Responsive Values

Our app transformation is almost complete. There is just one more Responsive Framework package feature I would like to show you how to implement. The package provides us with a class we can use to create an object that contains different values depending on the breakpoint conditions we set.

These values can be of any type, but for our example we are going to create a dynamic double to set a font size for our header text. Switch over to the widgets.dart file and find the PageHeader widget. There, find the Text widget that displays the ‘Our Courses’ text and change the current font size to the following.

widgets.dart


...
fontSize: ResponsiveValue(
  context,
  defaultValue: 60.0,
  valueWhen: const [
    Condition.smallerThan(
      name: MOBILE,
      value: 40.0,
    ),
    Condition.largerThan(
      name: TABLET,
      value: 80.0,
    )
  ],
).value,
...

We are only specifying  three arguments for the ResponsiveValue object here. The context, defaultValue, and valueWhen. The defaultValue will be used when no conditions are met. The valueWhen contains a List of conditions. For these conditions we specify the name of the breakpoints and the value we want when they are met.

We can’t simply provide the ResponsiveValue object to the fontSize argument, so that is why at the end we are accessing the value field to retrieve the double. With this configuration, when the screen size is smaller than the MOBILE breakpoint, the font size will be 40 and when the screen size is larger than the TABLET breakpoint it will be 80.

Go ahead and run the app one more time. You should now see the header font size change when the conditions we specified are met.

Other Package Features

We are now all done configuring our app. But before we wrap up this tutorial I would like to mention some of the Responsive Framework package features that we haven’t covered, but you may find worth exploring.

Responsive Constraints

You can use a ResponsiveConstraints widget to wrap your widgets. It will return a Container with BoxConstraints based on the conditions you specify. This Container will wrap around the widget you provide as the child for ResponsiveConstraints.

Responsive GridView

You can use the ResponsiveGridView provided by the package to create a GridView with additional grid layout controls. You can check out the package source code for more details.

Conclusion

That’s all for this tutorial! We learned how to utilize the Responsive Framework package to make Flutter apps more optimal on all kinds of screens. All of the features you learned about here can be used in a variety of ways to achieve different looks and effects. The configuration you decide on at the end will vary depending on the app you’re working on. However, no matter what the app is, what you’ve learned here should allow you to customize any project in the way you see fit.

About the author 

Ashley Novik

Ashley is a Flutter developer and tutor at Reso Coder with a passion for tech and an infinite drive to learn and teach others 😄. On her days off she enjoys exploring nature and powering through off-road trails on her mountain bike 🚵‍♀️.

You may also like

Flutter SVG Animations With Rive

Flutter SVG Animations With Rive
{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
>