Flutter Layouts & Responsive Design

Expanded & Flexible

50 min Lesson 3 of 16

Introduction to Flexible Widgets

When building layouts in Flutter, you often need children to share available space proportionally rather than using fixed sizes. The Expanded and Flexible widgets solve this problem by giving children the ability to grow and shrink within a Row, Column, or Flex parent.

Both widgets wrap a child and assign it a flex factor that determines how much of the remaining space it receives. The key difference lies in how they handle the allocated space.

Note: Expanded and Flexible can only be used as direct children of Row, Column, or Flex. Placing them inside a Container or any other widget will cause an error.

The Expanded Widget

Expanded forces its child to fill all the remaining available space along the main axis. The child must take up exactly the space allocated to it -- no more, no less.

Basic Expanded Example

Row(
  children: <Widget>[
    Container(width: 80, height: 50, color: Colors.red),
    Expanded(
      child: Container(height: 50, color: Colors.green),
    ),
    Container(width: 80, height: 50, color: Colors.blue),
  ],
)
// Red: 80px | Green: fills remaining | Blue: 80px

In this example, the red and blue containers have fixed widths of 80px each. The green container, wrapped in Expanded, takes all the remaining width. If the Row is 400px wide, the green container gets 400 - 80 - 80 = 240px.

The Flexible Widget

Flexible is similar to Expanded but gives the child the option to be smaller than the allocated space. The child can size itself up to the maximum allocated space but is not forced to fill it entirely.

Basic Flexible Example

Row(
  children: <Widget>[
    Flexible(
      child: Container(
        width: 100,  // Only uses 100px even if more is available
        height: 50,
        color: Colors.orange,
      ),
    ),
    Container(width: 80, height: 50, color: Colors.purple),
  ],
)
// Orange: up to allocated space but only 100px | Purple: 80px

With Flexible, the orange container requests 100px. If the allocated space is 320px, the container still only takes 100px -- the remaining 220px stays empty. With Expanded, it would have been forced to stretch to 320px.

The flex Factor

Both Expanded and Flexible accept a flex parameter (default: 1) that determines the proportion of remaining space each child receives. The space is divided according to the ratio of flex values.

Proportional Space with flex

Row(
  children: <Widget>[
    Expanded(
      flex: 2,
      child: Container(height: 50, color: Colors.red),
    ),
    Expanded(
      flex: 1,
      child: Container(height: 50, color: Colors.green),
    ),
    Expanded(
      flex: 1,
      child: Container(height: 50, color: Colors.blue),
    ),
  ],
)
// Total flex = 2 + 1 + 1 = 4
// Red: 2/4 = 50% | Green: 1/4 = 25% | Blue: 1/4 = 25%
Tip: Think of flex values as parts of a whole. If you have flex values of 2, 1, and 1 (total 4), the first child gets 2 parts out of 4. You can use any positive integers -- flex: 3 and flex: 7 gives a 30%/70% split.

FlexFit: Tight vs Loose

The fundamental difference between Expanded and Flexible comes down to the FlexFit property:

  • FlexFit.tight (used by Expanded) -- The child must fill the allocated space completely. It is forced to be exactly the size of its allocation.
  • FlexFit.loose (used by Flexible) -- The child can be smaller than the allocated space. It sizes itself naturally, up to the maximum allocation.

FlexFit.tight vs FlexFit.loose

// Expanded is shorthand for:
Flexible(
  fit: FlexFit.tight,
  child: Container(height: 50, color: Colors.red),
)

// Flexible defaults to:
Flexible(
  fit: FlexFit.loose,
  child: Container(height: 50, color: Colors.blue),
)

// You can make Flexible behave like Expanded:
Flexible(
  fit: FlexFit.tight,
  flex: 1,
  child: Container(height: 50, color: Colors.green),
)
Note: Expanded is literally a convenience wrapper around Flexible(fit: FlexFit.tight). They are functionally identical when using tight fit.

The Spacer Widget

The Spacer widget is a convenience widget that creates an empty expanded space. It is equivalent to Expanded(child: SizedBox.shrink()). Spacer is useful for pushing children apart without creating visible elements.

Spacer Usage

Row(
  children: <Widget>[
    Text('Left', style: TextStyle(fontSize: 18)),
    Spacer(),  // Pushes 'Left' and 'Right' to opposite ends
    Text('Right', style: TextStyle(fontSize: 18)),
  ],
)

// With flex factor:
Row(
  children: <Widget>[
    Text('Start'),
    Spacer(flex: 2),  // Twice the space
    Text('Middle'),
    Spacer(flex: 1),  // Half the space
    Text('End'),
  ],
)
Tip: Spacer is cleaner than using Expanded(child: SizedBox()) and communicates intent better. Use it when you need flexible empty space between widgets.

Distributing Space Proportionally

A common requirement is splitting a layout into proportional sections. Here is how to create common split ratios:

Common Split Ratios

