Master Flutter Layout: Container, Column, Row & Stack | (Ch. 7)

Blue tech-themed background with subtle grid lines, featuring a 3D tilted Flutter logo, white icons for Column, Row, Stack, and Container layout widgets, and clean white title text reading ‘Flutter Core Layout Widgets’.

Welcome to Chapter 7! In the last chapter, we took a deep dive into the *theory* of Stateless vs. Stateful widgets. Before that, in Project 1, we built a digital business card and got our first taste of layout by using Column, Center, and SizedBox.

We were a bit like chefs following a recipe—we knew the steps, but maybe not *why* each step worked. Why did mainAxisAlignment center things vertically? What did Card really do?

This chapter is where we become layout masters. The entire Flutter framework is built on a simple, powerful idea: "everything is a widget." But the *skeleton* that holds those widgets together is layout. If you can master layout, you can build *any* 2D user interface you can dream of.

We're going to focus on the "Big Four" of layout:

  • Container: The basic "box" or "div" for styling.
  • Column: For arranging widgets vertically.
  • Row: For arranging widgets horizontally.
  • Stack: For "layering" widgets on top of each other (like pancakes or Photoshop layers).

This is one of the most important chapters in the entire course. Take your time, and let's get building.

1. `Container`: The Magic Box

The Container widget is the most fundamental and versatile layout widget in Flutter. If you come from web development, it's very similar to a <div>, but on steroids.

A Container, by itself, is just an invisible, empty box. Its true power comes from its properties. It can hold a single child widget, but it can also be used as a "spacer" or a "decorator" all by itself.

Sizing (width & height)

By default, a Container with no child and no size will try to be as big as possible (it will fill its parent). If it has a child, it will shrink to fit that child.

We can give it a specific size:


@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Container(
        color: Colors.blue,
        width: 200.0,
        height: 100.0,
        child: Text('I am in a box!'),
      ),
    ),
  );
}

This gives us a 200x100 blue box with text in it. Simple!

Spacing (padding & margin)

This is a concept that confuses many beginners, but it's simple with the right analogy.

  • padding: Space inside the container, between the border and the child. Think of it as "padding" a box with cotton to protect what's inside.
  • margin: Space outside the container, between its border and the *next* widget. Think of it as "social distancing" from other widgets.

Let's see an example:


@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.grey,
    body: Center(
      child: Container(
        color: Colors.blue,
        width: 300.0,
        height: 300.0,
        
        // 10 pixels of "social distancing" on all sides
        margin: EdgeInsets.all(10.0),
        
        // 40 pixels of "cotton padding" on all sides
        padding: EdgeInsets.all(40.0),
        
        child: Container(
          color: Colors.red,
          child: Text('Hello'),
        ),
      ),
    ),
  );
}

In this code, the blue box will have 10 pixels of grey space around it (margin). The red box *inside* it will have 40 pixels of blue space around it (padding).

Styling (decoration & the color conflict)

This is the "gotcha" that hits every new Flutter dev. You can set a color directly, like we did above.

But what if you want rounded corners? Or a border? Or a shadow? For that, you need the decoration property, which takes a BoxDecoration.

CRITICAL ERROR: You cannot provide *both* a color property and a decoration property. The color property is just a shortcut for decoration: BoxDecoration(color: ...). If you provide both, Flutter throws an error.

The Fix: If you need *any* decoration, you must put the color *inside* the BoxDecoration.


@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Container(
        width: 300.0,
        height: 300.0,
        
        // This is how you do complex styling
        decoration: BoxDecoration(
          // 1. The color goes INSIDE
          color: Colors.lightGreen,
          
          // 2. Add a border
          border: Border.all(
            color: Colors.black,
            width: 3.0,
          ),
          
          // 3. Add rounded corners
          borderRadius: BorderRadius.circular(12.0),
          
          // 4. Add a drop shadow
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.5),
              spreadRadius: 5,
              blurRadius: 7,
              offset: Offset(0, 3), // changes position of shadow
            ),
          ],
        ),
        
        child: Center(child: Text('I am a fancy box!')),
      ),
    ),
  );
}

Alignment

What if your Container is big, but its child is small? Where does the child go? By default, it goes in the top-left.

We can use the alignment property to change this.


      child: Container(
        color: Colors.blue,
        width: 300.0,
        height: 300.0,
        
        // Tell the container to align its child
        alignment: Alignment.bottomRight,
        
        child: Text('I am small!'),
      ),

Now, the text "I am small!" will be in the bottom-right corner of the blue box.

2. The "Golden Rule" of Layout: Main Axis vs. Cross Axis

