Now that we’re familiar with some widgets that hold a value and how to make them respond to gestures, we are ready to make them lay out properly. In this chapter, we’re going to deal with the major techniques of getting your widgets to appear on the screen in various relations to one another and to manage the space between them. Notice that I said “major techniques” not “all techniques.” This is because Flutter has dozens of widgets for laying things out, many of which overlap in functionality with others. This is great if you enjoy lots of choices, but the more choices you have, the more complex a subject is.1 So to spare you the confusion, we’re not going to cover 100% of the widgets or the options. Instead we’re going to focus on the ones that will get the job done in the real world without overwhelming you. We suggest that you learn the techniques in this chapter to get you 90% of what you’ll ever need for layouts. Then, when you run across a situation that you can’t solve with these techniques, you can do some research or call for help.
- 1.
Layout the entire screen (aka scene)
This is where we’ll set the look and feel of the entire app and create the structure of the scene like the title, action button, and menus (Figure 6-1). - 2.
Position widgets above and below each other or side by side
When designing any scene, we break it into widgets and place them on the screen. For example, the following scene (Figure 6-2) must be broken into widgets. Since it is a scrolling list of people, we might want a bunch of PersonCard widgets (Figure 6-3) on the scene each above and below another. We’d do that with a ListView.Then in turn, each PersonCard widget should have an image side by side with text (Figure 6-4). How do you get the text next to the image? We’ll use a Row widget. Also notice that the text is a series of data about that person. How do you get the text above and below? We’ll use a Column widget there. - 3.
Handle extra space in the scene
Hey, there’s extra space on the right side of each Person. What if we wanted that space to be on the left? Or what if we wanted to put some of that extra space on the left of the image?
- 4.
Handle situations when we run out of space and overflow the scene
In the scene with all of the PersonCards, we have more people than we have screen so we’ve overflowed it. This normally throws an error, but there are several ways to fix the situation. We’ll look at the best way.
- 5.
Make finer adjustments in positioning
Our scene currently feels crowded. What can we do to create a little elbow room (Figure 6-5)? Let’s make it look a little more like in the figure:
Alright, so there’s our plan for the chapter. We’ll do a deep dive into each of the five steps. But before we do, let’s take just a moment to see how to debug our visual layout.
Tip
Use visual debugging to see how Flutter is making its decisions for your layout. Figure 6-6 is how your screen might look normally; when you toggle debug painting, you’ll see Figure 6-7.
All visual boxes get a teal border. Padding, margin, and border are colored in blue. Alignment/positioning is made obvious with yellow arrows. The big green arrows show widgets that can scroll. Once you get accustomed to them, these visual cues will help you see how Flutter thinks so you can tune your layout.
In VS Code open the command palette (cmd-shift-P or control-shift-P) and type in “Toggle debug painting.”
In Android Studio/IntelliJ go to View ➤ Tool Windows ➤ Flutter Inspector and hit the “Show debug paint” button in the toolbar.
Laying out the whole scene
Here’s a tip for you: Apps should never surprise their users.2 When apps do things in the way that the user expects, they think the app is friendly, simple, and easy. Users have been trained to see a status bar at the top followed by a title bar. And while other screen affordances will vary based on need, there are definite conventions. Flutter has widgets to make your layouts feel ... well ... normal.
MaterialApp widget
The MaterialApp widget creates the outer framework for your app. As important as it is, the user never sees the MaterialApp because no parts of it are technically visible. It wraps your entire app, giving you the opportunity to give it a title so that when your OS moves the app into the background, it’ll have a name. This is also where you’ll apply the default theme for your app – fonts, sizes, colors, and so forth. We’ll get way more into themes in the styles chapter. Stay tuned for that. MaterialApp is also the place to specify routes, something we’ll talk much more about in the routing chapter.
Note
The “Material” in MaterialApp does indeed refer to Material Design, which is kind of a Google/Android thing. But it is probably misnamed because all apps, even iOS-focused apps, will have a MaterialApp widget at its root. It does not give your app any more of an Android feel or less of an iOS feel.
Finally, MaterialApp has a home property. Remember that your project will have lots of custom widgets. You specify which one is the startup widget by setting your MaterialApp’s home property. This widget will be the root of your main scene and will therefore probably begin with a Scaffold widget. “What’s a Scaffold widget,” you say? Glad you asked ...
The Scaffold widget
Whereas the MaterialApp widget creates the outer invisible framework, the Scaffold widget creates the inner visible framework.
An AppBar for the title
A section for the body
A navbar at the bottom or a navigation drawer to the left
A floating action button
A bottom sheet – a section that is usually collapsed but can be slid up to reveal context-aware information for the scene that the user is on at that moment
All parts of the Scaffold are optional. That kind of makes sense because you don’t always want a floatingActionButton or a drawer or a bottomNavigationBar. Our screen designs will dictate which parts we need and which we don’t.
The AppBar widget
If you have a navigation drawer, you’re probably going to want to omit the leading icon.
SafeArea widget
Device screens are seldom neat rectangles. They have rounded corners and notches and status bars at the top. If we ignored those things, certain parts of our app would be cut off or hidden. Don’t want that? You have two choices, keep a huge database of all devices with their displayable areas and have a ton of gnarly conditional renderings. Horrible! Or use the SafeArea widget which in essence does that for you.
SnackBar widget
Note that you run the showSnackBar() method to bring the SnackBar up. You are in control of the duration that it stays up. Finally, you can add an action to the SnackBar if you want. Of course you may just want to bring up a message only with no action. It’s up to you.
How Flutter decides on a widget’s size
We all have constraints in life – rules and laws and boundaries we must live by. If we don’t submit to those constraints, there are consequences. Flutter widgets have constraints also and they have consequences. Just like in real life, things will be easier on you if you learn the rules and how those constraints work.
In Flutter, every widget on your device’s screen eventually has a height and a width which it calls the “RenderBox.” Each widget also has constraints: a minHeight, a minWidth, a maxHeight, and a maxWidth which it calls the “BoxConstraints.”
Note
All of these measures are in units of pixels which is obviously device-dependent. You iOS developers call them points, and Android devs call them density-independent pixels.
As long as the widget’s RenderBox is completely within its BoxConstraints, life is good. In other words, its height must be between minHeight and maxHeight, and its width must be between the minWidth and maxWidth. But the moment that a widget demands to be drawn outside the constraints, bad things happen. Sometimes Flutter throws an exception, and other times it does its best and just clips the widget or shrinks it.
The dreaded “unbounded height” error
Now wouldn’t that have been clearer? You’d understand the problem and clearly know how to fix it.
Let me help you interpret what Flutter is trying to tell us; certain widgets want to fill all of the available space that they can. In other words, they’re greedy. They need a parent to constrain them. If they’re inside of a parent who refuses to provide that constraint, Flutter freaks out because it can’t understand what we developers are trying to do. To be blunt, this is a symptom of the developer not really understanding how Flutter handles layouts. So let me try to explain Flutter’s layout algorithm in hopes of predicting and therefore avoiding snafus like the preceding example.
Note
If you don’t completely understand Flutter’s layout algorithm, it isn’t the end of the world. You can still work with Flutter without memorizing this section. But the better understanding you have of this concept, the less frustrated you’ll be when you run across layout problems in the real world. So try.
Flutter’s layout algorithm
Flutter travels down the tree starting at the root. It reads the constraint of the root widget. “What is the tallest you can be? And the widest?” It remembers them and then looks for any children. For each child, it communicates its BoxConstraints to them and then travels to the grandchildren. It keeps doing this all the way to the end of every branch. We call this the leaf level.
It then asks each leaf how big it would prefer to be. “What is your favorite height? What is your favorite width?” It allows the leaf to be drawn at its preferred size within the constraints of all of its ancestors. If the preferred size is too big, Flutter clips it at runtime – something we really try to avoid! If the preferred size is too small, Flutter pads it with extra space until it fits.
It then goes back up a level and tries to fit those branches inside their common parent which has its own constraints. And so on all the way back up to the top.
The result is that each child gets to be its favorite height and width – as long as its parent allows it. And no parent has a final size until all of its child do.
Tip
Another situation you’re going to come across is when you have a widget whose RenderBox is larger than its BoxConstraints. In other words, this single widget can’t fit inside its parent. The solution for that problem is occasionally a FittedBox,3 a widget that shrinks it’s child to fit. By default, you’ll get a centered widget that is scaled down until it just fits both horizontally and vertically, but you have the options to align it vertically/horizontally and to stretch it or clip top/bottom or left/right.
So you can see how we’d get the “unbounded height” error. If we had a child who tries to be as large as it can and it doesn’t have a parent to tell it to stop, Flutter panics because it is now infinitely tall. To solve the problem, that child simply needs a parent to tell it to stop growing. A LimiteBox() widget’s main characteristic is to do exactly that; it tells a child just how big it is allowed to get if the parent refuses to. And Flutter has a ton of widgets to control size and position. For the rest of this chapter, we’re going to study the most critical of those layout widgets – the ones you absolutely must know. We’ll start with Row and Colum.
Putting widgets next to or below others
Notice that they both have a children property which is an array of Widgets. All widgets in the children array will be displayed in the order you add them. You can even have rows inside columns and vice versa as many levels deep as you like. In this way you can create nearly any layout imaginable in any app.
Rows and columns will be your go-to layout widgets. Yes, there are others, but these two are your first calls.
Note
Your widgets will never fit!
- 1.
What if there’s extra space left over? (more screen than pixels taken up by the widgets)
- 2.
What if there’s not enough space? (too many widgets in a given space)
These are both likely to happen simultaneously on different parts of your scene. Let’s tackle leftover space first.
What if there’s extra space left over?
This is an easy problem to solve. The only question you really need to answer is how to distribute the extra room. How much space do you want to allocate around each of the other widgets? You have several options. The easiest and quickest is to use mainAxisAlignment and crossAxisAlignment.
mainAxisAlignment
crossAxisAlignment
There’s also one more: baseline. But it only makes sense in a row, and it is much less frequently used.
Tip
So you can see that as the width of the longest button increases, so do they all.
Expanded widget
mainAxisAlignment is awesome if the spacing is cut and dried – you want equal spacing somehow. But what if you don’t want spacing at all? What if you want the widgets to expand to fill the remaining space? Expanded widget to the rescue (Figure 6-19).
When you wrap a Row/Column’s child in an Expanded widget (Figure 6-20), it makes that child flexible, meaning that if there is extra space, it will stretch along the main axis to fill that space.
Note that the mainAxisAlignment now makes no difference because there is no extra space. It’s all eaten up by the Expanded.
Note that the second one is now smaller because the extra space is shared with the third and fourth widgets, divided equally among them.
Notice that the free space has still been allocated to the Expandeds but in the proportions of 1, 3, and 2 instead of evenly. So the one with a flex factor of 3 gets three times as much space as the one with a flex factor of 1.
Note
What if there’s not enough space?
We’ve tackled the situations where there is too much space and how to control where that extra space is allocated. But what if there is too little space? Like we are trying to squeeze too many widgets into too small a row or column? Unless you do something about it, Flutter will clip the widgets which looks terrible and worse, may hide some widgets from the user.
So what do you do? You allow scrolling!
While it is possible to scroll in both directions, it creates some serious usability issues. So we recommend that you stick to scrolling in one direction only and that it usually be vertical scrolling. The easiest way to scroll is with a ListView.
The ListView widget
- 1.
new ListView – Normal use. It has a children property that takes a collection of static widgets.
- 2.
ListView.builder – For dynamically creating children from a list of items.
- 3.
ListView.separated – Like builder but also puts a widget ∗between∗ each item. Great for inserting ads in the list periodically. Read more at http://bit.ly/flutter_listview_separated.
- 4.
ListView.custom – For rolling your own advanced listviews. Read more at http://bit.ly/flutter_listview_custom.
Let’s take a look at the first two options starting with the regular ListView.
Regular ListView: When you have a few widgets to display
This version of ListView is great for a small number of widgets to display, but where ListView really shines is when you want to display a list of things – people, products, stores – anything you’d retrieve from a database or Ajax service. For displaying an indeterminate number of scrollable items, we’ll want the ListView.builder constructor.
ListView.builder: When you’re building widgets from a list of objects
The itemCount property is an integer that tells us how many things we’re going to draw so we usually set it to the length of the array/collection of things we’re scrolling through. The itemBuilder function receives two parameters: the context and an integer which is 0 for the first item and increments each time it is run.
We’ve covered laying out the scene including what to do if there is extra space on the scene or there isn’t enough of it. So let’s cover the last of our five topics, how to fine-tune the spacing and position of widgets. We’ll do this by exploring the box model.
Container widget and the box model
This would not work since Image widgets don’t have a padding, margin, or borders. But you know what does? Containers!
Web developers often apply these things by wrapping elements in a generic container called a <div> and then applying styles to create pleasant spacing for our web pages.
Tip
EdgeInsets.all(8.0) – Same value applied to all four sides evenly.
EdgeInsets.symmetric(horizontal: 7.0, vertical: 5.0) – Top and bottom are the same. Left and right are the same.
EdgeInsets.only(top: 20.0, bottom: 40.0, left: 10.0, right: 30.0) – Left, top, right bottom can all be different.
EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0) – Same as the preceding one but less typing.
Container( padding: EdgeInsets.all(5), child: Text("foo"), ), | Padding( padding: EdgeInsets.all(5), child: Text("foo"), ), |
These two are equivalent.
Alignment and positioning within a Container
Alignment can take on any of these values: topLeft, topCenter, topRight, centerLeft, center, centerRight, bottomLeft, bottomCenter, and bottomRight. Now, isn’t that easier to write and easier for your fellow devs to read?
Tip
Container( alignment: Alignment.center, child: Text("foo"), ), | Align( alignment: Alignment.center, child: Text("foo"), ), | Center( child: Text("foo"), ), |
These three are equivalent.
So how do you determine the size of a Container?
You may have noticed that I tried to slip width and height by you in that last section. Yes, you can tell a Container you want it to have a particular width and height, and it will comply when it is able. Width and height both take a simple number that can range from zero to double.infinity. The value double.infinity hints to be as large as its parent will allow.
Now, I know what you’re thinking. “Rap, what do you mean by ‘when it is able’ and ‘hints’? Aren’t there any hard rules? I want Container sizes to be predictable!” And I completely agree. A Container’s size is tough to predict until you know its rules. So, how does it decide then?
Remember two things. First, a Container is built to contain a child, but having a child is optional. 99% of the time it will have a child. The other 1% of the time we use the Container to provide a background color or to create spacing for its neighbors/siblings. Second, remember that Flutter determines layout in two phases, down the render tree to determine Box Constraints and then back up to determine RenderBox (aka “size,” remember?).
Flutter limits max size by passing Box Constraints down into the Container from its parent.
The Container is laid back as it tells its parent, “If my neighbors need some space, go ahead and take it. I’ll be as small as you need me to.”
If height and/or width is set, it honors those up to its max size as determined by its Box Constraints. Note that it is not an error for you to list a size greater than its Box Constraints, it just won’t grow any larger. This is why you can use double.infinity without error.
Tip
Setting height and width makes the Container super rigid; it locks in a size. While this is handy when you want to fine-tune your layout, the best practice is to avoid using them unless you have a darn good reason. You generally want to allow widgets to decide their own size.
In the 1% of the time that it has no child, it consumes all the remaining space up to its max Box Constraint.
But most of the time, it has a child so the layout engine looks at the child’s RenderBox.
If the child’s RenderBox is bigger than my Box Constraints, it clips the child which is a big, fat problem. It’s not technically an error, but it looks bad. So avoid it. When in debug mode, Flutter will draw yellow and black stripes where it has overflowed so the developer doesn’t miss it.
If the child’s RenderBox is within my Box Constraints, there is leftover room so we look at the alignment property. If alignment is not set, we put it in the upper-left corner and make the container tight – it shrinks to fit the child. Leftover room is just empty. If alignment is set, it makes the container greedy. This sort of makes sense when you think about it because how will it align top/bottom/left/right if it doesn’t add space by growing?
After all this, shrink as needed to honor the margins.
Special layout widgets
Like we said at the top of the chapter, we’ve now covered the tools you’ll need for 90% of your layout needs, but there are more. A few are worth a glance just so you know what to look for should the situation come up. These widgets are designed for very particular layout situations that, while common, aren’t everyday but need specialized tools to make happen.
Stack widget
This is for when you want to layer widgets so that they overlap one another. You want to stack them in the Z-direction. With Stack, you’ll list some number of widgets, and they’ll be displayed in that order one on top of another. The last one will occult (hide) the previous one if they overlap which will occult the one before that which will overlap the one before that and so on.
I was really torn about where to cover the stack widget. On one hand, it involves laying out a screen which fits much better in this chapter. But on the other hand, Stacks excel in creating cards which is definitely a styling concept and therefore fits better in the next chapter. We decided to mention it here but really focus on it in later. So stay tuned for that.
GridView widget
Here’s another thing borrowed from HTML and the Web. GridView is for displaying a list of widgets when you want them to appear in rows and columns but don’t particularly care which rows and which columns – you just want them to show up in a grid.
To use a GridView, you’ll set its children property to the list of widgets you want to display and it will create the grid populating across and then wrapping to the next row, resizing its space available until it just fits. And here’s the greatest part, it automatically scrolls!
GridView has two constructors, GridView.extent() and GridView.count().
GridView.extent()
GridView.count()
GridView.extent() is probably more useful because when the device is portrait, maybe you’ll have two columns, but when it goes landscape, you can now fit three columns in and the contents can still fit.
The Table widget
The GridView is great when displaying widgets in rows and columns that wrap. The wrapping part means that you really don’t care what children widgets end up in which row and column.
Caution
Anyone coming from an HTML background knows that you can lay out a page using HTML <table>s is possible but it is a bad idea. <table>s are for data, not for layout. Well it’s the same thing in Flutter. It is possible, but generally speaking, stay away from tables for laying out a page. But if you have data, Tables are the right choice.
How do you span columns? Like, for a table header for example. Unfortunately, you don’t with Flutter Table – yet. Stay tuned, though. There is a feature request for spanning columns.
Conclusion
I know this was a long chapter. But layouts in Flutter are not only hugely important but they’re also hugely complex because of the large number of layout widgets and the way that they interact with one another. But because understanding the algorithm can save you tons of hand-wringing and head-scratching later on we thought it would be wise to cover it in depth. We hope you’ll agree in the long run. After a couple more scans through this chapter and working with the widgets, we’re convinced that you’ll have Flutter layouts figured out.
Of course to have a complete app, you’ll need to create multiple scenes and be able to navigate between them. And how do you do that? We’ll cover that in the next chapter.