Get unlimited access to all of programming knowledges for less than 30 min
Chapter 6 Laying Things Out

Chapter 6

Laying Things Out

IN THIS CHAPTER

check Putting widgets where you want them

check Dealing with common layout problems

check Working with various screen sizes

According to folklore, the size of a fish tank determines the sizes of the goldfish in the tank. A goldfish in a small tank can be only one or two inches long, but the same goldfish in a larger tank grows to be ten inches long. It’s as if a fish’s cells sense the boundaries of the fish’s living space, and the cells stop growing when they feel that doing so would be impractical.

Several online resources say that the tank size phenomenon is a myth, but that doesn’t stop me from comparing it with Flutter layouts. (Nothing stops me from making comparisons with Flutter layouts.)

In a Flutter layout, widgets are nested inside of other widgets. The outer widget sends a constraint to the inner widget:

“You can be as wide as you want, as long as your width is between 0 and 400 density-independent pixels.”

Later on, the inner widget sends its exact height to the outer widget:

“I’m 200 density-independent pixels wide.”

The outer widget uses that information to position the inner widget:

“Because you’re 200 density-independent pixels wide, I’ll position your left edge 100 pixels from my left edge.”

Of course, this is a simplified version of the true scenario. But it’s a useful starting point for understanding the way Flutter layouts work. Most importantly, this outer/inner communication works its way all along an app’s widget chain.

Imagine having four widgets. Starting from the outermost widget (such as the Material widget), call these widgets “great-grandmother”, “grandmother”, “mother”, and “Elsie.” Here’s how Flutter decides how to draw these widgets:

  1. Great-grandmother tells grandmother how big she (grandmother) can be.
  2. Grandmother tells mother how big she (mother) can be.
  3. Mother tells Elsie how big she (Elsie) can be.
  4. Elsie decides how big she is and tells mother.
  5. Mother determines Elsie’s position, decides how big she (mother) is, and then tells grandmother.
  6. Grandmother determines mother’s position, decides how big she (grandmother) is, and then tells great-grandmother.
  7. Great-grandmother determines mother’s position and then decides how big she (great-grandmother is).

Yes, the details are fuzzy. But it helps to keep this pattern in mind as you read about Flutter layouts.

The Big Picture

Listings 6-1 and 6-2 introduce a handful of Flutter layout concepts, and Figure 6-1 shows what you see when you run these listings together.

LISTING 6-1 Reuse This Code

// App06Main.dart
 
import 'package:flutter/material.dart';
 
import 'App0602.dart'; // Change this line to App0605, App0606, and so on.
 
void main() => runApp(App06Main());

class App06Main extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: _MyHomePage(),
);
}
}
 
class _MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: Colors.grey[400],
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20.0,
),
child: buildColumn(context),
),
);
}
}
 
Widget buildTitleText() {
return Text(
"My Pet Shop",
textScaleFactor: 3.0,
textAlign: TextAlign.center,
);
}
 
Widget buildRoundedBox(
String label, {
double height = 88.0,
}) {
return Container(
height: height,
width: 88.0,
alignment: Alignment(0.0, 0.0),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.all(
Radius.circular(10.0),
),
),
child: Text(
label,
textAlign: TextAlign.center,
),
);
}

LISTING 6-2 A Very Simple Layout

// App0602.dart
 
import 'package:flutter/material.dart';
 
import 'App06Main.dart';
 
Widget buildColumn(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildTitleText(),
SizedBox(height: 20.0),
buildRoundedBox(
"Sale Today",
height: 150.0,
),
],
);
}

Snapshot of a sale at My Pet Shop.

FIGURE 6-1: A sale at My Pet Shop.

Crossreference The code in Listing 6-1 refers to code in Listing 6-2, and vice versa. As long as these two files are in the same Android Studio project, running the app in Listing 6-1 automatically uses code from Listing 6-2. This works because of the import declarations near the top of each of the listings. For info about import declarations, refer to Chapter 4.

Listings 6-1 and 6-2 illustrate some coding concepts along with a bunch of useful Flutter features. I cover these in the next several sections.

Creating bite-size pieces of code

In Listings 6-1 and 6-2, I create some of the widgets by making method calls.

child: buildColumn(context),
 
// … And elsewhere, …
 
