© Rap Payne 2019
R. PayneBeginning App Development with Flutterhttps://doi.org/10.1007/978-1-4842-5181-2_6

6. Laying Out Your Widgets

Rap Payne1 
(1)
Dallas, TX, USA
 

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.

So to get us where we need to be, we really must know how to do five things:
  1. 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).
    ../images/482531_1_En_6_Chapter/482531_1_En_6_Fig1_HTML.jpg
    Figure 6-1

    Title and menu appear at the top along with other things like action buttons

     
  2. 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.
    ../images/482531_1_En_6_Chapter/482531_1_En_6_Fig2_HTML.jpg
    Figure 6-2

    A ListView can place widgets above and below each other

    ../images/482531_1_En_6_Chapter/482531_1_En_6_Fig3_HTML.jpg
    Figure 6-3

    We might create a PersonCard widget

    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.
    ../images/482531_1_En_6_Chapter/482531_1_En_6_Fig4_HTML.jpg
    Figure 6-4

    Row widgets and Column widgets can be used to place things

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

     
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig5_HTML.jpg
Figure 6-5

Fine-tuned spacing

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.

../images/482531_1_En_6_Chapter/482531_1_En_6_Fig6_HTML.jpg
Figure 6-6

Without visual debugging turned on

../images/482531_1_En_6_Chapter/482531_1_En_6_Fig7_HTML.jpg
Figure 6-7

With visual debugging turned on

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.

To turn this feature on
  • 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.

Widget build(BuildContext context) {
  return MaterialApp(
    home: MainWidget(),
    title: "Ch 6 Layouts",
    theme: ThemeData(primarySwatch: Colors.deepPurple),
    routes: <String, WidgetBuilder>{
      '/scene1: (BuildContext ctx) => MyWidget1(),
      '/scene2: (BuildContext ctx) => MyWidget2(),
      '/scene3: (BuildContext ctx) => MyWidget3(),
    },
    debugShowCheckedModeBanner: false,
  );
}

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.

Scaffold has one purpose in life: to lay out the visible structure of your app to give it the predictable and therefore usable layout that so many other apps have. It creates, among other things:
  • 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

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: MyAppBar(),
    drawer: MyNavigationDrawer(),
    body: TheRealContentOfThisPartOfTheApp(),
    floatingActionButton: FloatingActionButton(
      child: Icon(Icons.add),
      onPressed: () { /* Do things here */},
    ),
    bottomNavigationBar: MyBottomNavBar,
  );
}

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

To create a header bar at the top of the screen, use an AppBar widget (Figure 6-8). This is strictly optional. But your users are totally going to expect an AppBar for almost every app that isn’t a game. You’ll almost always have a title. And you may want to add an Icon at the start. An Icon is the leading property:
return Scaffold(
  appBar: AppBar(
      leading: Icon(Icons.traffic),
      title: Text("My Cool App"),
  ),
  /* More stuff here. FAB, body, drawer, etc. */
);
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig8_HTML.jpg
Figure 6-8

The AppBar widget with a leading icon and a title

One problem though. If you have the leading icon and also a navigation drawer, then Flutter can’t use that space to display the hamburger menu (Figure 6-9):
return Scaffold(
  appBar: AppBar(
      /* No leading this time. */
      title: Text("My Cool App"),
  ),
  /* More stuff here. FAB, body, drawer, etc. */
);
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig9_HTML.jpg
Figure 6-9

An AppBar without a leading icon is able to display a hamburger menu icon

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.

Simply wrap the SafeArea widget around all of your body content and let it do the heavy lifting for you. Putting it inside the Scaffold but around the body is a terrific place:
return Scaffold(
  drawer: LayoutDrawer(),
  body: SafeArea(
    child: MyNormalBody(),
  ),
  floatingActionButton: FloatingActionButton(
    child: Icon(Icons.next),
    onPressed: () {},
  ),
);

SnackBar widget

