Chapter 6
Laying Things Out
IN THIS CHAPTER
Putting widgets where you want them
Dealing with common layout problems
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:
- Great-grandmother tells grandmother how big she (grandmother) can be.
- Grandmother tells mother how big she (mother) can be.
- Mother tells Elsie how big she (Elsie) can be.
- Elsie decides how big she is and tells mother.
- Mother determines Elsie’s position, decides how big she (mother) is, and then tells grandmother.
- Grandmother determines mother’s position, decides how big she (grandmother) is, and then tells great-grandmother.
- 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,
),
],
);
}

FIGURE 6-1: A sale at My Pet Shop.
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:
-
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), -
Place the mouse cursor on the constructor call’s name.
For the snippet in Step 1, click on the word
Text
. -
On Android Studio’s main menu, select Refactor ⇒ Extract ⇒ Method.
As a result, Android Studio displays the Extract Method dialog box.
-
In the Extract Method dialog box, type a name for your new method.
For a constructor named
Text
, Android Studio suggests the method namebuildText
. But, to create Listings 6-1 and 6-2, I made up the namebuildTitleText
. -
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( - 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 theWidget
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’sText
widget with aCenter
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 theWidget
class. But, no, an instance of theCenter
class isn’t an instance of theText
class. Your method returns an instance ofCenter
, but the method’s header expects the method to return an instance ofText
. Don’t you wish you had changed the first word in the header toWidget
? 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.
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 |
|
Green |
|
Blue |
|
Purple (equal amounts of Red and Blue) |
|
Black |
|
White |
|
Grey (approximately 75% whiteness) |
|
50% transparent Red |
|
Nothing (complete transparency, no matter what the Red, Green, and Blue values are) |
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 |
|
20.0 dp on all four sides |
|
15.0 dp on the left 10.0 dp on top |
|
10.0 dp on top 15.0 dp on the right 15.0 dp on the bottom |
|
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.
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.)

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.

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

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

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.
Both the
Container
andBoxDecoration
constructors havecolor
parameters. When you put aBoxDecoration
inside of aContainer
, have acolor
parameter for theBoxDecoration
, not theContainer
. 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.

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],
),
),
),
),
);
}
}

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.
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"),
],
);
}

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.

FIGURE 6-9: Animals in cramped quarters.
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.

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.

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

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,
),
);
}

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

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:
-
Rerun the code from Listings 6-1 and 6-9. But, in the
buildRoundedBox
method declaration, changewidth: 88.0
towidth: 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.)FIGURE 6-15: Expanding to fit into a small space.
-
In the
buildRoundedBox
method declaration, changewidth
from its value in Step 1 (width: 130.0
) towidth: 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.) TheExpanded
widget isn’t a miracle worker. It doesn’t help solve every problem.

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 RowThis
I/flutter
message tells you that the layout’s inner row is being handed a width constraint that has something to do withInfinity
. This informative0.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.

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

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

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.
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();
}

FIGURE 6-20: Listing 6-13 in portrait 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 modesize.height
andsize.width
: The number of dp units from top to bottom and across the device’s screensize.longestSide
andsize.shortestSide
: The larger and smaller screen size values, regardless of which is the height and which is the widthsize.aspectRatio
: The screen’s width divided by its heightdevicePixelRatio
: The number of physical pixels for each dp unitpadding
,viewInsets
, andviewPadding
: 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 keyboardalwaysUse24HourFormat
: The device’s time display settingplatformBrightness
: 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… .