Column(
// … Blah, blah, …
children: <Widget>[
buildTitleText(),
SizedBox(height: 20.0),
buildRoundedBox(
// … Etc.

Each method call takes the place of a longer piece of code — one that describes a particular widget in detail. I create these methods because doing so makes the code easier to read and digest. With a glance at Listing 6-2, you can tell that the Column consists of title text, a sized box, and a rounded box. You don’t know any of the details until you look at the buildTitleText and buildRoundedBox method declarations in Listing 6-1, but that’s okay. With the code divided into methods this way, you don’t lose sight of the app’s overall outline.

In the design of good software, planning is essential. But sometimes your plans change. Imagine this scenario: You start writing some code that you believe will be fairly simple. After several minutes (or, sometimes, several hours), you realize that the code has become large and unwieldy. So you decide to divide the code into methods. To do this, you can take advantage of one of Android Studio’s handy refactoring features. Here’s how it works:

  1. Start with a constructor call that you want to replace with your own method call.

    For example, you want to replace the Text constructor call in the following code snippet:

    children: <Widget>[
    Text(
    "My Pet Shop",
    textScaleFactor: 3.0,
    textAlign: TextAlign.center,
    ),
    SizedBox(height: 20.0),

  2. Place the mouse cursor on the constructor call’s name.

    For the snippet in Step 1, click on the word Text.

  3. On Android Studio’s main menu, select Refactor ⇒   Extract ⇒   Method.

    As a result, Android Studio displays the Extract Method dialog box.

  4. In the Extract Method dialog box, type a name for your new method.

    For a constructor named Text, Android Studio suggests the method name buildText. But, to create Listings 6-1 and 6-2, I made up the name buildTitleText.

  5. In the Extract Method dialog box, press Refactor.

    As if by magic, Android Studio adds a new method declaration to your code and replaces the original widget constructor with a call to the method.

    The new method’s return type is whatever kind of widget your code is trying to construct. For example, starting with the code in Step 1, the method’s first two lines might look like this:

    Text buildTitleText() {
    return Text(

  6. Do yourself a favor and change the type in the method’s header to Widget.

    Widget buildTitleText() {
    return Text(

    Every instance of the Text class is an instance of the Widget class, so this change doesn’t do any harm. In addition, the change adds a tiny bit of flexibility that may eventually save you some mental energy. Maybe later, you decide to surround the method’s Text widget with a Center widget.

    // Baby, you’re no good . . .
    Text buildTitleText() {
    return Center(
    child: Text(

    After you make this change, your code is messed up because the header’s return type is inaccurate. Yes, every instance of the Text class is an instance of the Widget class. But, no, an instance of the Center class isn’t an instance of the Text class. Your method returns an instance of Center, but the method’s header expects the method to return an instance of Text. Don’t you wish you had changed the first word in the header to Widget? Do it sooner rather than later. That way, you won’t be distracted when you’re concentrating on making changes in the method’s body.

Creating a parameter list

In Listing 6-1, the header of the buildRoundedBox declaration looks like this:

Widget buildRoundedBox(
String label, {
double height = 88.0,
})

The method has two parameters: label and height.

  • The label parameter is a positional parameter.

    It’s a positional parameter because it’s not surrounded by curly braces. In a header, all the positional parameters must come before any of the named parameters.

  • The height parameter is a named parameter.

    It’s a named parameter because it’s surrounded by curly braces.

    In a call to this method, you can omit the height parameter. When you do, the parameter’s default value is 88.0.

With these facts in mind, the following calls to buildRoundedBox are both valid:

buildRoundedBox( // Flutter style guidelines recommend having a
"Flutter", // trailing comma at the end of every list.
height: 1000.0, // It's the comma after the height parameter.
)
 
buildRoundedBox("Flutter") // In the method header, the height parameter
// has the default value 88.0.

Here are some calls that aren’t valid:

buildRoundedBox( // In a function call, all positional parameters
height: 1000.0, // must come before any named parameters.
"Flutter",
)
 
buildRoundedBox(
label: "Flutter", // The label parameter is a positional parameter,
height: 1000.0, // not a named parameter.
)
 
buildRoundedBox( // The height parameter is a named parameter,
"Flutter", // not a positional parameter.
1000.0,
)
 
buildRoundedBox() // You can't omit the label parameter, because
// the label parameter has no default value.

Crossreference For info about positional parameters and named parameters, refer to Chapter 3. For the basics on declaring functions, refer to Chapter 4.

Crossreference In Listing 6-2, the declaration of buildColumn has a BuildContext parameter. You may ask, “What good is this BuildContext parameter? The body of the buildColumn method makes no reference to this parameter’s value.” For an answer, see the last section of this chapter.

Living color

Chapter 5 introduces Flutter’s Colors class with basic things like Colors.grey and Colors.black. In fact, the Colors class provides 12 different shades of grey, 7 shades of black, 28 shades of blue, and a similar variety for other colors. For example, the shades of grey are named Colors.grey[50] (the lightest), Colors.grey[100], Colors.grey[200], Colors.grey[300], and so on, up to Colors.grey[900] (the darkest). You can’t put arbitrary numbers inside the brackets, so things like Colors.grey[101] and Colors.grey[350] simply don’t exist. But one shade — Colors.grey[500] — is special. You can abbreviate Colors.grey[500] by writing Colors.grey without having a number in brackets.

If you want extra-fine control over the look of your app, you can use Flutter’s Color.fromRGBO constructor. (That’s Color singular, as opposed to Colors plural.) The letters RGBO stand for Red, Green, Blue, and Opacity. In the constructor, the values of Red, Green, and Blue range from 0 to 255, and the value of Opacity ranges from 0.0 to 1.0. For example, Color.fromRGBO(255, 0, 0, 1.0) stands for completely opaque Red. Table 6-1 has some other examples:

TABLE 6-1 Sample Parameters for the Color.fromRGBO Constructor

Parameter List

What the Parameter List Means

(0, 255, 0, 1.0)

Green

(0, 0, 255, 1.0)

Blue

(255, 0, 255, 1.0)

Purple (equal amounts of Red and Blue)

(0, 0, 0, 1.0)

Black

(255, 255, 255, 1.0)

White

(190, 190, 190, 1.0)

Grey (approximately 75% whiteness)

(255, 0, 0, 0.5)

50% transparent Red

(255, 0, 0, 0.0)

Nothing (complete transparency, no matter what the Red, Green, and Blue values are)

On the web To find out about other options for describing colors, visit Flutter’s Color class documentation page:

https://api.flutter.dev/flutter/dart-ui/Color-class.html

Adding padding

Flutter’s Padding widget puts some empty space between its outermost edge and its child. In Listing 6-1, the code

Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20.0,
),
child: buildColumn(context),

surrounds the buildColumn call with 20.0 units of empty space on the left and the right. (Refer to Figure 6-1.) With no padding, the column would touch the left and right edges of the user’s screen, and so would the white Sale Today box inside the column. That wouldn’t look nice.

In Flutter, a line such as horizontal: 20.0 stands for 20.0 density-independent pixels. A density-independent pixel (dp) has no fixed size. Instead, the size of a density-independent pixel depends on the user’s hardware. In particular, every inch of the user’s screen is roughly 96 dp long. That makes every centimeter approximately 38 pixels long. According to Flutter’s official documentation, the rule about having 96 dp per inch “may be inaccurate, sometimes by a significant margin.” Run this section’s app on your own phone, and you’ll see what they mean.

In Flutter, you describe padding of any kind by constructing an EdgeInsets object. The EdgeInsets.symmetric constructor in Listing 6-1 has one parameter — a horizontal parameter. In addition to the horizontal parameter, an EdgeInsets.symmetric constructor can have a vertical parameter, like so:

Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20.0,
vertical: 10.0,
)

A vertical parameter adds empty space on the top and bottom of the child widget.

Table 6-2 lists some alternatives to the EdgeInsets.symmetric constructor.

TABLE 6-2 EdgeInsets Constructor Calls

Constructor Call

How Much Blank Space Surrounds the Child Widget

EdgeInsets.all(20.0)

20.0 dp on all four sides

EdgeInsets.only(

left: 15.0,

top: 10.0,

)

15.0 dp on the left

10.0 dp on top

EdgeInsets.only(

top: 10.0,

right: 15.0,

bottom: 15.0,

)

10.0 dp on top

15.0 dp on the right

15.0 dp on the bottom

EdgeInsets.fromLTRB(

5.0,

10.0,

3.0,

2.0,

)

5.0 dp on the left

10.0 dp on top

3.0 dp on the right

2.0 dp on the bottom

When I started working on the code in Listing 6-1, the listing had no Padding widget. The call to buildColumn was a direct descendant of the Material widget:

return Material(
color: Colors.grey[400],
child: buildColumn(context),
);

I used the Alt+Enter trick from Chapter 3 to surround the buildColumn call with the new Padding widget. When I did this, Android Studio also added its own const EdgeInsets code. I tinkered with Android Studio’s code a bit, but I didn’t remove the code’s const keyword. For the inside story on Dart’s const keyword, see Chapter 7.

Remember The Padding widget adds blank space inside of itself. To add space outside of a widget, see the section “Your friend, the Container widget,” later in this chapter.

Your humble servant, the Column widget

Think about it: Without Flutter’s Column widget, you wouldn’t be able to position one widget above another. Everything on a user’s screen would be squished into one place. The screen would be unreadable, and no one would use Flutter. You wouldn’t be reading this book. I wouldn’t earn any royalties. What an awful world it would be!

The Column widget in Listing 6-2 has two properties related to alignment:

Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
// … And so on.

The mainAxisAlignment property comes up in Chapter 3. It describes the way children are positioned from the top to the bottom of the column. With MainAxisAlignment.center, children gather about halfway down from the top of the screen. (Refer to Figure 6-1.) In contrast, the crossAxisAlignment describes how children are situated from side to side within the column. (See Figure 6-2.)

Snapshot of a Flutter book containig a drawing.

FIGURE 6-2: Every Flutter book contains a drawing like this.

A column’s crossAxisAlignment can make a big difference in the way the column’s children appear on the screen. For example, if you comment out the crossAxisAlignment line in Listing 6-2, you see the screen shown in Figure 6-3.

Snapshot of the user don't stretch the Sale Today box.

FIGURE 6-3: When you don’t stretch the Sale Today box.

In Listing 6-2, the CrossAxisAlignment.stretch value tells the column that its children should fill the entire cross axis. This means that, regardless of the children’s explicit width values, children shrink or widen so that they run across the entire column. If you don’t believe me, try the following experiment:

  1. Run the code in Listing 6-1.

    Use the iPhone simulator, the Android emulator, or a real physical phone. Start with the device in portrait mode, as in Figure 6-1.

  2. Turn the device sideways so that the device is in landscape mode.

    If you’re running a virtual device, press Command-right arrow (on a Mac) or Ctrl+right arrow (on Windows). If you’re running a physical device, turn the darn thing sideways.

  3. Observe the change in the size of the Sale Today box.

    No matter how wide the screen is, the Sale Today box stretches almost all the way across. The width: 88.0 setting in Listing 6-1 has no effect.

You can read more about axis alignments in the sections that follow.

Tip When you turn a device sideways, the device might not switch between portrait and landscape modes. This is true for both physical devices (real phones and tablets) and virtual devices (emulators and simulators). If your device’s orientation refuses to change, try this:

  • On an Android device, in Settings ⇒   Display, turn on Auto Rotate Screen.
  • On an iPhone or iPad, swipe up from the bottom of the screen, and press the button that displays a lock and a circular arrow.

With an emulator or a simulator, you can try turning the computer monitor sideways, but that probably won’t work.

The SizedBox widget

If I planned to live on a desert island and I could bring only seven widgets with me, those seven widgets would be Column, Row, SizedBox, Container, Expanded, Spacer, and Padding. (If I could bring only two kinds of food with me, the two kinds of food would be cheeseburgers and chocolate.)

A SizedBox is a rectangle that developers use for taking up space. A SizedBox has a width, a height, and possibly a child. Very often, only the width or the height matters.

Listing 6-2 has a SizedBox of height 20.0 sitting between the title text and the rounded box. Without the SizedBox, there would be no space between the title text and the rounded box.

Tip A Spacer is like a SizedBox, except that a Spacer uses flex instead of explicit height and width parameters. For a look at Flutter’s flex property, see the section “Flexing some muscles,” later in this chapter.

Your friend, the Container widget

In Listing 6-2, the box displaying the words Sale Today uses a Container widget. A Container is a widget that contains something. (That’s not surprising.) While the widget is containing something, it has properties like height, width, alignment, decoration, padding, and margin.

The height and width parameters

You might be curious about a particular line in Listing 6-1:

return Container(
height: height,

What could height: height possibly mean? The height is what it is? The height is the height is the height?

To find out what’s going on, place the cursor on the second occurrence of the word height — the one after the colon. When you do, Android Studio highlights that occurrence along with one other. (See Figure 6-4.)

Snapshot of selecting a name in Android Studio's editor.

FIGURE 6-4: Selecting a name in Android Studio’s editor.

Noticeably absent is any highlight on the height that’s immediately before the colon. Listing 6-1 has two variables named height. One is a parameter of buildRoundedBox; the other is a parameter of the Container constructor. The line

height: height,

makes the Container parameter have the same value as the buildRoundedBox parameter. (The buildRoundedBox parameter gets its value from the call in Listing 6-2.)

Warning In a Container constructor call, the height and width parameters are suggestions — not absolute sizes. For details, refer to the section “Your humble servant, the Column widget,” earlier in this chapter. And, while you’re at it, check out the section “Using the Expanded Widget,” later in this chapter.

The alignment parameter

To align a child within a Container widget, you don’t use mainAxisAlignment or crossAxisAlignment. Instead, you use the plain old alignment parameter. In Listing 6-1, the line

alignment: Alignment(0.0, 0.0)

tells Flutter to put the child of the container in the center of the container. Figure 6-5 illustrates the secrets behind the Alignment class.

Snapshot of using a container's alignment parameter.

FIGURE 6-5: Using a container’s alignment parameter.

The decoration parameter

As the name suggests, decoration is something that livens up an otherwise dull-looking widget. In Listing 6-1, the BoxDecoration constructor has three parameters of its own:

  • color: The widget’s fill color.

    This property fills the Sale Today box in Figure 6-1 with white.

    Warning Both the Container and BoxDecoration constructors have color parameters. When you put a BoxDecoration inside of a Container, have a color parameter for the BoxDecoration, not the Container. If you have both, your program may crash.

  • border: The outline surrounding the widget.

    Listing 6-1 uses the Border.all constructor, which describes a border on all four sides of the Sale Today box.

    To create a border whose sides aren’t all the same, use Flutter’s Border constructor (without the .all part). Here’s an example:

    Border(
    top: BorderSide(width: 5.0, color: Colors.black),
    bottom: BorderSide(width: 5.0, color: Colors.black),
    left: BorderSide(width: 3.0, color: Colors.blue),
    right: BorderSide(width: 3.0, color: Colors.blue),
    )

  • borderRadius: The amount of curvature of the widget’s border.

    Figure 6-6 shows what happens when you use different values for the borderRadius parameter.

Snapshot of the experiments with a border radius.

FIGURE 6-6: Experiments with a border radius.

The padding and margin parameters

The Container constructor call in Listing 6-1 has no padding or margin parameters, but padding and margin can be useful in other settings. To find out how padding and margin work, look first at Listing 6-3.

LISTING 6-3 Without Padding or Margin

// App0603.dart
 
import 'package:flutter/material.dart';
 
void main() => runApp(App0602());

class App0602 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Material(
color: Colors.grey[50],
child: Container(
color: Colors.grey[500],
child: Container(
color: Colors.grey[700],
),
),
),
);
}
}

Listing 6-3 has a container within another container that’s within a Material widget. The inner container is grey[700], which is fairly dark grey. The outer container is a lighter grey, and the Material widget background is grey[50], which is almost white.

I told my editor that I wanted to use up page space with a figure devoted to a run of Listing 6-3, but he said no. I wonder why! Who could object to a figure that’s nothing but a dark grey rectangle?

When you run the app in Listing 6-3, the inner container completely covers the outer container, which, in turn, completely covers the Material widget. Each of these widgets expands to fill its parent, so each of the three widgets takes up the entire screen. The only widget you can see is the innermost, dark grey container. What a waste!

To remedy this situation, Listing 6-4 uses both padding and margin. Figure 6-7 shows you the result.

LISTING 6-4 With Padding and Margin

// App0604.dart
 
import 'package:flutter/material.dart';
 
void main() => runApp(App0603());
 
class App0603 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: SafeArea(
child: Material(
color: Colors.grey[50],
child: Container(
color: Colors.grey[500],
padding: EdgeInsets.all(80.0),
margin: EdgeInsets.all(40.0),
child: Container(
color: Colors.grey[700],
),
),
),
),
);
}
}

Snapshot of padding versus margin.

FIGURE 6-7: Padding versus margin.

Listing 6-4 is all about the middle container — the one whose color is a medium shade of grey. I’ve marked up Figure 6-7 to make the result crystal-clear. The general rules are as follows:

  • Padding is the space between a widget’s outermost edges and the widget’s child.

    In Figure 6-7, the medium grey stuff is padding.

  • A margin is the space between a widget’s outermost edges and the widget’s parent.

    In Figure 6-7, the white (or nearly white) stuff is the margin.

From what I observe, Flutter developers use padding a lot but use margin sparingly.

Remember You can add padding to almost any widget without putting that widget inside a Container. To do so, simply put the widget inside of a Padding widget. For an example, look for the Padding widget in Listing 6-1.

When you think about a mobile device, you probably imagine a rectangular screen. Does this mean that an entire rectangle is available for use by your app? It doesn’t. The top of the rectangle may have a notch. The corners of the rectangle may be rounded instead of square. The operating system (iOS or Android) may consume parts of the screen with an Action Bar or other junk.

To avoid items in this obstacle course, Flutter has a SafeArea widget. The SafeArea is the part of the screen that’s available for the free, unencumbered use by your app. In Listing 6-4, a SafeArea helps me show the padding and margin in all their glory. Without that SafeArea, the top part of the margin might be covered by stuff that’s not part of my app.

Nesting Rows and Columns

You hardly ever see an app with only one column of widgets. Most of the time, you see widgets alongside other widgets, widgets arranged in grids, widgets at angles to other widgets, and so on. The most straightforward way to arrange Flutter widgets is to put columns inside of rows and rows inside of columns. Listing 6-5 has an example, and Figure 6-8 shows you the results.

LISTING 6-5 A Row Within a Column

// App0605.dart
 
import 'package:flutter/material.dart';
 
import 'App06Main.dart';
 
Widget buildColumn(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildTitleText(),
SizedBox(height: 20.0),
_buildRowOfThree(),
],
);
}
 
Widget _buildRowOfThree() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
buildRoundedBox("Cat"),
buildRoundedBox("Dog"),
buildRoundedBox("Ape"),
],
);
}

