In a Flutter Column with spacing, why does showBadge ? badge : const SizedBox.shrink() lay out differently from if (showBadge) badge, and which should you use?
answer
- a placeholder is still a child
- spacing counts every child
- spaceBetween divides by child count
- if element removes the slot
- reserve space: Visibility maintainSize
basics
~20 sA ternary always inserts a child, so a zero-size SizedBox.shrink still counts: Column's spacing adds one extra gap and spaceBetween or spaceAround shares free space with it; an if element removes the child, so prefer it unless the slot must be kept.
solid answer
~40 s`Row` and `Column` got a `spacing` parameter in Flutter 3.27. `RenderFlex` adds `spacing * (childCount - 1)` of fixed space, and `childCount` includes zero-size children, so a `SizedBox.shrink()` placeholder adds one extra gap and the visible children end up unevenly spaced. With `MainAxisAlignment.spaceBetween` or `spaceAround` the free space is divided by the number of children, so the invisible child also takes a share. `if (showBadge) badge` contributes no child at all, which gives the layout you meant and creates no placeholder widget. Keep a placeholder only when you deliberately want a slot, such as a fixed-size box or `Visibility(maintainSize: true)` that holds the space so the layout does not jump when the badge appears.
code
dart · 23 linesimport 'package:flutter/material.dart';
class MenuHeader extends StatelessWidget {
const MenuHeader({super.key, required this.showAdminBadge});
final bool showAdminBadge;
@override
Widget build(BuildContext context) {
return Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Signed in as Ana'),
// Adds nothing when false: one gap between the two texts.
if (showAdminBadge) const Chip(label: Text('Admin')),
// A ternary here would add a zero-size child and a second gap:
// showAdminBadge ? const Chip(label: Text('Admin')) : const SizedBox.shrink(),
const Text('Last login: today'),
],
);
}
}go deeper
Remember that if (cond) widget removes the child, while cond ? widget : SizedBox.shrink() still adds one, even if you cannot see it.
Explain how spacing and the space* alignments count children, and why a zero-size child still changes the result.
Spot placeholder ternaries in review, predict the spacing or alignment shift they cause, and know when a reserved slot via Visibility maintainSize is deliberate.
Set layout conventions for optional content, such as preferring if elements and documenting reserved slots, so spacing stays consistent across a shared component library.
## Two ways to make a child optional Inside a Flutter `children` list you can make a widget optional in two ways: - a **ternary with a placeholder**: `showBadge ? badge : const SizedBox.shrink()` (older code used an empty `Container()`); - a **collection if element**: `if (showBadge) badge`. They look equivalent on screen in the simplest case, because `SizedBox.shrink()` is a 0 by 0 box. They are not equivalent: the ternary always contributes **one child**, the `if` element contributes **zero or one**. Anything that counts children sees the difference. ## Spacing counts every child Flutter 3.27 added a `spacing` parameter to `Row` and `Column` (it lives on `Flex`). Inside `RenderFlex`, the fixed space is computed as `spacing * (childCount - 1)`, and `childCount` is the number of children in the list, whatever their size: | Children | Visible | Gaps added with `spacing: 8` | |---|---|---| | `[title, if (false) badge, date]` | 2 | 8 | | `[title, false ? badge : SizedBox.shrink(), date]` | 2 | 16 | With the placeholder, the title and date end up 16 pixels apart instead of 8, which reads as a bug in design review and is hard to spot in code. ## Alignment modes that share free space `MainAxisAlignment` also counts children when it distributes leftover space: - `spaceBetween` divides free space by `childCount - 1`; - `spaceAround` and `spaceEvenly` divide it by `childCount` or `childCount + 1`. A zero-size placeholder therefore takes a share of the space, shifting the visible children away from where they would sit if the badge were simply absent. ## Which to choose 1. **Default to the `if` element.** It says exactly what you mean, adds no extra element to the widget tree, and keeps spacing and alignment correct. 2. **Use `if ... else`** when either of two widgets must appear; it still contributes exactly one child, but both branches are real content. 3. **Keep a deliberate placeholder** only when the slot itself matters: - a fixed-size `SizedBox(width: 24)` that reserves room so neighbours do not jump when the badge appears; - `Visibility(visible: showBadge, maintainSize: true, maintainAnimation: true, maintainState: true, child: badge)`, which keeps the badge's size while hiding it. `Visibility` defaults its `replacement` to `const SizedBox.shrink()`, so without the `maintain*` flags it behaves like the ternary: a zero-size child that still counts. ## Beyond layout Two more differences are worth knowing: - **Flexible siblings**: a zero-size placeholder takes no flex share, so `Expanded` siblings are unaffected; the difference shows up in `spacing` and the `space*` alignments, not in flex distribution. - **Element identity** across rebuilds depends on each child's position, type and key. Adding or removing a child with `if` changes the positions of the siblings after it; whether that matters is a question of keys, which belongs to the widget-tree topic. ## Why the bug hides The placeholder version passes every obvious check: - it compiles and never throws; - a screenshot with the badge visible looks identical to the `if` version; - widget tests that only look for the badge with `find.text` pass in both states. The defect only shows as an **extra gap** in the hidden state, often noticed late by a designer comparing screens. That is why reviewers treat `cond ? w : const SizedBox.shrink()` inside a `children` list as a smell: nothing is gained, and one layout state is quietly wrong. A golden test of the hidden state is the reliable way to catch it. ## Review checklist - Search for `SizedBox.shrink()` and `Container()` on the false side of ternaries inside `children` lists and replace them with `if` elements. - Check any `Row` or `Column` that uses `spacing` or a `space*` alignment after such a change; the layout will tighten. - When a placeholder is intentional, leave a comment saying why.
- How does Visibility relate to the ternary-with-placeholder pattern in Flutter?`Visibility` swaps its child for `replacement`, which defaults to `const SizedBox.shrink()`, so by default it behaves like the ternary: a zero-size child that spacing and alignment still count. Its value is the `maintain*` flags; `maintainSize` (which requires `maintainAnimation` and `maintainState`) keeps the hidden child's size, reserving the slot on purpose.
- Why did the placeholder problem exist before Flutter 3.27 added spacing?Gaps were written as `SizedBox(height: 8)` children between items. A conditional item replaced by a placeholder left the gaps on both sides of it in place, doubling the space. An `if (show) ...[item, const SizedBox(height: 8)]` group removed the item and its gap together.
A placeholder is like an empty chair left at a dinner table: nobody sits there, but the host still spaces every seat around it. An if element takes the chair away, and the remaining seats close up.
saying these in an interview costs you the question
- A zero-size SizedBox.shrink child is ignored by Column's spacing
- Ternary placeholders and if elements always produce identical layouts
- if elements force the whole Column to rebuild more often than ternaries
- Visibility with default flags removes the child from layout entirely
- Container() is a cheaper placeholder than an if element