Flutter for Beginners: Layouts with Rows, Columns, and Constraints

Capítulo 4

Estimated reading time: 11 minutes

+ Exercise

How Flutter Lays Out Widgets: Constraints Go Down, Sizes Go Up

Flutter layout is driven by a simple negotiation between parent and child widgets. The parent gives constraints (minimum and maximum width/height). The child chooses a size within those constraints, then the parent positions the child.

  • Constraints go down: parents tell children how big they are allowed to be.
  • Sizes go up: children report the size they decided to take.
  • Parent sets position: alignment and layout widgets decide where the child sits.

This is why the same widget can look different depending on where it is placed. A Container inside a Center behaves differently than inside a Row, because the constraints differ.

Core building blocks you will use constantly

  • Container: convenience widget for size, padding, margin, decoration (color, border, radius), alignment.
  • Padding: adds inner space around a child.
  • Align and Center: position a child within available space.
  • Row and Column: lay out children horizontally/vertically.
  • Expanded and Flexible: control how children share remaining space in a Row/Column.
  • SizedBox: fixed spacing or fixed size box.
  • Spacer: flexible empty space in a Row/Column.

Challenge 1: Build a Profile Header (Row + Column + Alignment)

Goal: create a header with an avatar on the left and name/title on the right, aligned nicely and with spacing.

Step 1: Skeleton layout with Row and Column

We will use a Row for the horizontal structure and a Column for the text stack. Use SizedBox for spacing and CrossAxisAlignment.start to left-align the text.

Widget profileHeader() {  return Padding(    padding: const EdgeInsets.all(16),    child: Row(      crossAxisAlignment: CrossAxisAlignment.center,      children: [        Container(          width: 64,          height: 64,          decoration: BoxDecoration(            color: const Color(0xFFE0E0E0),            borderRadius: BorderRadius.circular(32),          ),          child: const Center(            child: Icon(Icons.person, size: 32, color: Color(0xFF616161)),          ),        ),        const SizedBox(width: 12),        Column(          crossAxisAlignment: CrossAxisAlignment.start,          children: const [            Text(              'Alex Johnson',              style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),            ),            SizedBox(height: 4),            Text(              'Flutter Developer',              style: TextStyle(fontSize: 14, color: Color(0xFF616161)),            ),          ],        ),      ],    ),  );}

Step 2: Add a trailing action using Spacer

Now add a button on the far right. A Spacer expands to fill remaining space, pushing the trailing widget to the end.

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

Download the app

Widget profileHeader() {  return Padding(    padding: const EdgeInsets.all(16),    child: Row(      children: [        Container(          width: 64,          height: 64,          decoration: BoxDecoration(            color: const Color(0xFFE0E0E0),            borderRadius: BorderRadius.circular(32),          ),          child: const Center(            child: Icon(Icons.person, size: 32, color: Color(0xFF616161)),          ),        ),        const SizedBox(width: 12),        Column(          crossAxisAlignment: CrossAxisAlignment.start,          children: const [            Text('Alex Johnson', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),            SizedBox(height: 4),            Text('Flutter Developer', style: TextStyle(fontSize: 14, color: Color(0xFF616161))),          ],        ),        const Spacer(),        Container(          width: 40,          height: 40,          decoration: BoxDecoration(            color: const Color(0xFFEEEEEE),            borderRadius: BorderRadius.circular(12),          ),          child: const Icon(Icons.edit, size: 20),        ),      ],    ),  );}

Why Spacer works: inside a Row, Spacer is like an Expanded widget with an empty child. It takes flexible width, leaving the rest aligned to the end.

Step 3: Use Align and Center intentionally

Center centers its child both horizontally and vertically within its own box. Align lets you choose a specific alignment (topRight, centerLeft, etc.). For example, to pin the edit button to the top-right of a taller header area, wrap the Row in a fixed-height Container and align the trailing widget.