Snapshot of the animals for sale.

FIGURE 6-8: Animals for sale.

In Listing 6-1, the Column widget’s crossAxisAlignment property forces the Sale Today box to be as wide as it could possibly be. That happens because the Sale Today box is one of the Column widget’s children. But in Listing 6-5, the Cat, Dog, and Ape boxes aren’t children of the Column widget. Instead, they’re grandchildren of the Column widget. So, for Listing 6-5, the major factor positioning the Cat, Dog, and Ape boxes is the Row widget’s mainAxisAlignment property.

To see this in action, change the lines

return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,

in Listing 6-5 to the following lines:

return Row(
mainAxisAlignment: MainAxisAlignment.center,

When you do, you see the arrangement shown in Figure 6-9.

Snapshot of the animals in cramped quarters.

FIGURE 6-9: Animals in cramped quarters.

Crossreference To find out about values you can give to a mainAxisAlignment property, refer to Chapter 3.

More Levels of Nesting

Every sack had seven cats,

Every cat had seven kits …

FROM A TRADITIONAL ENGLISH LANGUAGE NURSERY RHYME

Yes, you can create a row within a column within a row within a column within a row. You can go on like that for a very long time. This section has two modest examples. The first example (Listing 6-6) has a row of captioned boxes.

LISTING 6-6 (Does This Listing Have Three Captions?)

// App0606.dart
 
import 'package:flutter/material.dart';
 
import 'App06Main.dart';
 
Widget buildColumn(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildTitleText(),
SizedBox(height: 20.0),
_buildCaptionedRow(),
],
);
}

