Developing for Multiple Screen Sizes and Orientations in Flutter (Fragments in Flutter)

Making Adaptive Screens in Google’s Flutter Mobile SDK

Mobile applications need to support a wide range of device sizes, pixel densities and orientations. Apps need to be able to scale well, handle orientation changes and persist data through all these. Flutter gives you the capability to choose the way to tackle these challenges instead of only giving one particular solution.

The Android solution for tackling larger screens

In Android, we tackle larger screens like tablets with alternate layout files which we can define for a minimum width and landscape/portrait orientation.

Meaning we have to define one layout file for phones, one for tablets and then both orientations for each device type. These layouts are then instantiated according to whichever device is running it. We then check for which layout is active(mobile/tablet) and initialise accordingly.

For most applications, a master-detail flow is used for handling larger screen sizes, which uses Fragments. We’ll go into what master-detail flow is in a while.

Fragments in Android are essentially reusable components which can be used in a screen. Fragments have their own layouts and Java/Kotlin classes to control the data and for the lifecycle of the Fragment. This is a rather large undertaking and takes a lot of code to get working.

Let us first look at handling orientation and then handling screen sizes for Flutter.

Working with orientation in Flutter

When we work with orientation, we want to use the full width of the screen and display the maximum amount of information possible.

The example below creates a rudimentary profile page in both orientations and builds the layout differently depending on the orientation to maximise the use of the width of the screen. The complete source code will be hosted on GitHub (Link given at the end of this article).

Here we have a simple screen which has different layouts for portrait as well as landscape. Let’s try to understand how we actually switch layouts in Flutter by creating the example above.

How do we go about this?

In concept, we work very similarly to the Android way of doing things. We have two layouts (not layout files, as Flutter doesn't have those), one for portrait and one for landscape orientation. When the device changes orientation we rebuild our layout.

How do we detect orientation changes?

To start out, we use a widget called OrientationBuilder. OrientationBuilder is a widget which builds a layout or part of a layout on an orientation change.

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? _buildVerticalLayout()
: _buildHorizontalLayout();
},
),
);
}

The OrientationBuilder has a builder function to build our layout. The builder function is called when the orientation changes. The possible values being Orientation.portrait or Orientation.landscape.

In this example, we check if the screen is in portrait mode and construct a vertical layout if that is the case, else we construct a horizontal layout for the screen.

_buildVerticalLayout() and _buildHorizontalLayout() are methods I’ve written to create the respective layouts.

We can also check the orientation at any point in the code (inside or outside the OrientationBuilder) using

MediaQuery.of(context).orientation

Note: For that time when we’re lazy and/or only portrait will do, use

SystemChrome.setPreferredOrientations(DeviceOrientation.portraitUp);

Creating layouts for larger screens in Flutter

When we deal with larger screen sizes, we want our screens to adapt to use the available space on the screen. The most straight-forward way to do this is simply creating two different layouts or even screens for tablets and phones. (Here, “layout” means the visual part of the screen. “Screen” refers to the layout and the all the backend code connected to it.) However this involves a lot of unnecessary code and the code needs to be repeated.

So what do we do to solve this problem?

First, let’s take a look at the most common use case of it.

Let’s go back to the “Master-Detail Flow” we were going to talk about. When it comes to apps, you will see a common pattern where you have a Master list of items and when you click on a list item, you get redirected to a different Detail screen. Take the example of Gmail, where we have a list of emails and when we click on one, a detail view opens with the content of the mail.

Let’s take make an example app for this flow.

Master-Detail Flow in mobile portrait mode

This app simply holds a list of numbers and displays a number prominently when tapped on. We have a master list of numbers and a detail view that displays a number when clicked. Just like the emails.

If we used the same layout in tablets, it would be a rather large waste of space. So what can we do to solve it? We can have both the master list and detail view on the same screen as we have the available screen space.

Master-Detail Flow in tablet landscape mode

So what can we do to reduce the work of writing two separate screens?

Let’s see how Android tackles this. Android creates reusable components called Fragments out of the master list and the detail view. A Fragment can be defined separately from the screen and just added into the screen without repeating the code twice.

So Fragment A is the master list fragment and B is the detail fragment. In mobiles or smaller width layouts, a click on a list item would navigate to a separate page whereas in tablets it would remain on the same page and change the detail fragment. We can also do a tablet-like interface when a phone is rotated to landscape.

This is where the power of Flutter comes in.

Every widget in Flutter is by nature, reusable.

Every widget in Flutter is like a Fragment.

All we need to do is define two widgets. One for the master list, one for the detail view. These are, in effect, fragments. We simply check if the device has enough width to handle both the list and detail part. If it does, we use both widgets. If the device does not have enough width to support both, we only show the list and navigate to a separate screen to show the detail content.

We first need to check the device’s width to see if we can use the larger layout instead of the smaller one. To get the width, we use

MediaQuery.of(context).size.width

The Size gives us the height and width of the device in dps.

Let’s set the minimum width to 600 dp for switching to the second layout.

