Responsive design at different levels in Flutter

Wenkai Fan
ITNEXT
Published in
3 min readApr 14, 2021

--

Responsive design is very important for those who want to target different platforms using a single code base. This is especially true for Flutter development since Flutter targets all major platforms.

How to do responsive design?

Responsive design in Flutter can be achieved in many ways. The simplest way is to get the current screen info using the MediaQuery widget:

Size screenSize = MediaQuery.of(context).size;
Orientation orientation = MediaQuery.of(context).orientation;

Then you build your widget differently based on that information.

Or you can use a package that simplifies this process for you. They typically provide you with an interface like:

ResponsiveBuilder(
builder: (context, sizingInformation) {
// Check the sizing information here and return your UI
if (sizingInformation.deviceScreenType == DeviceScreenType.desktop) {
return Container(color:Colors.blue);
}

if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) {
return Container(color:Colors.red);
}

if (sizingInformation.deviceScreenType == DeviceScreenType.watch) {
return Container(color:Colors.yellow);
}

return Container(color:Colors.purple);
},
},
);
}

Or provide you with some predefined screen types:

ScreenTypeLayout.builder(
mobile: (BuildContext context) => Container(color:Colors.blue),
tablet: (BuildContext context) => Container(color:Colors.yellow),
desktop: (BuildContext context) => Container(color:Colors.red),
watch: (BuildContext context) => Container(color:Colors.purple),
);

(The above code samples are from the responsive_builder package).

A different interface inspired by MaterialStateProperty

We want to achieve good flexibility in writing responsive code. Sometimes we just want a single value (like a crossAxisCount integer in GridView) to be flexible. Sometimes we want complete different UI designs for different screens (e.g. TabBarView for mobile and Row for desktop). So we should be able to make either an integer or a Widget responsive, without writing if/switch statements over and over. Also, people hold different opinions on where to set the breakpoints. So we should let people define their own breakpoints with ease.

The built-in Material components (like the ElevatedButton) change their appearance based on whether it is pressed, hovered, or selected. Those components do not use a single Color instance for their background color. Rather, they use MaterialStateProperty which will generate different Color values based on the internal state of the component.

property?.resolve(_states)

What if we use a similar interface for responsive purposes?

A state would now be some screen configuration we want to target. I name it the ScreenScope:

class ScreenScope {
final double minWidth;
final double maxWidth;
final double minHeight;
final double maxHeight;
final Orientation? orientation;
...
}

I have provided some predefined ScreenScopes:

mobileScreenScope (0px - 480px width)
tabletScreenScope (480px - 840px width)
desktopScreenScope (840px - width)

mobilePortraitScreenScope (0px - 480px width, portrait)
tabletPortraitScreenScope (480px - 840px width, portrait)
desktopPortraitScreenScope (840px - width, portrait)

mobileLandscapeScreenScope (0px - 840px width, landscape)
tabletLandscapeScreenScope (840px - 1200px width, landscape)
desktopLandscapeScreenScope (1200px - width, landscape)
ScreenScopes with 2 breakpoints and different orientations

If you use just one breakpoint:

smallScreenScope (0px - 600px width)
bigScreenScope (600px - width)

smallPortraitScreenScope (0px - 600px width, portrait)
bigPortraitScreenScope (600px - width, portrait)

smallPortraitScreenScope (0px - 1000px width, landscape)
bigPortraitScreenScope (1000px - width, landscape)

Then you declare a Responsive instance and get the actual value by calling resolve on it.

GridView.count(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: Responsive({
mobileScreenScope: 2,
tabletScreenScope: 4,
desktopScreenScope: 6
}).resolve(context)!,
children: List.generate(
30,
(index) =>
Container(color: Colors.green, child: Text("SOME TEXT"))),
);
A responsive grid

If you want to use different widgets for different screen:

Widget widget = Responsive({
mobileScreenScope: mobileWidget,
tabletScreenScope: tabletWidget,
desktopScreenScope: desktopWidget,
}).resolve(context);

I have also created a helper Widget called ScreenBuilder that can save you some time:

ScreenBuilder(
mobile: mobile,
tablet: tablet,
desktop: desktop,
);

ScreenBuilder.builder(
mobileBuilder: mobileBuilder,
tabletBuilder: tabletBuilder,
desktopBuilder: desktopBuilder,
)

The builder syntax uses a WidgetBuilder so you have access to the BuildContext.

Responsive wiget builder

Conclusion

That’s it for the responsive_property package. The goal is simple: flexibility for responsive design while saving you from writing if statements. If you have any suggestions or find any bugs, feel free to open an issue on Github. Thank you!

--

--

Ph.D. in Nuclear Physics, M.S. in Computer Science at Duke University, Flutter lover