Widget _buildCaptionedRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildCaptionedItem(
"Cat",
caption: "Meow",
),
_buildCaptionedItem(
"Dog",
caption: "Woof",
),
_buildCaptionedItem(
"Ape",
caption: "Chatter",
),
],
);
}
 
Column _buildCaptionedItem(String label, {String caption}) {
return Column(
children: <Widget>[
buildRoundedBox(label),
SizedBox(
height: 5.0,
),
Text(
caption,
textScaleFactor: 1.25,
),
],
);
}

Figure 6-10 shows a run of the code from Listing 6-6.

Snapshot of the noisy animals for sale.

FIGURE 6-10: Noisy animals for sale.

The next example, Listing 6-7, does something a bit different. In Listing 6-7, two boxes share the space where one box might be.

LISTING 6-7 More Widget Nesting

// App0607.dart
 
import 'package:flutter/material.dart';
 
import 'App06Main.dart';
 
Widget buildColumn(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildTitleText(),
SizedBox(height: 20.0),
_buildColumnWithinRow(),
],
);
}
 
Widget _buildColumnWithinRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
buildRoundedBox("Cat"),
SizedBox(width: 20.0),
buildRoundedBox("Dog"),
SizedBox(width: 20.0),
Column(
children: <Widget>[
buildRoundedBox(
"Big ox",
height: 36.0,
),
SizedBox(height: 16.0),
buildRoundedBox(
"Small ox",
height: 36.0,
),
],
),
],
);
}