Summing up:

  1. We create two widgets, one containing the master list and one containing the detail view.
  2. We create two screens. On the first screen, we check if the device has enough width to handle both widgets.
  3. If there is enough width, we add both widgets on one page. If there is not, we navigate to a second page when a list item is tapped which only has a detail view.

Let’s code it

Let’s code the demo that I’ve included at the top of this section where we have a list of numbers and the detail view displays that number. First we make two widgets.

The List Widget (List Fragment)

typedef Null ItemSelectedCallback(int value);

class ListWidget extends StatefulWidget {
final int count;
final ItemSelectedCallback onItemSelected;

ListWidget(
this.count,
this.onItemSelected,
);

@override
_ListWidgetState createState() => _ListWidgetState();
}

class _ListWidgetState extends State<ListWidget> {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.count,
itemBuilder: (context, position) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: InkWell(
onTap: () {
widget.onItemSelected(position);
},
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(position.toString(), style: TextStyle(fontSize: 22.0),),
),
],
),
),
),
);
},
);
}
}

In the list, we take how many items we want to display as well as a callback when an item is clicked. This callback is important as it decides whether to simply change the detail view on a larger screen or navigate to a different page on a smaller screen.

We simply display cards for each index and surround it with an InkWell to respond to taps.

The Detail Widget (Detail Fragment)

class DetailWidget extends StatefulWidget {

final int data;

DetailWidget(this.data);

@override
_DetailWidgetState createState() => _DetailWidgetState();
}

class _DetailWidgetState extends State<DetailWidget> {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(widget.data.toString(), style: TextStyle(fontSize: 36.0, color: Colors.white),),
],
),
),
);
}
}

The Detail Widget simply takes a number and displays it prominently.

Notice that these are not screens. These are simply widgets we are going to use on the screens.

The Main Screen

class MasterDetailPage extends StatefulWidget {
@override
_MasterDetailPageState createState() => _MasterDetailPageState();
}

class _MasterDetailPageState extends State<MasterDetailPage> {
var selectedValue = 0;
var isLargeScreen = false;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: OrientationBuilder(builder: (context, orientation) {

if (MediaQuery.of(context).size.width > 600) {
isLargeScreen = true;
} else {
isLargeScreen = false;
}

return Row(children: <Widget>[
Expanded(
child: ListWidget(10, (value) {
if (isLargeScreen) {
selectedValue = value;
setState(() {});
} else {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return DetailPage(value);
},
));
}
}),
),
isLargeScreen ? Expanded(child: DetailWidget(selectedValue)) : Container(),
]);
}),
);
}
}

This is the main page of the app. We have two variables: selectedValue for storing the selected list item, and isLargeScreen is a simple boolean which stores if the screen is large enough to display both the list and detail widgets.

We also have an OrientationBuilder around it, so that if a mobile phone is rotated to landscape mode and it has enough width to display both elements, then it will rebuild in that way.

We first check if the width is large enough to display our layout using

if (MediaQuery.of(context).size.width > 600) {
isLargeScreen = true;
} else {
isLargeScreen = false;
}

The main part of the code is:

isLargeScreen ? Expanded(child: DetailWidget(selectedValue)) : Container(),

If the screen is large, we add a detail widget, and if it is not, we return an empty container. We use the Expanded widgets around it to fill the screen or divide the screen into proportions in case of a larger screen. So Expanded allows each widget to fill half of the screen or even a certain percentage by setting the Flex property.

The second important part is:

if (isLargeScreen) {
selectedValue = value;
setState(() {});
} else {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return DetailPage(value);
},
));
}

Meaning, if the larger layout is used, we don’t need to go to a different screen as the detail widget is on the page itself. If the screen is smaller, we need to navigate to a different page as only the list is displayed on the current screen.

And finally,

The Detail Page (For smaller screens)

class DetailPage extends StatefulWidget {

final int data;

DetailPage(this.data);

@override
_DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: DetailWidget(widget.data),
);
}
}

It only holds one detail widget on the page, and is used for displaying the data on smaller screens.

Now we have a functioning app that adapts to screens of different sizes and their orientation.

Some more important things

  1. If you want to simply have different layouts and not have any Fragment-like layouts, you can just simply write inside the build method
if (MediaQuery.of(context).size.width > 600) {
isLargeScreen = true;
} else {
isLargeScreen = false;
}
return isLargeScreen? _buildTabletLayout() : _buildMobileLayout();

And write two methods to build your layouts.

2. If you want to have a design for tablets only, instead of checking for width from MediaQuery, get the size and use it to get actual width instead of width in that specific orientation. When we used the width from MediaQuery directly, it will get the get the width in that orientation only. So in landscape mode, the length of the phone is considered width.

Size size = MediaQuery.of(context).size;
double width = size.width > size.height ? size.height : size.width;

if(width > 600) {
// Do something for tablets here
} else {
// Do something for phones
}

Github Link for the samples in this article:

https://github.com/deven98/FlutterAdaptiveLayouts

That’s it for this article! I hope you enjoyed it and leave a few claps if you did. Follow me for more Flutter articles and comment for any feedback you might have about this article.

Some of my other articles