I cannot overstate this. If you understand this one concept, you will understand Column and Row instantly.

Column and Row are both "flex" widgets. They arrange a List of children along an "axis."

  • The Main Axis is the direction the widget *scrolls* in.
  • The Cross Axis is the other direction.

For a Column:

  • It arranges children vertically.
  • So, its Main Axis is Vertical (Y-axis).
  • Its Cross Axis is Horizontal (X-axis).

For a Row:

  • It arranges children horizontally.
  • So, its Main Axis is Horizontal (X-axis).
  • Its Cross Axis is Vertical (Y-axis).

Why does this matter? Because the two main layout properties are mainAxisAlignment and crossAxisAlignment.

This is brilliant. Instead of "verticalAlignment" or "horizontalAlignment," Flutter uses these two names. Now you know exactly what they mean:

  • mainAxisAlignment in a Column = Vertical alignment.
  • crossAxisAlignment in a Column = Horizontal alignment.
  • mainAxisAlignment in a Row = Horizontal alignment.
  • crossAxisAlignment in a Row = Vertical alignment.

3. `Column`: Stacking Vertically

This is our workhorse. We used it in Project 1. It takes a List<Widget> called children and arranges them from top to bottom.

MainAxisAlignment (Vertical Alignment)

This controls how the children are spaced out along the vertical (main) axis.


@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      // Let's try different values here!
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      
      children: <Widget>[
        Container(color: Colors.red, width: 100, height: 100),
        Container(color: Colors.green, width: 100, height: 100),
        Container(color: Colors.blue, width: 100, height: 100),
      ],
    ),
  );
}
  • .start: (Default) Puts all children at the top.
  • .center: Puts all children in a group in the middle.
  • .end: Puts all children at the bottom.
  • .spaceBetween: Puts space between the children. The first and last child are flush with the edges.
  • .spaceAround: Puts space "around" each child. (Half space on the ends).
  • .spaceEvenly: Puts equal space between all children *and* the ends.

CrossAxisAlignment (Horizontal Alignment)

This controls how the children are aligned on the horizontal (cross) axis.


@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      
      // How are the children aligned horizontally?
      crossAxisAlignment: CrossAxisAlignment.center,
      
      children: <Widget>[
        // Notice the different widths
        Container(color: Colors.red, width: 100, height: 100),
        Container(color: Colors.green, width: 300, height: 100),
        Container(color: Colors.blue, width: 150, height: 100),
      ],
    ),
  );
}
  • .start: (Default) Aligns all children to the left.
  • .center: Aligns all children to the center. (This is what we did in Project 1!).
  • .end: Aligns all children to the right.
  • .stretch: This is a powerful one. It forces all children to be as wide as the Column itself.

4. `Row`: Arranging Horizontally

A Row is *exactly* like a Column, but the axes are flipped. It arranges its children from left to right.

Main Axis: Horizontal
Cross Axis: Vertical

The Overflow Error & The `Expanded` Widget

Row has one *huge* problem that Column doesn't. A phone screen is very tall, so you can add many Containers to a Column.

But a phone screen is not very wide. What happens if you put too much stuff in a Row?


@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Row(
        children: <Widget>[
          Container(color: Colors.red, width: 200, height: 100),
          Container(color: Colors.green, width: 200, height: 100),
          // Uh oh! 200 + 200 is > screen width!
        ],
      ),
    ),
  );
}

You'll get the dreaded "Overflow" error: a yellow and black striped box. This is Flutter's way of screaming, "These widgets don't fit!"

The solution is the Expanded widget.

Expanded is a widget that tells its child, "I know you want a certain size, but just shut up and take up all the remaining empty space in the Row or Column."

It *must* be a child of a Row or Column.

Let's fix our error:


      child: Row(
        children: <Widget>[
          // This first Container is a fixed size
          Container(color: Colors.red, width: 100, height: 100),
          
          // This Expanded widget will take up ALL other space
          Expanded(
            child: Container(color: Colors.green, height: 100),
          ),
          
          Container(color: Colors.blue, width: 100, height: 100),
        ],
      ),

Now, the red and blue boxes will be 100px wide, and the green box will *expand* to fill the entire space in between them. No overflow!

You can also use Expanded with a flex factor to divide the space.


      child: Row(
        children: <Widget>[
          Expanded(
            flex: 2, // Take 2 "shares" of the space
            child: Container(color: Colors.red, height: 100),
          ),
          Expanded(
            flex: 1, // Take 1 "share" of the space
            child: Container(color: Colors.blue, height: 100),
          ),
        ],
      ),