Figure 6-11 shows a run of the code from Listing 6-7.

Snapshot of a multilevel arrangement.

FIGURE 6-11: A multilevel arrangement.

Using the Expanded Widget

Start with the code in Listing 6-5, and add two more boxes to the row:

Widget _buildRowOfThree() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
buildRoundedBox("Cat"),
buildRoundedBox("Dog"),
buildRoundedBox("Ape"),
buildRoundedBox("Ox"),
buildRoundedBox("Gnu"),
],
);
}

Yes, the method name is still _buildRowOfThree. If the name bothers you, you can either change the name or Google the Hitchhiker’s Guide to the Galaxy trilogy.

When you run this modified code on a not-too-large phone in portrait mode, you see the ugly display in Figure 6-12. (If your phone is too large to see the ugliness, add more buildRoundedBox calls.)

Snapshot of the user can't cross the barricade.

FIGURE 6-12: You can’t cross the barricade.

The segment on the right side of Figure 6-12 (the stuff that looks like barricade tape) indicates overflow. To put it crudely, you’ve created a blivit. The row is trying to be wider than the phone’s screen. Look near the top of Android Studio’s Run tool window and you see the following message:

A RenderFlex overflowed by 67 pixels on the right.

What else is new?

When you line up too many boxes side-by-side, the screen becomes overcrowded. That’s not surprising. But some layout situations aren’t so obvious. You can stumble into an overflow problem when you least expect it.

What can you do when your app overflows? Here’s an off-the-wall suggestion: Tell each of the boxes to expand. (You read that correctly: Tell them to expand!) Listing 6-8 has the code, and Figure 6-13 shows you the results.

LISTING 6-8 Expanding Your Widgets

// App0608.dart
 
import 'package:flutter/material.dart';
 