Widget profileHeader() {  return Container(    height: 96,    padding: const EdgeInsets.symmetric(horizontal: 16),    child: Row(      children: [        Container(          width: 64,          height: 64,          decoration: BoxDecoration(            color: const Color(0xFFE0E0E0),            borderRadius: BorderRadius.circular(32),          ),          child: const Center(child: Icon(Icons.person, size: 32)),        ),        const SizedBox(width: 12),        Column(          mainAxisAlignment: MainAxisAlignment.center,          crossAxisAlignment: CrossAxisAlignment.start,          children: const [            Text('Alex Johnson', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),            SizedBox(height: 4),            Text('Flutter Developer', style: TextStyle(color: Color(0xFF616161))),          ],        ),        const Spacer(),        Align(          alignment: Alignment.topRight,          child: Container(            margin: const EdgeInsets.only(top: 8),            width: 40,            height: 40,            decoration: BoxDecoration(              color: const Color(0xFFEEEEEE),              borderRadius: BorderRadius.circular(12),            ),            child: const Icon(Icons.edit, size: 20),          ),        ),      ],    ),  );}

Challenge 2: Build a Card (Container + Padding + Constraints)

Goal: create a reusable card with a title, subtitle, and a right-aligned badge. This challenge focuses on spacing, decoration, and controlling sizes.

Step 1: Card container with padding and decoration

Widget infoCard() {  return Padding(    padding: const EdgeInsets.all(16),    child: Container(      padding: const EdgeInsets.all(16),      decoration: BoxDecoration(        color: const Color(0xFFFFFFFF),        borderRadius: BorderRadius.circular(16),        border: Border.all(color: const Color(0xFFE0E0E0)),      ),      child: Row(        children: [          Expanded(            child: Column(              crossAxisAlignment: CrossAxisAlignment.start,              children: const [                Text('Today\'s Progress', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),                SizedBox(height: 6),                Text('You completed 3 tasks', style: TextStyle(color: Color(0xFF616161))),              ],            ),          ),          const SizedBox(width: 12),          Container(            padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),            decoration: BoxDecoration(              color: const Color(0xFFE8F5E9),              borderRadius: BorderRadius.circular(999),            ),            child: const Text('OK', style: TextStyle(color: Color(0xFF2E7D32), fontWeight: FontWeight.w600)),          ),        ],      ),    ),  );}

What to notice: the left text area is wrapped in Expanded so it takes remaining width and allows the badge to keep its natural size.

Step 2: Add a fixed-height media area using SizedBox

Cards often include a thumbnail or chart area. Use SizedBox to enforce a height, and let width expand naturally.

Widget infoCardWithMedia() {  return Padding(    padding: const EdgeInsets.all(16),    child: Container(      decoration: BoxDecoration(        color: const Color(0xFFFFFFFF),        borderRadius: BorderRadius.circular(16),        border: Border.all(color: const Color(0xFFE0E0E0)),      ),      child: Column(        crossAxisAlignment: CrossAxisAlignment.stretch,        children: [          SizedBox(            height: 120,            child: Container(              decoration: const BoxDecoration(                color: Color(0xFFEEEEEE),                borderRadius: BorderRadius.only(                  topLeft: Radius.circular(16),                  topRight: Radius.circular(16),                ),              ),              child: const Center(child: Icon(Icons.show_chart, size: 36)),            ),          ),          Padding(            padding: const EdgeInsets.all(16),            child: Row(              children: [                Expanded(                  child: Column(                    crossAxisAlignment: CrossAxisAlignment.start,                    children: const [                      Text('Weekly Stats', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),                      SizedBox(height: 6),                      Text('Tap to view details', style: TextStyle(color: Color(0xFF616161))),                    ],                  ),                ),                const SizedBox(width: 12),                const Icon(Icons.chevron_right),              ],            ),          ),        ],      ),    ),  );}

CrossAxisAlignment.stretch makes children in the Column take the full available width (within constraints), which is useful for full-width sections.

Understanding Overflow Errors (and Fixing Them)

One of the most common beginner errors is a yellow/black striped warning with text like “A RenderFlex overflowed by XX pixels”. This usually happens in a Row or Column when children try to be bigger than the available space along the main axis.

Typical overflow scenario in a Row

If you place long text next to other widgets in a Row, the text may demand more width than is available.

