Device-Agnostic Design

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.

Run the program to see the output

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.

Run the program to see the output

ListView and limiting space

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.


Loading Exercise...

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.

Run the program to see the output

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).

Run the program to see the output

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.

What the ~/?

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.

Run the program to see the output

Loading Exercise...

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.

Run the program to see the output

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.