import 'App06Main.dart';
 
Widget buildColumn(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildTitleText(),
SizedBox(height: 20.0),
_buildRowOfFive(),
],
);
}
 
Widget _buildRowOfFive() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildExpandedBox("Cat"),
_buildExpandedBox("Dog"),
_buildExpandedBox("Ape"),
_buildExpandedBox("Ox"),
_buildExpandedBox("Gnu"),
],
);
}
 
Widget _buildExpandedBox(
String label, {
double height = 88.0,
}) {
return Expanded(
child: buildRoundedBox(
label,
height: height,
),
);
}

Snapshot of a nice row of five.

FIGURE 6-13: A nice row of five.

I quote from the official Flutter documentation (https://api.flutter.dev/flutter/widgets/Expanded-class.html):

  • A widget that expands a child of a Row, Column, or Flex so that the child fills the available space.
  • Using an Expanded widget makes a child of a Row, Column, or Flex expand to fill the available space along the main axis (horizontally for a Row or vertically for a Column). If multiple children are expanded, the available space is divided among them according to the flex factor.

In spite of its name, the Expanded widget doesn’t necessarily make its child bigger. Instead, the Expanded widget makes its child fill the available space along with any other widgets that are competing for that space. If that available space differs from the code’s explicit height or width value, so be it. Listing 6-8 inherits the line

width: 88.0,

to describe the width of each rounded box. But, in Figure 6-13, none of the boxes is 88.0 dp wide. When I run the app on an iPhone 11 Pro Max, each box is only 74.8 dp wide.

Expanded versus unexpanded

The code in the previous section surrounds each of a row’s boxes with the Expanded widget. In this section, Listing 6-9 shows you what happens when you use Expanded more sparingly.

LISTING 6-9 Expanding One of Three Widgets

// App0609.dart
 
import 'package:flutter/material.dart';
 
import 'App06Main.dart';
 
Widget buildColumn(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildTitleText(),
SizedBox(height: 20.0),
_buildRowOfThree(),
],
);
}
 
Widget _buildRowOfThree() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
buildRoundedBox(
"Giraffe",
height: 150.0,
),
SizedBox(width: 10.0),
buildRoundedBox(
"Wombat",
height: 36.0,
),
SizedBox(width: 10.0),
_buildExpandedBox(
"Store Manager",
height: 36.0,
),
],
);
}
 
Widget _buildExpandedBox(
String label, {
double height = 88.0,
}) {
return Expanded(
child: buildRoundedBox(
label,
height: height,
),
);
}

The code in Listing 6-9 surrounds only one box — the Store Manager box — with an Expanded widget. Here’s what happens:

  • The code gets width: 88.0 from the buildRoundedBox method in Listing 6-1, so the Giraffe and Wombat boxes are 88.0 dp wide each.
  • Two SizedBox widgets are 10.0 dp wide each.

    So far, the total is 196.0 dp.

  • Because the Store Manager box sits inside an Expanded widget, the remaining screen width goes to the Store Manager box. (See Figure 6-14.)
Snapshot of the store manager takes up space.

FIGURE 6-14: The store manager takes up space.

Use of the Expanded widget affects a widget’s size along its parent’s main axis, but not along its parent’s cross axis. So, in Figure 6-14, the Store Manager box grows from side to side (along the row’s main axis) but doesn’t grow from top to bottom (along the row’s cross axis). In fact, only the numbers 150.0, 36.0, and 36.0 in the _buildRowOfThree method (see Listing 6-9) have any influence on the heights of the boxes.

With a bit of tweaking, the code in Listing 6-9 can provide more evidence that an Expanded widget isn’t necessarily a large widget. Try these two experiments:

  1. Rerun the code from Listings 6-1 and 6-9. But, in the buildRoundedBox method declaration, change width: 88.0 to width: 130.0.

    On my iPhone 11 Pro Max simulator, the widths of the Giraffe and Wombat boxes are 130.0 dp each. But the width of the Expanded Store Manager box is only 94.0 dp. The Giraffe and Wombat boxes are quite large. So, when the Store Manager box fills the remaining available space, that space is only 94.0 dp wide. (See Figure 6-15.)

    Snapshot of expanding to fit into a small space.

    FIGURE 6-15: Expanding to fit into a small space.

  2. In the buildRoundedBox method declaration, change width from its value in Step 1 (width: 130.0) to width: 180.0.

    With the Giraffe and Wombat boxes and the SizedBox widgets taking up 380.0 dp, there’s no room left on my iPhone 11 Pro Max simulator for the Store Manager box. Alas! I see the black-and-yellow stripe, indicating RenderBox overflow. (See Figure 6-16.) The Expanded widget isn’t a miracle worker. It doesn’t help solve every problem.

Snapshot of more barricade tape.

FIGURE 6-16: More barricade tape.

Expanded widget saves the day

Listings 6-10 and 6-11 illustrate a nasty situation that may arise when you mix rows and columns at various levels.

LISTING 6-10 A Listing That’s Doomed to Failure

// App0610.dart -- BAD CODE
 
import 'package:flutter/material.dart';
 
import 'App06Main.dart';
import 'constraints_logger.dart';
 
Widget buildColumn(BuildContext context) {
return Row(
children: [
_buildRowOfThree(),
],
);
}
 
Widget _buildRowOfThree() {
return ConstraintsLogger(
comment: 'In _buildRowOfThree',
child: Row(
children: <Widget>[
_buildExpandedBox("Cat"),
_buildExpandedBox("Dog"),
_buildExpandedBox("Ape"),
],
),
);
}
 
Widget _buildExpandedBox(
String label, {
double height = 88.0,
}) {
return Expanded(
child: buildRoundedBox(
label,
height: height,
),
);
}