Weird name, I know. Sounds like something delicious, but this widget is really a standard way to alert your user to something. A SnackBar (Figure 6-10) will appear at the bottom of your screen, occulting whatever is already down there and will disappear after a short time. You get to decide what the SnackBar says and you can even place a button on it for the user to take action.
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig10_HTML.jpg
Figure 6-10

A SnackBar shows a message and optional actions

You can show a SnackBar in any scene you like as long as you do it in a widget that is nested inside a Scaffold:
GestureDetector(
  child: PersonCard(person),
  onTap: () {
    String msg = '${person['name']['first']} deleted.';
    final SnackBar sb = SnackBar(
      content: Text(msg),
      duration: Duration(seconds: 5),
      action: SnackBarAction(
        textColor: Colors.white,
        label: "UNDO",
        onPressed: () {},
      ),
    );
    Scaffold.of(context).showSnackBar(sb);
  }))

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

I guarantee that at some point in your career, you’re going to see Flutter throw this error:
══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════
The following assertion was thrown during performLayout(): RenderFlex children have non-zero flex but incoming width constraints are unbounded. When a row is in a parent that does not provide a finite width constraint, for example if it is in a horizontal scrollable, it will try to shrink-wrap its children along the horizontal axis. Setting a flex on a child (e.g. using Expanded) indicates that the child is to expand to fill the remaining space in the horizontal direction. These two directives are mutually exclusive. If a parent is to shrink-wrap its child, the child cannot simultaneously expand to fit its parent.
It’s not the most developer-friendly error message, is it? Most of us would have no hope of understanding the problem in our code with that error message. Similar messages may say “Vertical viewport was given unbounded height” error. Or “RenderViewport does not support returning intrinsic dimensions.” None of these are very helpful. If they were being kind, they’d have said something like:
══╡ YOU'RE DOING IT WRONG ╞═════════════════════════
The ListView you're drawing wants to be infinitely tall and it needs a parent widget that will keep it reasonably short. Maybe tell it to be small by wrapping it with a LimitedBox widget?

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

In your custom widget, you have a root widget at the top of your main method. It has branches and branches of branches and on and on. Let’s call this the widget tree (Figure 6-11). Flutter has to decide how big to make each widget in the tree. It does so by asking each widget how big it would prefer to be and asking its parent if that is okay.
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig11_HTML.png
Figure 6-11

Every scene has a widget tree

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

Row and Column, as the names suggest, are made for laying out widgets side by side (Row, Figure 6-12) or above and below (Column, Figure 6-13). Other than how they lay out their children, they’re nearly identical.
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig12_HTML.png
Figure 6-12

A Row widget lays out side by side

Row(
  children: <Widget>[
    Widget(),
    Widget(),
    Widget(),
    Widget(),
  ],
),
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig13_HTML.png
Figure 6-13

A Column widget lays out above and below

Column(
  children: <Widget>[
    Widget(),
    Widget(),
    Widget(),
    Widget(),
  ],
),

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

Occasionally you’ll want two things above and below when the device is in landscape and side by side when in portrait. So you want them in a row at times and a column at others. This is when you’ll use a Flex widget which can do both. It has an orientation property that will be conditional:
Flex(
  direction:
    MediaQuery.of(context).orientation == Orientation.landscape ?
       Axis.horizontal : Axis.vertical,
  children: <Widget>[
    SomeWidget(),
    SomeWidget(),
    SomeWidget(),
  ],
),
This doesn’t happen as often as you might think. Use it sparingly.

Your widgets will never fit!

It would be an overwhelming coincidence if the elements fit perfectly in a scene. And if they ever fit perfectly, as soon as the app is run on a different screen size or rotated from portrait to landscape, that will change. So we need to handle two situations:
  1. 1.

    What if there’s extra space left over? (more screen than pixels taken up by the widgets)

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

