The problem with imperative list building

When you start a Flutter screen, you often end up with a List<Widget> that you fill imperatively: create an empty list, push a header, loop over a data source, conditionally add a divider, then finally return the list. The code works, but it spreads the UI description across several statements, making it harder to see the final widget tree at a glance.

Enter collection if and collection for

Dart 2.3 introduced two syntactic sugars that let you write the whole list as a single literal: collection if and collection for. They behave like the familiar if and for statements, but they appear *inside* a list, set, or map literal. The result is a declarative description that reads like the UI you actually want.

Real‑world scenario

Imagine a settings screen that shows a user’s profile header, a dynamic list of preference tiles, and an optional “beta features” section that only appears when the user opts in. With collection if/for the entire column can be expressed in one place.

Widget buildSettingsColumn(User user, List prefs, bool showBeta) {
  return Column(
    children: [
      // Static header – always present
      ProfileHeader(user: user),

      // Dynamically generated tiles
      for (final pref in prefs)
        PreferenceTile(preference: pref),

      // Optional beta section
      if (showBeta) ...[
        const Divider(),
        const Text('Beta features'),
        for (final beta in betaFeatures)
          BetaTile(feature: beta),
      ],
    ],
  );
}

Notice the spread operator (...) combined with if. It expands the inner list only when the condition is true, keeping the outer list flat.

Why this approach shines

  • Readability: The widget tree mirrors the visual hierarchy. No mental simulation of loops required.
  • Maintainability: Adding or removing a conditional block is a one‑line change; you never touch a separate add call.
  • Performance: The compiler builds the list in a single allocation, avoiding repeated add overhead.
  • Type safety: The list literal is type‑checked as a whole, so mismatched widget types surface at compile time.

Collection if/for are not just syntactic sugar — they enable the compiler to optimise list construction and give you a single source of truth for the UI.

Common pitfalls and how to avoid them

  1. Forgetting the spread operator when you want to inline a sub‑list. Without ... you end up with a nested List<Widget> which the framework treats as a single child.
  2. Mixing if with else inside a collection literal is not supported. Use a ternary expression or extract the conditional part into a helper method if you need an else branch.
  3. Over‑nesting – while you *can* nest collection for loops, deep nesting hurts readability. Keep the depth to two levels max; beyond that, factor out a builder method.

Putting it together in a reusable pattern

For screens that share the “header + dynamic list + optional footer” shape, extract a small helper:

List buildSection({
  required Widget header,
  required Iterable items,
  List? footer,
  bool showFooter = false,
}) => [
  header,
  for (final item in items) item,
  if (showFooter && footer != null) ...footer,
];

Now the settings column becomes:

Column(
  children: buildSection(
    header: ProfileHeader(user: user),
    items: prefs.map((p) => PreferenceTile(preference: p)),
    footer: [
      const Divider(),
      const Text('Beta features'),
      for (final b in betaFeatures) BetaTile(feature: b),
    ],
    showFooter: showBeta,
  ),
)

The helper isolates the structural pattern, letting each screen focus on its data.

Final thoughts

Collection if and collection for turned a frequent source of boilerplate into a clean, expressive idiom. They let you describe *what* the UI looks like instead of *how* to assemble it step by step. In a codebase where widgets are composed daily, that shift pays off in faster reviews, fewer bugs, and a clearer mental model for every developer who touches the file.