Skip to content

prefer_container

v0.4.0WarningFixConfigurableWidget Replacement

Flags a chain of 3 or more nested layout widgets that Container can express in one call. The collapsible set is Padding, Align, Center, ColoredBox, DecoratedBox, ConstrainedBox, SizedBox, Transform, ClipRRect, ClipOval, ClipPath and FractionallySizedBox.

analysis_options.yaml
many_lints:
rules:
prefer_container: true
prefer_padding_over_container: false # the opposing rules;
prefer_align_over_container: false # leave them off
prefer_constrained_box_over_container: false
prefer_transform_over_container: false
Widget build(BuildContext context) {
return Padding( // LINT
padding: const EdgeInsets.all(8),
child: Align(
alignment: Alignment.center,
child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFF2196F3)),
child: const Text('Hello'),
),
),
);
}
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
alignment: Alignment.center,
decoration: const BoxDecoration(color: Color(0xFF2196F3)),
child: const Text('Hello'),
);
}
// Don't — Padding > Align > ConstrainedBox
Padding(
padding: const EdgeInsets.all(16),
child: Align(
alignment: Alignment.topLeft,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 120),
child: const Text('Total'),
),
),
);
// Do
Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft,
constraints: const BoxConstraints(minWidth: 120),
child: const Text('Total'),
);

Center becomes alignment: Alignment.center

Section titled “Center becomes alignment: Alignment.center”
// Don't
Center(
child: Padding(
padding: const EdgeInsets.all(12),
child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFFEEEEEE)),
child: const Text('Empty'),
),
),
);
// Do
Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(color: Color(0xFFEEEEEE)),
child: const Text('Empty'),
);

min_sequence is how many collapsible widgets must be stacked before the rule reports. With min_sequence: 4, the three-deep chain above is left alone:

analysis_options.yaml
many_lints:
rules:
prefer_container:
min_sequence: 4
// Not reported at min_sequence: 4 — only 3 deep
Padding(
padding: const EdgeInsets.all(8),
child: Align(
alignment: Alignment.center,
child: SizedBox(width: 100, child: Text('Hi')),
),
);

Lower it to 2 to catch every collapsible pair.

A chain with two widgets mapping to the same Container parameter is never reported, because Container can hold only one of them:

// Two Paddings both want `padding:` — not reported
Padding(
padding: const EdgeInsets.all(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Align(alignment: Alignment.center, child: Text('Hi')),
),
);

A widget Container cannot express — Opacity, IntrinsicHeight, IntrinsicWidth, LimitedBox — breaks the chain rather than being collapsed across, so neither half here reaches three:

// Not reported: Opacity splits the chain
Padding(
padding: const EdgeInsets.all(8),
child: Opacity(
opacity: 0.5,
child: Align(alignment: Alignment.center, child: Text('Hi')),
),
);

This rule trades nesting for a single widget, and that is not always a pure simplification. Container composes its parameters in one fixed order, so a chain whose order differs renders differently after the collapse. Check the diff before accepting the fix in these cases.

// Before: padding is outside the colour.
// The blue paints only the 100x50 box; the 8px border is transparent.
Padding(
padding: const EdgeInsets.all(8),
child: ColoredBox(
color: const Color(0xFF2196F3),
child: const SizedBox(width: 100, height: 50, child: Text('World')),
),
);
// After: Container paints the colour *behind* the padding.
// The blue now fills 100x50 including the 8px inset, and 100x50 became the
// OUTER size — the Text gets 84x34.
Container(
padding: const EdgeInsets.all(8),
color: const Color(0xFF2196F3),
width: 100,
height: 50,
child: const Text('World'),
);

If you want the original render, keep the nesting — or set the size on the inner child instead:

Container(
padding: const EdgeInsets.all(8),
color: const Color(0xFF2196F3),
child: const SizedBox(width: 100, height: 50, child: Text('World')),
);

Three arguments have no Container equivalent and are dropped by the quick fix. Re-add them by hand, or keep the widget:

Dropped Why
ClipRRect(borderRadius:) Container clips through decoration; the radius must move into a BoxDecoration
Transform(origin:) Container exposes transformAlignment, not a pixel origin
FractionallySizedBox(widthFactor:, heightFactor:) Container has no fractional sizing

A ClipRRect in the chain becomes clipBehavior: on the Container, which clips against the decoration rather than against a rounded rectangle of its own. With no decoration set, nothing is clipped.

Option Type Default Description
min_sequence int 3 Minimum number of consecutive Container-compatible widgets in a nesting chain before the rule reports
many_lints.yaml
rules:
prefer_container: false

To keep the rule on but skip certain paths, use per-rule exclude.