
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
toRow
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 simpleAppWidget
which returns aMaterialApp
that has theCoursesPage
as thehome
argument. - lib/courses_data.dart
This file contains aCourse
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 aScaffold
with anAppBar
that has a title and some action buttons. TheScaffold
body
contains aListView
which displays thePageHeader
widget,Column
withCourseTile
widgets and aSubscribeBlock
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.
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 thebuilder
argument of theMaterialApp
. - For the positional argument of the
ResponsiveWrapper.builder
we need to provide thewidget
that we get from the callback parameter. We are providing thatwidget
here wrapped in aCalmpingScrollWrapper.builder
. The primary reason for that is to disable the overscroll glow, which is a default effect on Android. You can also use theBouncingScrollWrapper.builder
here if you prefer bouncing over clamping behavior. - Here we also provided a
List
ofResponsiveBreakpoint
objects to thebreakpoints
argument. We usedresize
andautoScale
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.
.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 thelandscapePlatforms
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 adefaultName
set, but you can set one. Since we didn’t specify any custom values for the other two arguments, thedefaultScale
is set tofalse
and thedefaultScaleFactor
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.
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 aRow
. - Then, we are using the
rowPadding
andcolumnPadding
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 theDESKTOP
breakpoint (800px) and if it is, then we set thelayout
argument toResponsiveRowColumnType.COLUMN
. If the current width is instead 800px and larger, the layout will be set toResponsiveRowColumnType.ROW
. - For the
children
argument, we provide aList
of ourCourseTile
widgets wrapped inside of theResponsiveRowColumnItem
widgets. We also set therowFlex
argument for these widgets to 1 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.
Nice Article! Thanks for the intuitive tutorial. I’ve tried to follow you on twitter but the link point to this same page. Do you mind providing the link?
Nice Article… In this specific case, If I don’t want to scale any one specific screen in the whole app, so at that time what should I use.