LISTING 6-11 An Aid For Debugging

When you run the code in Listings 6-10 and 6-11, three things happen:

  • Nothing appears on your device’s screen except maybe a dull, grey background.
  • In Android Studio’s Run tool window, you see the following error message:

    RenderFlex children have non-zero flex but incoming width
    constraints are unbounded.

    Flutter developers start groaning when they see this message.

    Later on, in the Run tool window …

    If a parent is to shrink-wrap its child, the child
    cannot simultaneously expand to fit its parent.

  • Also, in the Run tool window, you see a message like this one:

    I/flutter ( 5317): In _buildRowOfThree:
    BoxConstraints(0.0<=w<=Infinity, 0.0<=h<=683.4) to Row

    This I/flutter message tells you that the layout’s inner row is being handed a width constraint that has something to do with Infinity. This informative 0.0<=w<=Infinity message comes to you courtesy of the code in Listing 6-11.

What do all these messages mean? In a Flutter app, your widgets form a tree. Figure 6-17 shows a tree of widgets as it’s depicted in Android Studio’s Flutter Inspector.

Snapshot of the tree created by Listings 6-10 and 6-11.

FIGURE 6-17: The tree created by Listings 6-10 and 6-11.

To display your widgets, Flutter travels in two directions:

  • Along the tree from top to bottom

    During this travel, each widget tells its children what sizes they can be. In Flutter terminology, each parent widget passes constraints to its children.

    For example, a Run tool window message says that, in Listing 6-11, the outer row passes the width constraint of 0.0<=w<=Infinity to the inner row. Because of the word Infinity, this constraint is called an unbounded constraint.

    If you’re looking for an example of a bounded constraint, look at the same Run tool window message. The outer row passes the height constraint of 0.0<=h<=683.4 to the inner row. That constraint is bounded by the value 683.4 dp.

    Eventually, Flutter reaches the bottom of your app’s widget tree. At that point …

  • Along the tree again — this time, from bottom to top

    During this travel, each child widget tells its parent exactly what size it wants to be. The parent collects this information from each of its children and uses the information to assign positions to the children.

    Sometimes this works well, but in Listing 6-11, it fails miserably.

In Listing 6-11, because each animal box is inside an Expanded widget, the inner row doesn’t know how large it should be. The inner row needs to be given a width in order to divide up the space among the animal boxes. But the outer row has given an unbounded constraint to the inner row. Instead of telling the inner row its width, the outer row is asking the inner row for its width. Nobody wants to take responsibility, so Flutter doesn’t know what to do. (See Figure 6-18.)

Snapshot of my first graphic novel.

FIGURE 6-18: My first graphic novel.

How can you fix this unpleasant problem? Oddly enough, another Expanded widget comes to the rescue.

Widget _buildRowOfThree() {
return Expanded(
child: ConstraintsLogger(
comment: 'In _buildRowOfThree',
child: Row(
children: <Widget>[
_buildExpandedBox("Cat"),
_buildExpandedBox("Dog"),
_buildExpandedBox("Ape"),
],
),
),
);
}

This new Expanded widget passes bounded constraints down the widget tree, as you can see from this new message in the Run tool window:

I/flutter ( 5317): In _buildRowOfThree:
BoxConstraints(w=371.4, 0.0<=h<=683.4) to Row

The new Expanded widget tells the inner row that its width must be exactly 371.4 dp, so the confusion that’s illustrated in Figure 6-18 goes away. Flutter knows how to display the app’s widgets, and you see three nicely arranged animal boxes on your device’s screen. Problem solved!

Technical Stuff The constraint w=371.4 is called a tight constraint because it gives the row an exact size with no leeway whatsoever. In contrast, the constraint 0.0<=h<=683.4 is called a loose constraint. The loose constraint says, “Be as short as 0.0 dp high and as tall as 683.4 dp high. See if I care.”

This business with constraints and sizes may seem overly complicated. But the process of scanning down the tree and then up the tree is an important part of the Flutter framework. The two-scan approach makes for efficient rebuilding of stateful widgets. And the rebuilding of stateful widgets is fundamental to the way Flutter apps are designed.

Some layout schemes work well with small numbers of components but start slowing down when the number of components becomes large. Flutter’s layout scheme works well with only a few widgets and scales nicely for complicated layouts with large numbers of widgets.

Remember The ConstraintsLogger widget is for debugging purposes only. Before publishing an app, remove all uses of the ConstraintsLogger from your code.

Flexing some muscles

Using Flutter’s Expanded widget, you can specify the relative sizes of the children inside a column or a row. Listing 6-12 has an example.

LISTING 6-12 How to Specify Relative Sizes

// App0612.dart
 
import 'package:flutter/material.dart';
 
import 'App06Main.dart';
 
Widget buildColumn(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildTitleText(),
SizedBox(height: 20.0),
_buildRowOfThree(),
],
);
}
 
Widget _buildRowOfThree() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildExpandedBox(
"Moose",
),
_buildExpandedBox(
"Squirrel",
flex: 1,
),
_buildExpandedBox(
"Dinosaur",
flex: 3,
),
],
);
}
 
Widget _buildExpandedBox(
String label, {
double height = 88.0,
int flex,
}) {
return Expanded(
flex: flex,
child: buildRoundedBox(
label,
height: height,
),
);
}

What will happen to our heroes, the Moose and the Squirrel, in Listing 6-12? To find out, see Figure 6-19.

Snapshot of the squirrel is small and the dinosaur is big.

FIGURE 6-19: The squirrel is small; the dinosaur is big.

Notice the frequent use of the word flex in Listing 6-12. An Expanded widget can have a flex value, also known as a flex factor. A flex factor decides how much space the widget consumes relative to the other widgets in the row or column.