// 50/50 split
Row(
  children: [
    Expanded(flex: 1, child: Container(color: Colors.red)),
    Expanded(flex: 1, child: Container(color: Colors.blue)),
  ],
)

// 1/3 and 2/3 split
Row(
  children: [
    Expanded(flex: 1, child: Container(color: Colors.red)),
    Expanded(flex: 2, child: Container(color: Colors.blue)),
  ],
)

// 25/50/25 split
Row(
  children: [
    Expanded(flex: 1, child: Container(color: Colors.red)),
    Expanded(flex: 2, child: Container(color: Colors.green)),
    Expanded(flex: 1, child: Container(color: Colors.blue)),
  ],
)

Combining Expanded with Fixed-Size Widgets

The most practical pattern is mixing fixed-size widgets with Expanded widgets. Flutter first allocates space to fixed-size children, then distributes the remaining space among Expanded/Flexible children based on their flex factors.

Fixed + Expanded Layout

Row(
  children: <Widget>[
    // Fixed: avatar (56px)
    CircleAvatar(radius: 28, child: Icon(Icons.person)),
    SizedBox(width: 12),
    // Flexible: takes remaining space
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('John Doe', style: TextStyle(fontWeight: FontWeight.bold)),
          Text('Online', style: TextStyle(color: Colors.green, fontSize: 12)),
        ],
      ),
    ),
    // Fixed: action button
    IconButton(icon: Icon(Icons.more_vert), onPressed: () {}),
  ],
)

Practical Example: Split Layouts

A two-panel layout where the left panel is narrower and the right panel is wider:

Two-Panel Split Layout

Row(
  children: <Widget>[
    Expanded(
      flex: 1,
      child: Container(
        color: Colors.grey[200],
        child: ListView(
          children: [
            ListTile(title: Text('Item 1')),
            ListTile(title: Text('Item 2')),
            ListTile(title: Text('Item 3')),
          ],
        ),
      ),
    ),
    Expanded(
      flex: 3,
      child: Container(
        padding: EdgeInsets.all(16),
        child: Text('Content area', style: TextStyle(fontSize: 24)),
      ),
    ),
  ],
)

Practical Example: Sidebar + Content

A common application layout with a fixed-width sidebar and flexible content area:

Sidebar + Content Layout

Row(
  children: <Widget>[
    // Fixed sidebar
    Container(
      width: 250,
      color: Colors.blueGrey[900],
      child: Column(
        children: [
          SizedBox(height: 40),
          Text('Menu', style: TextStyle(color: Colors.white, fontSize: 20)),
          ListTile(
            leading: Icon(Icons.dashboard, color: Colors.white),
            title: Text('Dashboard', style: TextStyle(color: Colors.white)),
          ),
          ListTile(
            leading: Icon(Icons.settings, color: Colors.white),
            title: Text('Settings', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
    ),
    // Flexible content
    Expanded(
      child: Container(
        padding: EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Dashboard', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
            SizedBox(height: 16),
            Text('Welcome back! Here is your overview.'),
          ],
        ),
      ),
    ),
  ],
)

Practical Example: Proportional Columns

A statistics row where each stat card takes equal space:

Equal-Width Stat Cards

Row(
  children: <Widget>[
    Expanded(
      child: Card(
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            children: [
              Icon(Icons.people, size: 32, color: Colors.blue),
              SizedBox(height: 8),
              Text('1,234', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
              Text('Users', style: TextStyle(color: Colors.grey)),
            ],
          ),
        ),
      ),
    ),
    Expanded(
      child: Card(
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            children: [
              Icon(Icons.shopping_cart, size: 32, color: Colors.green),
              SizedBox(height: 8),
              Text('567', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
              Text('Orders', style: TextStyle(color: Colors.grey)),
            ],
          ),
        ),
      ),
    ),
    Expanded(
      child: Card(
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            children: [
              Icon(Icons.attach_money, size: 32, color: Colors.orange),
              SizedBox(height: 8),
              Text('\$9,876', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
              Text('Revenue', style: TextStyle(color: Colors.grey)),
            ],
          ),
        ),
      ),
    ),
  ],
)
Warning: Never use Expanded or Flexible inside a widget that provides unlimited space along the main axis (like a scrollable ListView). Expanded tries to fill “remaining space,” but if the space is infinite, Flutter cannot compute a size and throws an error. Use fixed sizes or SizedBox in scrollable contexts instead.

Summary

  • Expanded forces its child to fill all allocated space (FlexFit.tight).
  • Flexible allows its child to be smaller than allocated space (FlexFit.loose).
  • The flex factor controls proportional space distribution -- higher flex means more space.
  • Spacer creates empty flexible space, useful for pushing widgets apart.
  • Flutter allocates space to fixed-size children first, then distributes remaining space among flex children.
  • Expanded and Flexible must be direct children of Row, Column, or Flex.
  • Do not use Expanded/Flexible inside scrollable widgets with unbounded main axis.