Widget badRow() {  return Padding(    padding: const EdgeInsets.all(16),    child: Row(      children: const [        Icon(Icons.info),        SizedBox(width: 8),        Text('This is a very very very long piece of text that may overflow in a Row'),      ],    ),  );}

Fix 1: Wrap the text with Expanded or Flexible

Expanded forces the child to take remaining space. Flexible allows the child to be smaller than the remaining space if it wants. For text, both are commonly used; pair them with text overflow settings.

Widget fixedRow() {  return Padding(    padding: const EdgeInsets.all(16),    child: Row(      children: const [        Icon(Icons.info),        SizedBox(width: 8),        Expanded(          child: Text(            'This is a very very very long piece of text that may overflow in a Row',            maxLines: 1,            overflow: TextOverflow.ellipsis,          ),        ),      ],    ),  );}

Fix 2: Allow wrapping by using Flexible and multiple lines

Widget fixedRowWrap() {  return Padding(    padding: const EdgeInsets.all(16),    child: Row(      crossAxisAlignment: CrossAxisAlignment.start,      children: const [        Icon(Icons.info),        SizedBox(width: 8),        Flexible(          child: Text(            'This is a very very very long piece of text that may overflow in a Row. With Flexible it can wrap.',          ),        ),      ],    ),  );}

Typical overflow scenario in a Column

A Column inside a screen with limited height can overflow if its children have fixed heights or too much content.

  • If content should scroll: wrap the Column in a scrollable widget (commonly SingleChildScrollView).
  • If content should fit by sharing space: use Expanded/Flexible for parts that can shrink/grow.

In this chapter we focus on the layout tools listed, so the key idea is: when you see overflow in a Row/Column, look for a child that needs to become flexible (Expanded/Flexible) or needs a bounded size (SizedBox/Container constraints).

Challenge 3: Build a Two-Column Screen (Expanded + Flexible + Constraints)

Goal: create a layout with a left navigation column and a right content area. This is common on tablets and in landscape mode.

Step 1: Basic two-column structure

Use a Row with two children. The left side has a fixed width; the right side expands.

Widget twoColumnScreen() {  return Row(    children: [      Container(        width: 220,        color: const Color(0xFFF5F5F5),        child: Padding(          padding: const EdgeInsets.all(16),          child: Column(            crossAxisAlignment: CrossAxisAlignment.stretch,            children: const [              Text('Menu', style: TextStyle(fontWeight: FontWeight.w600)),              SizedBox(height: 12),              Text('Profile'),              SizedBox(height: 8),              Text('Settings'),              SizedBox(height: 8),              Text('Help'),            ],          ),        ),      ),      Expanded(        child: Padding(          padding: const EdgeInsets.all(16),          child: Column(            crossAxisAlignment: CrossAxisAlignment.start,            children: [              const Text('Content', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),              const SizedBox(height: 12),              infoCard(),              infoCardWithMedia(),            ],          ),        ),      ),    ],  );}

Important constraint detail: the right side is wrapped in Expanded so it receives a bounded width (the remaining width). Without Expanded, the right side might try to be as wide as it wants, causing layout issues.

Step 2: Use Flexible to control proportional widths

If you want the left and right columns to scale proportionally, use Flexible (or Expanded) with flex factors.

Widget twoColumnScreenProportional() {  return Row(    children: [      Flexible(        flex: 2,        child: Container(          color: const Color(0xFFF5F5F5),          child: const Padding(            padding: EdgeInsets.all(16),            child: Text('Menu area'),          ),        ),      ),      Flexible(        flex: 5,        child: Container(          color: const Color(0xFFFFFFFF),          child: const Padding(            padding: EdgeInsets.all(16),            child: Text('Content area'),          ),        ),      ),    ],  );}

Expanded vs Flexible: Expanded is a Flexible with fit: FlexFit.tight, meaning it must fill the allocated space. Flexible can be tight or loose; loose allows the child to be smaller than its allocation.

Step 3: Add spacing and separators with SizedBox and Container

To create a divider between columns, insert a 1-pixel wide Container. Use SizedBox for consistent gaps.

