Constraints, Expanded and Flexible

Flutter · Practical · 14 min

Almost every confusing layout bug is explained by the constraint model.

Flutter layout follows one rule: constraints go down, sizes go up, parent sets position.

1. A parent passes each child constraints: a min and max width and height. 2. The child picks its own size within those constraints and reports it up. 3. The parent decides where to place the child.

Consequences:

  • A widget can't choose any size it wants — only one inside its constraints. Container(width: 100) directly inside the screen fills the screen, because the screen forces tight constraints. Wrap it in Center or Align to loosen them.
  • A widget doesn't know its position, and can't size itself relative to the screen without being told.

In a Row or Column, non-flexible children get unbounded space along the main axis: they're asked to be as big as they like. Then:

  • `Expanded(child: ...)` forces the child to fill the leftover space, divided by flex factors.
  • `Flexible(child: ...)` lets the child be at most its share (FlexFit.loose).
  • Text that would overflow a Row should be in Expanded or Flexible with overflow: TextOverflow.ellipsis.

Errors decoded:

  • "RenderFlex overflowed by N pixels" — children are larger than the Row/Column; make one flexible or scrollable.
  • "Viewport was given unbounded height" — a scrollable inside a Column; give it bounded height with Expanded or SizedBox.
  • "BoxConstraints forces an infinite width" — a widget that wants to be infinitely wide inside an unbounded parent.

LayoutBuilder exposes the incoming constraints so you can choose a layout from the available space.

Example

import 'package:flutter/material.dart';
class ProductRow extends StatelessWidget {
  const ProductRow({super.key, required this.title, required this.price});
  final String title;
  final String price;

  @override
  Widget build(BuildContext context) => Row(children: [
        const Icon(Icons.shopping_bag),
        const SizedBox(width: 8),
        Expanded(                               // takes the remaining width
          child: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
        ),
        Text(price),                            // keeps its natural size
      ]);
}

class SplitPanes extends StatelessWidget {
  const SplitPanes({super.key});
  @override
  Widget build(BuildContext context) => Row(children: [
        Expanded(flex: 2, child: Container(color: Colors.teal, height: 80)),  // 2/3
        Expanded(child: Container(color: Colors.amber, height: 80)),          // 1/3
      ]);
}

Remember

  • Constraints go down, sizes go up, the parent positions
  • Expanded fills leftover space by flex; Flexible may be smaller
  • Center/Align loosen tight constraints so explicit sizes apply

Watch out for

  • Fixing overflow by hard-coding widths, which breaks on other screen sizes.

Check yourself

  1. Why does `Container(width: 100, height: 100, color: Colors.red)` as the app's root fill the whole screen?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/ui/layout/constraints