Widgets and layout

Stateless versus stateful widgets, how the layout constraints propagate, and the container widgets that do most of the work.

Stateless and stateful

A StatelessWidget is immutable: given the same inputs it always builds the same output. A StatefulWidget pairs an immutable widget with a long-lived State object that can call setState and rebuild.

WidgetHolds mutable stateTypical use
StatelessWidgetNoLabels, icons, rows, purely derived UI
StatefulWidgetYesForms, animations, local counters, anything with a controller
InheritedWidgetNoExposing data to a whole subtree (the basis of theming)
class Tag extends StatelessWidget {
  const Tag({super.key, required this.label});

  final String label;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.secondaryContainer,
        borderRadius: BorderRadius.circular(6),
      ),
      child: Text(label, style: Theme.of(context).textTheme.labelMedium),
    );
  }
}

Constraints flow down, sizes flow up

Each widget receives constraints from its parent and returns a size to it, then the parent positions the child. Most layout confusion disappears once you know which constraint your child was actually given.

Scaffold(
  body: Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [
      const Padding(
        padding: EdgeInsets.all(16),
        child: Text('Recent notes', style: TextStyle(fontSize: 20)),
      ),
      Expanded(
        child: ListView.builder(
          itemCount: notes.length,
          itemBuilder: (context, index) => ListTile(
            title: Text(notes[index].title),
            trailing: const Icon(Icons.chevron_right),
          ),
        ),
      ),
      SafeArea(
        top: false,
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: FilledButton(
            onPressed: addNote,
            child: const Text('New note'),
          ),
        ),
      ),
    ],
  ),
)
  • Column and Row are unbounded along their main axis, so a long child needs Expanded, Flexible or a scrollable such as ListView.
  • Stack overlays children; use Positioned when you need exact placement, and Align for corners.
  • SafeArea keeps content clear of notches and gesture bars. MediaQuery exposes the real window size and text scale.
  • Check the app at a large text scale and in landscape before calling a screen finished — overflow stripes are the usual result.
⚠️
A red overflow stripe means a child wanted more space than its parent allowed. Do not paper over it with a smaller font: give the flexible child an Expanded, make the parent scrollable, or wrap the text with Flexible and allow it to wrap.

FAQ

Why does my Column overflow but my ListView does not?
A scrollable gives its children unbounded main-axis space, so nothing overflows; it just scrolls. A Column gives its children the remaining fixed space, and it will not clip or scroll on its own.
Should every widget be its own class?
Extract a widget when it is reused, when it has state, or when the build method becomes hard to read. Small private helper methods are fine for a single call site.

Setup and your first app State, navigation and release builds

Last refreshed 2026-09-18.