In this example, the red container will be *twice as wide* as the blue one.

MainAxisAlignment (Horizontal Alignment)

This controls how children are spaced on the horizontal axis.

  • .start: (Default) Puts all children at the left.
  • .center: Puts all children in a group in the middle.
  • .end: Puts all children at the right.
  • .spaceBetween: Puts space between the children.

CrossAxisAlignment (Vertical Alignment)

This controls how children are aligned on the vertical axis. This is very useful if your Row has a fixed height.


      child: Container(
        height: 300.0, // Give the Row a fixed height
        color: Colors.grey,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          
          // How are children aligned vertically?
          crossAxisAlignment: CrossAxisAlignment.center,
          
          children: <Widget>[
            Container(color: Colors.red, width: 50, height: 50),
            Container(color: Colors.green, width: 50, height: 150),
            Container(color: Colors.blue, width: 50, height: 80),
          ],
        ),
      ),
  • .start: Aligns all children to the top of the Row.
  • .center: (Default) Aligns all children to the vertical center.
  • .end: Aligns all children to the bottom.

5. `Stack`: Layering Widgets

Column and Row are for 2D layout. What if you want to lay widgets on top of each other? Like text *on* an image?

For this, we use a Stack.

A Stack is like a stack of pancakes. It also takes a children list. The first widget in the list is the bottom layer. The last widget in the list is the top layer.

By default, all children in a Stack are aligned to the top-left.


@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Stack(
        children: <Widget>[
          // 1. Bottom Layer
          Container(
            color: Colors.green,
            width: 300,
            height: 300,
          ),
          
          // 2. Middle Layer
          Container(
            color: Colors.yellow,
            width: 200,
            height: 200,
          ),
          
          // 3. Top Layer
          Text('I am on top!'),
        ],
      ),
    ),
  );
}

This will show a green box, with a smaller yellow box on top of it, with the text on top of that. All are stuck in the top-left corner *of the Stack*.

To fix this, we can use Stack's alignment property (e.g., alignment: Alignment.center) to align all the *non-positioned* children.

The `Positioned` Widget

What if you want total control? What if you want one widget in the top-left and another in the bottom-right?

For this, you wrap a child of a Stack in a Positioned widget.

Positioned lets you "pin" a widget to any corner or edge of the Stack.


      child: Stack(
        // Set a default alignment for non-Positioned children
        alignment: Alignment.center,
        
        children: <Widget>[
          // 1. Bottom Layer (the background)
          Container(
            color: Colors.blue,
            width: 300,
            height: 300,
          ),
          
          // 2. This text is not Positioned, so it uses
          // the Stack's "Alignment.center"
          Text('I am in the center'),
          
          // 3. This widget is Positioned!
          Positioned(
            bottom: 10.0,
            right: 10.0,
            child: Container(
              color: Colors.red,
              width: 50,
              height: 50,
            ),
          ),
          
          // 4. This one is also Positioned
          Positioned(
            top: 10.0,
            left: 10.0,
            child: Text('I am in the corner'),
          ),
        ],
      ),

This is an incredibly powerful pattern. The most common use is to have a Container with a background image as the first child (bottom layer), and then use Positioned widgets to place text or buttons on top of it.

Conclusion: You are Now a Layout Architect

This was a massive chapter. If your head is spinning, that's normal! This is the densest part of Flutter. But you now have the "golden rules."

You've learned:

  • Container is your "magic box" for spacing (padding, margin) and styling (decoration).
  • The "Golden Rule": Main Axis (the direction of flow) and Cross Axis (the other one).
  • Column stacks vertically. mainAxisAlignment is vertical, crossAxisAlignment is horizontal.
  • Row stacks horizontally. mainAxisAlignment is horizontal, crossAxisAlignment is vertical.
  • Rows overflow! You must use Expanded to tell children how to share space and prevent errors.
  • Stack layers widgets. The first child is the bottom, the last child is the top.
  • Positioned gives you fine-grained control over where children live inside a Stack.

Here is the secret: There are no other "magic" layout widgets. 99% of all UIs in Flutter are just clever, nested combinations of these four widgets.

A Twitter UI? It's a Column. Each item in the Column is a Row. That Row contains a CircleAvatar (for the profile pic) and another Column (for the name and tweet text). It's just nesting these simple blocks.

In the next chapter, we'll move on to the next set of "essential" UI widgets: Text, Icon, and Image. We'll learn how to style text deeply and, most importantly, how to load images from our app's "assets" folder and from the internet.

Go play! Try to build a simple social media post. You have all the tools you need.

Comments