Containers, Expanded, and Spacer widgets

  1. Objectives
  2. Code along
  3. Boilerplate
  4. The Container widget
    1. Adding borders to a Container
    2. A word on Container performance
  5. Controlling widget size within Rows and Columns - Expanded and Spacer
    1. Grow a child the Expanded widget
      1. Flexing
    2. Keep ‘em separated with the Spacer widget
      1. Flexing
    3. Combining Expanded and Spacer widgets
  6. Other resources

Objectives

Code along

Recording link: https://youtu.be/fmZwDQghlp8

Boilerplate

Create a new Flutter project and copy the code below into lib/main.dart to get started:

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
      title: "Containers and Positioning", home: HomeScreen()));
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: const Column(children: [
          Row(
            children: [
              TealBox(),
              TealBox(),
              TealBox(),
            ],
          ),
          Row(
            children: [
              TealBox(),
              TealBox(),
              TealBox(),
            ],
          ),
        ]));
  }
}

class TealBox extends StatelessWidget {
  const TealBox({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
        width: 48,
        height: 48,
        color: Colors.teal,
        margin: const EdgeInsets.all(8),
        child: const Icon(Icons.beach_access));
  }
}

Your app should look like this to start:

starting teal containers

The Container widget

The Container widget is a structural widget to help you position, stylize, and size other widgets. Container widgets have a child that the container gives you more control over.

Think of Containers as a general purpose widget for controlling the style and position of a rectangle on the screen. They are a useful tool. If you are familiar with web development, think of Container widgets as the equivalent of a <div>.

Container widgets have several useful properties (which will be familiar if you know web development):

diagram of borders, padding, and margin

The EdgeInsets widget gives you substantial control over the padding and margin. You can specify EdgeInsets to all sides, to one side, symmetrically (top-bottom, left-right), or to each side with a different value.

