Flutter Flexible Layouts
Learning Objectives
- You know of the ListView and GridView widgets in Flutter.
- You can create flexible layouts using ListView and GridView.
We previously looked into MediaQuery and LayoutBuilder, which can be used to create responsive widgets (and layouts). Here, we look into a few more widgets that are especially designed for flexible layouts: ListView and GridView.
The ListView widget allows creating scrollable content, while the GridView widget allows creating scrollable two-dimensional arrays. Both widgets can be used to create flexible layouts that adjust based on the available space. When contrasting these to CSS, ListView resembles the flex property, while GridView resembles the grid property.
List view
The ListView widget allows creating scrollable content. The following example demonstrates the use of ListView with ten cards as children. When you launch the application, depending on the size of the screen of the device you use, not all of the cards are visible at the same time. You can scroll the content e.g. by using the finger, and by scrolling the scroll wheel of the mouse.
In addition to ListView, the above example demonstrates how to create a list of widgets using the generate constructor of the Iterable class, and how to enable scrolling by mouse drag.
To summarize creating the list of widgets, the call Iterable<int>.generate(10)
creates an iterable of integers from 0 to 9. The map function is used to transform the integers to cards, which are then collected to a list using the toList
method. Nothing is actually done with the integers, this is just a trick to create ten items.
Scrolling by pressing the mouse button and dragging is disabled by default, and thus we need to enable it with a custom scroll behavior. If you’re interested in the background, see https://github.com/flutter/flutter/issues/71322 — we discuss this a bit more next.
Enabling scrolling by mouse
To enable scrolling by mouse drag, we extend the class MaterialScrollBehavior and override the method get dragDevices
to provide a list that includes PointerDeviceKind.mouse
. Note that PointerDeviceKind
is from the package dart:ui
, which needs to be imported for this to work.
import 'dart:ui';
class CustomScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}
With this in place, we can set the scrollBehavior
argument of the MaterialApp
to be an instance of our CustomScrollBehavior
that allows scrolling by mouse.
// ...
MaterialApp(
scrollBehavior: CustomScrollBehavior(),
// ...
Scroll direction
By default, ListView widgets are vertically aligned (i.e. stacked on top of each other). The direction can be changed using the scrollDirection
-argument, which takes an Axis as a value. The available values are Axis.horizontal
and Axis.vertical
.
To change the application so that the scroll direction is horizontal, we set the value of the scrollDirection
argument of the ListView to Axis.horizontal
.
ListView packs its contents. If the scroll direction is vertical, the height of the content is minimized and the width is maximized. If the scroll direction is horizontal, the height is maximized and the width is minimized.
The example below demonstrates one possible way of creating a layout for a somewhat larger application. The application also shows spacing of widgets using mainAxisAlignment
.
In practice, such an application would be decomposed into smaller widgets where each would have a specific responsibility.
Grid view
The GridView widget allows creating scrollable two-dimensional arrays. It is often used through the named constructor GridView.count that allows easy generation of a scrollable grid.
The constructor GridView.count
is given two arguments: (1) crossAxisCount
that describes the number of widgets that can be next to each others (i.e., the number of columns), and (2) children
that contains the list of widgets that are to be shown in the grid.
The following application demonstrates the use of GridView, showing a grid with nine cards, placed out in three columns (and three rows).
Changing the value given to crossAxisCount
to e.g. one would display the cards in a single column with nine rows.
Having information about the size of the screen or, depending on where used, the size of the parent widget, we can adjust the value given to the crossAxisCount
property. In the following example, we adjust the value based on the width of the screen divided by 200 (rounded down).
class GridScreen extends StatelessWidget {
final cards = Iterable<int>.generate(9)
.map(
(i) => Card(
child: Center(
child: Text(i % 2 == 0 ? "Hamburgers" : "Salads"),
),
),
)
.toList();
@override
Widget build(BuildContext context) {
int count = MediaQuery.of(context).size.width ~/ 200;
count = count < 1 ? 1: count;
return GridView.count(
crossAxisCount: count,
children: cards,
);
}
}
When we try out the above example, we observe that the number of side-by-side widgets changes based on the screen width.
The ~/
is a truncating division operator, where the result of the division is effectively floored.
Additional arguments that can be given to the GridView.count
named constructor include scrollDirection
(either Axis.vertical
or Axis.horizontal
), mainAxisSpacing
and crossAxisSpacing
(both used to control spacing between widgets), and padding
(for padding the GridView
).
To demonstrate the effect of these, we create a ColoredText
widget that shows a given text on an orange background.
class ColoredText extends StatelessWidget {
final String text;
ColoredText(this.text);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.orange,
child: Center(
child: Text(text),
),
);
}
}
The following application uses a Grid to display ten ColoredText
widgets with adjusted spacing and padding. Running the application, we see the orange containers with the texts, as well as the spacing between the containers and the padding around the gridview.
We notice, however, that the size of the font does not change with the size of the containers. Given that the text widgets are within containers, we could retrieve information about the parent containers and use that to adjust the used font size. As we now know, retrieving information about parent widgets is done using LayoutBuilder
.
The following example shows the use of LayoutBuilder to influence the size of the text shown in the ColoredText
widget. For adjusting the size of the text, we use the textScaler
property of the Text
widget, passing an instance of a TextScaler widget to it.
class ColoredText extends StatelessWidget {
final String text;
ColoredText(this.text);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Container(
color: Colors.orange,
child: Center(
child: Text(
text,
textScaler: TextScaler.linear(constraints.maxWidth / 25),
),
),
);
},
);
}
}
Custom Scaffold
Often, in applications, we wish to restrict the overall width of the content. As an example, if someone has a very wide screen and the application has no limits on width, the content can end up looking odd.
There are no definite rules on what the optimal maximum width of an application is, although there are guidelines. We will look into some of the guidelines when looking into design systems towards the end of the course.
For now, a handy trick that we can do is to create a custom scaffold that restricts the width of the content to an arbitrary number, say 960 pixels, and allowing passing a child widget to it. This makes following the same style across screens easy, as the same scaffold can be used throughout the application.
One possible implementation of such a custom scaffold is shown below.
In addition to wrapping the GridView
, a custom scaffold could also be used to decide on the layout of the application, including the possibility of having a sidebar or a bottom navigation bar — depending on the screen size, and so on.