Responsive UI in a flutter application.

Sanjib Maharjan
8 min readMay 11, 2021

--

Flutter is a cross-platform technology and one of the concern developers has is the different device sizes that the app runs. There may be more than one size to deal with for a single device. For instance, mobile devices have portrait and landscape orientations while the size of the browser can be varied. The design that worked perfectly on small devices may look worst on large and medium devices. There is hardly a design that worked perfectly for all the screen sizes. Larger screen sizes have more spaces and can accommodate more components as compared to lower ones. Sometimes there can also be the case where the placement of some components may be completely different due to space limitations.

If you are familiar with CSS, you must have ideas about different approaches such as bootstrap, media query, etc. In Flutter there are different ways such as MediaQuery, LayoutBuilder, etc. There are also widgets such as Expanded, Flexible, AspectRatio, FractionallySizedBox, OrientationBuilder, etc. Each of these widgets has its use-cases and should be used accordingly.

MediaQuery and LayoutBuilder

Both of these give us the constraints to work on and by their use, we can render the components as per the requirement. MediaQuery can be used anywhere as long as the context is available while LayoutBuilder is a widget. MediaQuery gives the actual size of the device while LayoutBuilder gives the size of the enclosing parent widget. Also, it should be noted that if the parent of LayoutBuilder is unbounded or unconstrained then it will not give the size. Let’s explore these widgets with the help of some examples and then it will be clearer.

As seen in the above examples, to use the LayoutBuilder class properly, its immediate parent must be bounded, or else it will not work properly. In case 3 of the above example, LayoutBuilder is used within a Row widget that has unbounded width, and hence LayoutBuilder cannot display the width. Similarly, if there was a Column(has unbounded height) instead of a Row widget, then maxHeight printed in the above statement would have been displayed as unbounded(Infinite).

build method: We are very much familiar with this method in Flutter where we write most of the code for design. This method is called in every frame including:

  • After calling initState. (Stateful)
  • After calling didUpdateWidget. (Stateful)
  • After receiving a call to setState. (Stateful)
  • After calling deactivate and reinserting the State object into the tree at another location. (Stateful)
  • After a dependency of this State object changes (e.g., an InheritedWidget referenced by the previous build changes).
  • Called when changing the screen sizes if MediaQuery.of(context).size is used within the widget tree

This means that this method is called when there is a change in screen size. For instance, when there is an orientation change in mobile devices or when the screen size is adjusted in a web browser. We can detect the change using MediaQuery class or LayoutBuilder widget and render the UI as per the requirement. If LayoutBuilder is used then the entire build method is not re-rendered, only the children within the LayoutBuilder are re-rendered on screen-size change. ie. Parents of the LayoutBuider are not re-rendered.

The above UI looks good on the mobile while on the web the design looks stretched and unpleasant. The solution can be “separate UIs for different sized device”, “replacement, resize or completely remove some components” etc.

Deciding how many screen sizes to consider: There are wide varieties of devices with different screen sizes and hence it will be very difficult to consider every screen size while designing. We should consider at least 3 layouts for the design :

  • Small: under 600px: assumption for mobile devices
  • Medium: 600px — 1024px: assumptions for tablets, some large phones, and small netbook-type computers.
  • Large: over 1024px: assumption for personal computers.

Using MediaQuery:

static bool isMobile(BuildContext context) =>
MediaQuery.of(context).size.width <= WIDTH_MOBILE;

Create a utility class ResponsiveUtil, some utility widgets, and also a builder class ResponsiveBuilder to be able to use ResponsiveUtil class as a widget with the following contents:

The above implementation only has 3 screen sizes considered, but we can modify it to have more intermediate values as per our requirement.

Instantiate the ResponsiveUtilityclass as:

final ResponsiveUtil _respUtil = ResponsiveUtil(context: context);

Generate the dynamic value of any type for mobile, tablet, and desktop.

_respUtil.value<double>(mobile: 80, tablet: 120, desktop: 160);_respUtil.value<Widget>(mobile: MobileWidget(), tablet: TabWidget(), desktop: DesktopWidget());_respUtil.incremental(20, factor: 10);

We can also use the default values that are defined in the responsive utility class. We can add, edit and delete the values there as per our requirements.

_respUtil.defaultGap;_respUtil.largeFontSize;

Check for different device types and render different components accordingly.

if(_respUtil.isMobile){
//mobile related logic
}else if(_respUtil.isDesktop){
//desktop related logic
}

We have to instantiate the ResponsiveUtil class ourselves to use it. Instead of that, we can also delegate the instantiation of ResponsiveUtil to a separate widget ResponsiveBuilder.

//without using a builder class
@override
Widget build(BuildContext context) {
ResponsiveUtil _responsiveUtil = ResponsiveUtil(context: context);

return Text(
_responsiveUtil.value<String>(
mobile: "mobile",
tablet:"tablet",
desktop: "desktop"
)
);
}
//using a builder class
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (responsiveUtil) {
return Text(
responsiveUtil.value<String>(
mobile: "mobile",
tablet:"tablet",
desktop: "desktop"
)
);
}
);
}