MainAxisAlignment is a property of the Row or Column (Figure 6-14). With it you control how the extra space is allocated with respect to the widgets along the main axis – vertical for columns and horizontal for rows:
child: Column(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: <Widget>[
    SubWidget(),
    SubWidget(),
You have a few choices:
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig14_HTML.jpg
Figure 6-14

mainAxisAlignment says how to distribute the extra space along the main axis

crossAxisAlignment

crossAxisAlignment is also a property of the Row or Column; it decides where to put the extra space if the widgets are of different heights in a row or widths in a column (Figure 6-15). Your options are
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig15_HTML.png
Figure 6-15

crossAxisAlignment says how to distribute extra space along the cross axis

There’s also one more: baseline. But it only makes sense in a row, and it is much less frequently used.

Tip

If you want the children of a Column to all be the same width but not necessarily the entire width of the screen, use the IntrinsicWidth widget. With crossAxisAlignment.stretch, they all stretch to the maximum width (Figure 6-16), but wrapped in an IntrinsicWidth, they’ll all be the same size as the largest widget (Figures 6-17 and 6-18).
child: IntrinsicWidth(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: <Widget>[ ...
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig16_HTML.jpg
Figure 6-16

Without IntrinsicWidth, all members will stretch to the entire width

../images/482531_1_En_6_Chapter/482531_1_En_6_Fig17_HTML.jpg
Figure 6-17

With IntrinsicWidth, they’ll only be as wide as the widest member

../images/482531_1_En_6_Chapter/482531_1_En_6_Fig18_HTML.jpg
Figure 6-18

With Intrinsic width and a wider member, all are made wider

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

Let’s take this code for an example:
Row(
  mainAxisAlignment: MainAxisAlignment.spaceAround,
  children: <Widget>[
    SubWidget(),
    SubWidget(),
    SubWidget(),
    SubWidget(),
    SubWidget(),
    SubWidget(),
  ],
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig19_HTML.jpg
Figure 6-19

This Row widget has lots of extra space

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.

Here’s the same thing but with an Expanded() around the second widget:
Row(
  mainAxisAlignment: MainAxisAlignment.spaceAround,
  children: <Widget>[
    SubWidget(),
    Expanded(child: SubWidget()),
    SubWidget(),
    SubWidget(),
    SubWidget(),
    SubWidget(),
  ],
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig20_HTML.jpg
Figure 6-20

The second widget is wrapped in an Expanded

Note that the mainAxisAlignment now makes no difference because there is no extra space. It’s all eaten up by the Expanded.

What if we add another Expanded? Let’s put one around the third and fourth widgets also (Figure 6-21):
Row(
  children: <Widget>[
    SubWidget(),
    Expanded(child: SubWidget()),
    Expanded(child: SubWidget()),
    Expanded(child: SubWidget()),
    SubWidget(),
    SubWidget(),
  ],
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig21_HTML.jpg
Figure 6-21

Expandeds will divide the free space among them

Note that the second one is now smaller because the extra space is shared with the third and fourth widgets, divided equally among them.

But wait! There’s more! We can control how much space each Expanded gets. The Expanded has a property called the flex factor which is an integer. When the Row/Column is laid out, the rigid elements are sized first. Then the flexible ones are expanded according to their flex factor (Figure 6-22). In the preceding examples, the Expandeds had the default flex factor of 1 so they got an equal amount of space. But if we gave them different flex factors, they’ll expand at different rates:
Row(
 children: <Widget>[
    SubWidget(),
    Expanded(flex: 1, child: SubWidget()),
    Expanded(flex: 3, child: SubWidget()),
    Expanded(flex: 2, child: SubWidget()),
    SubWidget(),
    SubWidget(),
  ],
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig22_HTML.jpg
Figure 6-22

Expandeds have flex property to control how much extra space each gets

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

Expanded eats up all the free space. But if you want to use Expandeds but you also want there to be some space between certain children, use the Spacer or SizedBox widgets (Figure 6-23). Spacers have a flex factor that plays well with all the other flex factors along this axis. The SizedBox has height and width properties for when you want to express its size in pixels:
Row(
  children: <Widget>[
    SubWidget(),
    Spacer(),
    Expanded(flex: 1, child: SubWidget()),
    Spacer(flex: 2),
    Expanded(flex: 3, child: SubWidget()),
    Expanded(flex: 2, child: SubWidget()),
    SubWidget(),
    SizedBox(width: 10,),
    SubWidget(),
  ],
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig23_HTML.jpg
Figure 6-23

Spacer() and SizedBox() add free space back in but put you in control as to where and how much

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

ListView has actually has four different ways to use it:
  1. 1.

    new ListView – Normal use. It has a children property that takes a collection of static widgets.

     
  2. 2.

    ListView.builder – For dynamically creating children from a list of items.

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

Generically, a ListView takes a small number of other widgets and makes it scrollable. Why a “small number”? Because this is designed to be a static list, one that you, the developer, simply types into the build() method by hand. In fact, oftentimes the way you discover you’ll need a regular ListView is when your column overflows. The fix is either to remove children, resize the children, or simply change the Column to a ListView. Columns and ListViews both have a children property:
Widget _build(BuildContext context) {
 return ListView(
  children: <Widget>[
    FirstWidget(),
    SecondWidget(),
    ThirdWidget(),
  ],
 );
}

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

ListView’s alternative constructor, ListView.builder receives two parameters, an itemCount and an ItemBuilder property that is a function. This makes the ListView lazy-loaded. The itemBuilder function dynamically creates children widgets on demand. As the user scrolls close to the bottom of the list, itemBuilder creates new items to be scrolled into view. And when we scroll something far enough off the screen, it is paged out of memory and disposed of. Pretty cool.
Widget _build(BuildContext context) {
  return ListView.builder(
    scrollDirection: Axis.vertical,
    itemCount: _people.length,
    itemBuilder: (BuildContext context, int i) {
      return PersonCard(_peopleList[i]);
    },
  );
}

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

Flutter has borrowed heavily from other technologies including HTML and the Web which have the ideas of borders, padding, and margin. These are collectively called the box model. They’re used to create pleasant-to-the-eyes spacing around and between screen elements. It’s a battle-proven concept that has worked great for the Web so why not borrow it for Flutter?
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig24_HTML.png
Figure 6-24

The box model defines padding, border, and margin

Let’s say that we have a sized image that we want framed so to speak with a padding of 8, a margin of 10, and a border of 1. Flutter newcomers might try this first:
Image.network(
  _peopleList[i]['picture']['thumbnail'],
  padding: 8.0,
  margin: 10.0,
  border: 1.0,
),

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.

Flutter doesn’t have a <div>, but it does have a div-like widget called a Container which ... well ... contains other things. In fact, its entire life purpose is to apply layout and styles to the things inside of it. An HTML <div> can hold multiple things, but a Flutter Container only holds one child. It has properties called padding, margin, and decoration. We’ll leave decoration for the styles chapter, but padding and margin are awfully handy for creating nice-looking spacing:
Container(
  padding: EdgeInsets.all(8.0),
  margin: EdgeInsets.all(10.0),
  decoration: BoxDecoration(border: Border.all(width: 1.0)),
  child:   Image.network(thePicture),
  // Container has *lots* of other properties, many of which
  // we'll cover in the Styles chapter.
),

Tip

Margin and padding might have been easier to learn if they had just allowed us to list four number values representing the four sides. (They couldn’t make it easy, could they?) Instead, we use a helper widget called EdgeInsets.
  • 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.

Also note that if you want padding only – no other formatting – the Padding widget is a shorthand.

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

When you place a small child widget in a large Container, there will be more space in the Container than is needed by its child widget. That child widget will be located in the top-left corner by default. You have the option of positioning it with the alignment property:
Container(
  width: 150, height: 150,
  alignment: Alignment(1.0, -1.0),
  child:   Image.network(
    _peopleList[i]['picture']['thumbnail'],
  ),
),
Those alignment numbers represent the horizontal alignment (–1.0 is far left, 0.0 is center, and 1.0 is far right) and the vertical alignment (–1.0 is top, 0.0 is center, and 1.0 is bottom). See Figure 6-25.
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig25_HTML.png
Figure 6-25

Alignment coordinate system with 0,0 at the center

But you will probably prefer to use English words rather than numbers when you can:
Container(
  width: 150, height: 150,
  alignment: Alignment.centerLeft,
  child:   Image.network(
    _peopleList[i]['picture']['thumbnail'],
  ),
),

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

The Align widget is a shorthand for specifying the alignment and no other properties. The Center widget is merely a shorthand for centering.

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

We go top down:
  • 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.

Then we go bottom up:
  • 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()

Extent refers to the maximum width of the child. GridView will only let its kids grow to that size. If they try to get bigger, it puts another element on that row and shrinks them all until they just fit across the entire width. Take a look at this example:
Widget build(BuildContext context) {
  return GridView.extent(
    maxCrossAxisExtent: 300.0,
    children:
      people.map<Widget>((dynamic person) =>
                          PersonCard(person)).toList(),
  );
}
Notice in Figures 6-26 and 6-27 how the containers resize to something less than 300. GridView decides that it can fit two across in portrait orientation. But when rotated, those two would have resized to something bigger than 300 so it puts three on each row.
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig26_HTML.jpg
Figure 6-26

GridView.extent() in portrait

../images/482531_1_En_6_Chapter/482531_1_En_6_Fig27_HTML.jpg
Figure 6-27

The same GridView.extent() in landscape mode

GridView.count()

With the count() constructor, you specify how many columns you want regardless of orientation. GridView takes care of resizing its contents to fit. In the following example, we’ve told GridView.count() that we want two columns regardless of the orientation and the GridView sizes its children to fit exactly two across Figures 6-28 and 6-29:
Widget build(BuildContext context) {
  return GridView.count(
    crossAxisCount: 2,
    children:
        people.map<Widget>((dynamic person) =>
                            PersonCard(person)).toList(),
  );
}
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig28_HTML.jpg
Figure 6-28

GridView.count() in portrait orientation

../images/482531_1_En_6_Chapter/482531_1_En_6_Fig29_HTML.jpg
Figure 6-29

The same GridView.count() in landscape orientation

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.

Rows and Columns are best when you do care in which row and column the children exist. They’re rigid when you want them to be. Unfortunately, the columns can’t talk to each other so they will often be misaligned (Figures 6-30 and 6-31).
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig30_HTML.png
Figure 6-30

Rows work but the columns are misaligned

../images/482531_1_En_6_Chapter/482531_1_En_6_Fig31_HTML.png
Figure 6-31

Columns work but the rows are misaligned

The Table widget fixes that problem. It is rigid like nested Rows and Columns, but each row and column is aware of the others and lines up nicely like GridView (Figure 6-32).
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig32_HTML.png
Figure 6-32

A Table aligns the rows and columns

Every Table widget will have children, a List of TableRow widgets. And each TableRow widget will have children, a List of widgets:
return Table(
  children: <TableRow>[
        TableRow(children: <Widget>[
          Text('Salesperson', style: bold,),
          Text('January', style: bold,),
          Text('February', style: bold,),
          Text('March', style: bold,),
        ]
        ),
        TableRow(children: <Widget>[
          Text('Dwight'),
          Text('3742'),
          Text('5573'),
          Text('4323'),
        ],),
        TableRow(children: <Widget>[
          Text('Phyllis'),
          Text('3823'),
          Text('4500'),
          Text('3277'),
        ],
        ),
      ],
    );
The preceding code would produce Figure 6-33.
../images/482531_1_En_6_Chapter/482531_1_En_6_Fig33_HTML.jpg
Figure 6-33

A Table widget lines up rows and columns simultaneously

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.

No matter what their contents, table columns are given equal portions of the width unless you override it with the columnWidths property. The following would give the first column 30% of the width and divide the remaining 70% evenly across the remaining columns:
return Table(
  columnWidths: {0: FractionColumnWidth(0.3)},
  children: <TableRow>[ ...

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.136.159