Try the following changes to the TealBox widget.

  1. Change the margin so that it only has symmetrical left-right margins. Try margin: const EdgeInsets.symmetric(horizontal: 16). Notice how you have double the margin between containers now.

    teal containers with symmetric margins

  2. Maybe you only want a margin on the leading edge. Try `const EdgeInsets.only(left: 16),’
  3. Okay, but now you want to add some space above and to the left of the Container. Try const EdgeInsets.only(top: 16, left: 16),
  4. Looks good. Let’s add some Padding. padding: const EdgeInsets.all(24),. What happened? The width and height properties of the Container are constraining the size of the box, so the Padding is giving us some unintended effects.

    teal containers with fixed size and unruly padding

  5. Delete the width and height properties. Now what happens?

The Flutter container will resize itself to honor the actual size, the margin, and the padding around its child iff no width or height properties are specified.

Your app should now look like this:

teal containers with padding

Adding borders to a Container

A Container’s borders are invisible by default. We can make them visible and style them by providing a BoxDecoration widget to the Container’s decoration property.

BoxDecoration has many properties, but not all of them can be used at the same time.

Try the following in order in the TealBox widget:

  1. First, comment out color: Colors.teal, on your Container. You cannot specify both a color property and the decoration property on a Container or you will get an exception.
  2. Add the parameter decoration: BoxDecoration(color: Colors.teal),. The boxes should look the same as before.
  3. Add border: Border.all(width:3), to the BoxDecoration. You should now see the border.
    • You can specify on which sides you want the border. For example border: Border(bottom: BorderSide(width: 3)) will place only a bottom border. You can specify any combination of top, bottom, left, or right.
  4. You can round the Container’s corners by adding borderRadius: BorderRadius.circular(24), to the BoxDecoration. Vary the numeric value to see the effects.

    rounded rectangular containers

  5. Maybe you want your Containers to have a shadow. This is a little more complicated. Add the following property to your BoxDecoration:
           boxShadow: const [
             BoxShadow(
               color: Colors.yellowAccent,
               blurRadius: 8,
               spreadRadius: 3,
             )
           ]
    

    Note that boxShadow takes a list of BoxShadow objects – you can specify more than one shadow.

  6. Maybe you like your Containers circular instead of rectangular. Comment out the borderRadius property and add shape: BoxShape.circle,

In the end, you should have something resembling this:

rounded teal containers

A word on Container performance

Containers are a very customizable, general purpose widget. You can wrap them with a GestureDecorator to achieve interactivity a well.

This flexibility comes at a cost – it takes a bit more time to construct and draw Container elements under the hood. You will likely not notice the difference unless you have many containers on the screen, and they are being redrawn many times in response to user interaction.

So while Containers can get you the rectangle you want, it is better from a performance perspective to use other built-in widgets if they suit your needs:

Controlling widget size within Rows and Columns - Expanded and Spacer

Rows and Columns are a strong basis for arranging widgets. Sometimes we want to size the widgets inside of a Row or Column to have a size that is proportional to its siblings. Or we may want to have one widget larger than the others, or fill all the “extra” space in our row or column.

  1. First, let’s simplify our TealBox again. Comment out the borderRadius, shape, and boxShadow properties.
  2. Set the padding property to a smaller value, like EdgeInsets.all(8).

Grow a child the Expanded widget

Note: Our example uses Rows, but everything here applies to Columns as well.

Now, go to the HomeScreen widget. Suppose we want the left-most widget in the top row to be as large as possible while still leaving room for the other boxes in the row.

  1. Wrap the first TealBox() with an Expanded widget like so: Expanded(child: TealBox()),. What happens?
    • Why is the right-most widget abutting the edge of the screen? Because we only have left and top margins on those widgets.
  2. Try wrapping the last TealBox in the first row in Expanded as well. What happens?
  3. Now add Expanded to the middle box. What happens?

In general, the Expanded tells a Widget to grow to fill as much space as possible. If multiple widgets are Expanded, they will divide the space evenly.

Flexing

You can tell widgets to Expand and control the proportion of each widget using the flex property. Replace your first Row with the following:

          Row(
            children: const [
              Expanded(flex: 4, child: TealBox()),
              Expanded(flex: 2, child: TealBox()),
              Expanded(flex: 2, child: TealBox()),
            ],
          ),

You should see something like the following: TealBoxes expanded proportionally

What is going on? Flutter sums up all the flex values of the Expanded widgets, in this case 4+2+2=8. Flutter divides the Row into 8 equally-sized cells, like below:

Flex grid

Flutter draws each widget over flex # of cells. So the first box gets 4 cells, and the other two get 2 cells each.

You can set the flex values to be any integers you want. The general formula that describes the size of a flexed widget is remainingSpace * (flex / totalOfAllFlexValues). The default flex value is 1. This flex model is similar to the grid model of popular web styling frameworks such as Bootstrap.

Try changing the flex values to 3, 1, 2 for the first row’s children and observe the effects.

The Expanded widget and its flex values will scale to different screen sizes. To test this, try creating a different phone with your emulator, or use Chrome as a target if you have Chrome installed.

Keep ‘em separated with the Spacer widget

In a previous lab, we discussed the mainAxisAlignment property of Rows and Columns and how it can be used to arrange child widgets. Specifically, the spaceEvenly, spaceBetween, and spaceAround options distribute the whitespace around a Row/Column’s children in specific pre-determined patterns. But, maybe you want a different pattern, like the one below:

Layout needing a Spacer

When you want more control over whitespace in a Row/Column, we use a Spacer widget. Replace the second Row in HomeScreen with the following:

Row(
    children: const [
        TealBox(),
        Spacer(),
        TealBox(),
        TealBox(),
    ],
),

The Spacer widget is similar to the Expanded widget, except that the Spacer expands the blank space rather than a child widget. You have added a transparent Spacer widget that is filling the space:

Spacer illustration

Place a second Spacer() between the second and third TealBoxes. Does this look familiar? It is exactly the same as specifying mainAxisAlignment: MainAxisAlignment.spaceBetween, on the Row! (Note: There is some extra whitespace to the left of the first box because the boxes have a left margin.)

Spacer illustration

By default, multiple Spacer widgets in a Row/Column will divide the whitespace evenly among themselves.

Flexing

Spacer widgets use the same “flex” rules as Expanded widgets. The default flex value is 1, which is why multiple Spacers divide the whitespace evenly.

Set the flex: 2 on the first Spacer, and flex: 1 on the second Spacer. You have divided the whitespace into 3 equally-sized cells, and each Spacer gets “flex” number of cells.

Spacer flexing illustration

Combining Expanded and Spacer widgets

The next logical question is, can you combine Expanded and Spacer widgets to achieve fancy geometric layouts? The answer is “Yes!”

Suppose that we want to have a widget on the left side that’s as wide as possible. Then we want a lot of space, then two smaller widgets on the right side with only a little space between them. Change your second row to the following:

Row(
    children: const [
        Expanded(flex: 3, child: TealBox()),
        Spacer(flex: 2),
        TealBox(),
        TealBox(),
    ],
),

Spacer plus Expanded example

The flex values of all Spacer and Expanded widgets within a Row/Column are shared. Remember, Flutter uses flex values to determine what to do with the whitespace left over in a Row/Column after drawing visible children.

Flutter sums up the flex values (3+2=5), divides the whitespace into 5 cells, then assigns 3 cells to the Expanded widget and 2 cells to the Spacer widget. Note that we still have some left-side margin on the TealBoxes.

Spacer plus Expanded grid

Other resources