Listing 6-12 has three boxes:

  • Moose, with no flex value (the value null)
  • Squirrel, with flex value 1
  • Dinosaur, with flex value 3

Here’s the lowdown on the resulting size of each box:

Because the Moose box has a null flex value, the Moose box has whatever width comes explicitly from the _buildExpandedBox method. The Moose box’s width is 88.0. (Refer to Figure 6-19.)

Both the Squirrel and Dinosaur boxes have non-null, non-zero flex values. So those two boxes share the space that remains after the Moose box is in place. With flex values of Squirrel: 1, Dinosaur: 3, the Dinosaur box is three times the width of the Squirrel box. On my Pixel 2 emulator, the Squirrel box is 70.9 dp wide, and the Dinosaur box is 212.5 dp wide. That’s the way flex values work.

Technical Stuff In addition to the Expanded widget’s flex property, Flutter has classes named Flex and Flexible. It’s easy to confuse the three of them. Every Flex instance is either a Row instance or a Column instance. And every Expanded instance is an instance of the Flexible class. A Flexible instance can have a flex value, but a Flexible instance doesn’t force its child to fill the available space. How about that!

How Big Is My Device?

The title of this section is a question, and the answer is “You don’t know.” I can run a Flutter app on a small iPhone 6, or in a web page on a 50-inch screen. You want your app to look good no matter what size my device happens to be. How can you do that? Listing 6-13 has an answer.

LISTING 6-13 Checking Device Orientation

// App0613.dart
 
import 'package:flutter/material.dart';
 
import 'App06Main.dart';
 
Widget buildColumn(context) {
if (MediaQuery.of(context).orientation == Orientation.landscape) {
return _buildOneLargeRow();
} else {
return _buildTwoSmallRows();
}
}
 
Widget _buildOneLargeRow() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
buildRoundedBox("Aardvark"),
buildRoundedBox("Baboon"),
buildRoundedBox("Unicorn"),
buildRoundedBox("Eel"),
buildRoundedBox("Emu"),
buildRoundedBox("Platypus"),
],
),
],
);
}
 
Widget _buildTwoSmallRows() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
buildRoundedBox("Aardvark"),
buildRoundedBox("Baboon"),
buildRoundedBox("Unicorn"),
],
),
SizedBox(
height: 30.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
buildRoundedBox("Eel"),
buildRoundedBox("Emu"),
buildRoundedBox("Platypus"),
],
),
],
);
}

Figures 6-20 and 6-21 show what happens when you run the code in Listing 6-13. When the device is in portrait mode, you see two rows, with three boxes on each row. But when the device is in landscape mode, you see only one row, with six boxes.

The difference comes about because of the if statement in Listing 6-13.

if (MediaQuery.of(context).orientation == Orientation.landscape) {
return _buildOneLargeRow();
} else {
return _buildTwoSmallRows();
}

Snapshot of Listing 6-13 in portrait mode.

FIGURE 6-20: Listing 6-13 in portrait mode.

Snapshot of Listing 6-13 in landscape mode.

FIGURE 6-21: Listing 6-13 in landscape mode.

Yes, the Dart programming language has an if statement. It works the same way that if statements work in other programming languages.

if (a certain condition is true) {
Do this stuff;
} otherwise {
Do this other stuff;
}

In the name MediaQuery, the word Media refers to the screen that runs your app. When you call MediaQuery.of(context), you get back a treasure trove of information about that screen, such as

  • orientation: Whether the device is in portrait mode or landscape mode
  • size.height and size.width: The number of dp units from top to bottom and across the device’s screen
  • size.longestSide and size.shortestSide: The larger and smaller screen size values, regardless of which is the height and which is the width
  • size.aspectRatio: The screen’s width divided by its height
  • devicePixelRatio: The number of physical pixels for each dp unit
  • padding, viewInsets, and viewPadding: The parts of the display that aren’t available to the Flutter app developer, such as the parts covered up by the phone’s notch or (at times) the soft keyboard
  • alwaysUse24HourFormat: The device’s time display setting
  • platformBrightness: The device’s current brightness setting
  • … and many more

For example, a Pixel C tablet with 2560-by-1800 dp is big enough to display a row of six animal boxes in either portrait or landscape mode. To prepare for your app to run on such a device, you may not want to rely on the device’s orientation property. In that case, you can replace the condition in Listing 6-13 with something like the following:

if (MediaQuery.of(context).size.width >= 500.0) {
return _buildOneLargeRow();
} else {
return _buildTwoSmallRows();
}

Notice the word context in the code MediaQuery.of(context). In order to query media, Flutter has to know the context in which the app is running. That’s why, starting with this chapter’s very first listing, the _MyHomePage class’s build method has a BuildContext context parameter. Listing 6-1 has this method call:

buildColumn(context)

And other listings have method declarations with this header:

Widget buildColumn(BuildContext context)

Listings 6-2 to 6-12 make no use of that context parameter. But what if, in Listing 6-1, I omit the method’s context parameter, like so:

buildColumn()

Then everything is hunky-dory until Listing 6-13, which has no access to the context and is unable to call MediaQuery.of(context). What a pity!

When I created Listing 6-1, I added the context parameter because I anticipated the need for the context value in this chapter’s last listing — Listing 6-13. Yes, I’m a very smart dude.

Well, that’s not really true. When I started writing this chapter, I didn’t anticipate the need for the context value. I didn’t see the context issue coming until I started writing this last section. At that point, I went back and modified every single listing so that the context would be available to Listing 6-13. Oh, well! Everybody has to make course corrections. It’s part of life, and it’s certainly part of professional app development.

On to the next chapter… .