Declarative UI Lists in Dart with Collection If and Collection For
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
addcall. - Performance: The compiler builds the list in a single allocation, avoiding repeated
addoverhead. - 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
- Forgetting the spread operator when you want to inline a sub‑list. Without
...you end up with a nestedList<Widget>which the framework treats as a single child. - Mixing
ifwithelseinside 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. - 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.