Skip to content

prefer_single_widget_per_file

v0.4.0WarningConfigurableWidget Best Practices

Flags every public widget class in a file after the first. Private widgets (_Name) and non-widget classes are not counted by default.

One widget per file makes the file system an index of the widget library: the file name tells you what is in it, an import pulls in exactly what it says, and a diff touches one widget. Private helpers stay next to the widget they serve.

This rule is in the opinionated preset, so it is on with preset: opinionated and preset: pedantic.

invoice_card.dart
class InvoiceCard extends StatelessWidget {
const InvoiceCard({super.key});
@override
Widget build(BuildContext context) => const Text('Invoice');
}
// The second public widget in the file is the one reported
class InvoiceList extends StatelessWidget {
const InvoiceList({super.key});
@override
Widget build(BuildContext context) => const Text('Invoices');
}
// invoice_card.dart — one public widget per file
class InvoiceCard extends StatelessWidget {
const InvoiceCard({super.key});
@override
Widget build(BuildContext context) => const _InvoiceHeader();
}
// Private helpers are fine in the same file
class _InvoiceHeader extends StatelessWidget {
const _InvoiceHeader();
@override
Widget build(BuildContext context) => const Text('Invoice');
}

InvoiceList moves to invoice_list.dart.

ignore_private_widgets: false makes a _Header count like any other widget — the strict reading of one widget per file:

many_lints.yaml
rules:
prefer_single_widget_per_file:
ignore_private_widgets: false
invoice_card.dart
class InvoiceCard extends StatelessWidget {
const InvoiceCard({super.key});
@override
Widget build(BuildContext context) => const _InvoiceHeader();
}
// Now reported under the config above
class _InvoiceHeader extends StatelessWidget {
const _InvoiceHeader();
@override
Widget build(BuildContext context) => const Text('Invoice');
}

A widget existing only so tests can drive something is a deliberate extra, not a file that needs splitting. ignore_visible_for_testing: true exempts it:

many_lints.yaml
rules:
prefer_single_widget_per_file:
ignore_visible_for_testing: true
class InvoiceCard extends StatelessWidget {
const InvoiceCard({super.key});
@override
Widget build(BuildContext context) => const Text('Invoice');
}
@visibleForTesting
class InvoiceCardHarness extends StatelessWidget { // not reported
const InvoiceCardHarness({super.key});
@override
Widget build(BuildContext context) => const InvoiceCard();
}

Only widget classes are counted. A file may hold any number of enums, extensions, typedefs and plain classes without being reported — prefer_single_declaration_per_file covers those.

The report lands on the second widget and every one after it, not on the file, so a file with four widgets carries three diagnostics.

analysis_options.yaml
many_lints:
rules:
prefer_single_widget_per_file:
ignore_private_widgets: false
ignore_visible_for_testing: true
Option Type Default Description
ignore_private_widgets bool true Skip widgets whose name starts with _
ignore_visible_for_testing bool false Skip widgets annotated @visibleForTesting
many_lints.yaml
rules:
prefer_single_widget_per_file: false

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