ResponsiveWidget is a widget that uses a LayoutBuilder where we can pass different widgets for different screen sizes. It uses the max-width to decide which widget to render and hence the parent of this widget must have a bounded width or else it will not work.

ResponsiveWidget(

mobile: MobileDesignWidget(),
desktop: DesktopDesignWidget(),
tablet: TabletDesignWidget()

)

We can also create some helper classes to make the responsive design easier.

RowOrColumn: We use this widget to render Row or Column according to the condition. We can use a Row to display the number of items but sometimes there may be the case when the given items do not fit in some device due to less available space. In this situation, we can use Column instead and this widget “RowOrColumn” makes this task easier.
ExpandedIf widget can be used alongside the RowOrColumn widget to handle the overflow issues.

@override
Widget build(BuildContext context) {
return Scaffold(
body: ResponsiveBuilder(
builder: (responsiveUtil) {
return RowOrColumn(
showRow: !responsiveUtil.isMobile,
columnCrossAxisAlignment: CrossAxisAlignment.center,
children: [
ExpandedIf(
expanded: !responsiveUtil.isMobile,
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 10),
child: Image.asset(
"assets/images/banner1.jpeg",
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
)),
ExpandedIf(
expanded: !responsiveUtil.isMobile,
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 10),
child: Image.asset(
"assets/images/banner2.jpeg",
height: 200,
fit: BoxFit.cover,
width: double.infinity,
),
)),
],
);
}
),
);
}

The UI renders good for web and tablet view but in mobile view, a high portion of images are not visible. Therefore in the mobile view, we can render images in a column instead of a row.

This looks a bit better than the previous implementation. Also, note that the implementation for the tablet view and the desktop view remains the same while the only mobile view is updated. We can use this technique to design more complex and separate UIs for devices of different screen sizes.

Similarly, we can use this logic to render different design variants of widgets at different positions of the application.

Different view configurations for different screen sizes: “Mobile view: Show only share and call icon, Tab view: Show icon with title, Desktop view: Show icon, title, and subtitle”

IntrinsicHeightIf widget can also be used alongside the RowOrColumn widget to give equal height to the children if need be.

PaddingSwitch: It is one of the custom widgets from the above code snippet and it can be used to have different padding values as per the device width.

PaddingSwitch(
switchIf: responsiveUtil.isDesktop,
padding: const EdgeInsets.all(2),
switchedPadding: const EdgeInsets.all(4),
child: const Text("Padding Switch"),
);
//Other Similar implementation for padding switch
Padding(
padding: EdgeInsets.all(
responsiveUtil.value<double>(mobile: 2, desktop: 4)),
child: const Text("Padding Switch"),
);
OR
Padding(
padding: responsiveUtil.value<EdgeInsets>(
mobile: const EdgeInsets.all(2),
desktop: const EdgeInsets.all(4)),
child: const Text("Padding Switch"),
);
//We can also add the value for tablet if needed

Flexible design using Flexible or Expanded or FractionallySizedBox: We can use this widget to vary the size of items for the different screen sizes. ie. we can allocate the available space by giving weights to each widget in the view.

class Expanded extends Flexible {
const Expanded({
Key? key,
int flex = 1,
required Widget child,
}) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
}

Expanded is just a Flexible widget with the fit property set to FlexFit.tight meaning the children of Expanded take the entire available space(tightly) of the parent while the children of Flexible do not. Their parent data is limited and could be used only inside the Row and Column widget.

RowOrColumn(
showRow: !_responsiveHelper.isMobile,
children: [
ExpandedIf(
expanded: !_responsiveHelper.isMobile,
child: _AppIcon(_responsiveHelper),
flex: 2,
),
ExpandedIf(
expanded: !_responsiveHelper.isMobile,
child: _LoginForm(
usernameController: _usernameController,
passwordController: _passwordController,
isPasswordVisible: _isVisible,
toggleVisibility: _toggleVisibility,
userData: _userData,
),
flex: 3,
)
],
)

The above code snippet is the portion of login UI and has a RowOrColumn widget with 2 children. The children are rendered in a Column when the width is equivalent to Mobile otherwise they use Row. Similarly, the children are rendered as Expanded with a flex of 2 and 3 respectively each getting 2/5th and 3/5th of the space available. Also, Expanded widgets are activated only for Row in this condition.

The above image is the final form of our design for this tutorial which has a separate login UI as compared to the web and tab. There is also a widget OrientationBuilder which listens for orientation changes ie changes from landscape to portrait and vice versa.

This is the end of the tutorial and If you guys have any more tips on responsive design do post them in the comment section below. You can also check the GitHub repo for this tutorial. Thank you!

#flutter, #responsive, #MediaQuery, #LayoutBuilder, #Flexible, #Expanded #responsiveflutter #responsiveuiflutter #responsivebuilder

--

--