Widget twoColumnScreenWithDivider() {  return Row(    children: [      SizedBox(        width: 220,        child: Container(          color: const Color(0xFFF5F5F5),          child: const Padding(            padding: EdgeInsets.all(16),            child: Text('Menu'),          ),        ),      ),      Container(width: 1, color: const Color(0xFFE0E0E0)),      Expanded(        child: Container(          color: const Color(0xFFFFFFFF),          child: const Padding(            padding: EdgeInsets.all(16),            child: Text('Content'),          ),        ),      ),    ],  );}

Mini-Lab: Responsive Thinking with MediaQuery and LayoutBuilder

Responsive layout means your UI adapts to different screen sizes and orientations. At a basic level, you can:

  • Read the screen size using MediaQuery.
  • React to the actual constraints of a specific area using LayoutBuilder.

Part A: Use MediaQuery to switch layouts

In this lab, you will show a single-column layout on narrow screens and a two-column layout on wider screens.

Widget responsiveHome() {  return Builder(    builder: (context) {      final width = MediaQuery.sizeOf(context).width;      if (width < 600) {        return Padding(          padding: const EdgeInsets.all(16),          child: Column(            crossAxisAlignment: CrossAxisAlignment.stretch,            children: [              profileHeader(),              infoCard(),              infoCardWithMedia(),            ],          ),        );      }      return twoColumnScreenWithDivider();    },  );}

Step-by-step:

  • Get the screen width from MediaQuery.sizeOf(context).width.
  • Choose a breakpoint (here, 600 logical pixels).
  • Return a different widget tree depending on the width.

Part B: Use LayoutBuilder to respond to local constraints

MediaQuery tells you about the whole screen. LayoutBuilder tells you how much space a widget actually gets from its parent. This is useful when your widget is placed inside a panel, dialog, or split view.

Widget responsiveCard() {  return LayoutBuilder(    builder: (context, constraints) {      final isWide = constraints.maxWidth > 420;      return Container(        padding: const EdgeInsets.all(16),        decoration: BoxDecoration(          border: Border.all(color: const Color(0xFFE0E0E0)),          borderRadius: BorderRadius.circular(16),        ),        child: isWide            ? Row(                children: [                  const Icon(Icons.insights, size: 32),                  const SizedBox(width: 12),                  Expanded(                    child: Column(                      crossAxisAlignment: CrossAxisAlignment.start,                      children: const [                        Text('Insights', style: TextStyle(fontWeight: FontWeight.w600)),                        SizedBox(height: 6),                        Text('This card switches to a Row when it has enough width.'),                      ],                    ),                  ),                ],              )            : Column(                crossAxisAlignment: CrossAxisAlignment.start,                children: const [                  Icon(Icons.insights, size: 32),                  SizedBox(height: 12),                  Text('Insights', style: TextStyle(fontWeight: FontWeight.w600)),                  SizedBox(height: 6),                  Text('This card switches to a Column when space is tight.'),                ],              ),      );    },  );}

Step-by-step:

  • Wrap the widget in LayoutBuilder.
  • Check constraints.maxWidth (or maxHeight) to decide a layout.
  • Return a Row for wide constraints and a Column for narrow constraints.

Practical Checklist for Debugging Layout Issues

  • If a Row overflows: make the “greedy” child flexible using Expanded or Flexible, and consider maxLines/ellipsis for Text.
  • If a Column overflows: identify fixed-height children; convert some to flexible, or reduce fixed sizes with SizedBox.
  • If a widget is unexpectedly small: check if its parent gave tight constraints (for example, a fixed-size Container).
  • If alignment seems ignored: verify whether the widget actually has extra space to align within (Align/Center need room to reposition).
  • Prefer Padding for spacing inside, SizedBox for gaps between siblings, and Spacer for flexible gaps in Row/Column.

Now answer the exercise about the content:

In a Row where a long Text causes a “RenderFlex overflowed” warning, what is the best fix to keep the Text within the available width?

You are right! Congratulations, now go to the next page

You missed! Try again.

Overflows in a Row happen when a child tries to be wider than the available space. Using Expanded or Flexible makes the Text adapt to the remaining width, often combined with maxLines and ellipsis.

Next chapter

Flutter for Beginners: Styling with Themes, Text, and Assets

Arrow Right Icon
Free Ebook cover Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch
33%

Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.