Skip to content

avoid_unnecessary_consumer_widgets

v0.1.0 Warning Fix Widget Best Practices

Flags a Riverpod consumer widget that never touches its ref.

A consumer subscribes to the provider container and joins the dependency graph whether or not it reads anything from it. Dropping back to StatelessWidget removes that, and makes the widget’s dependencies visible in its declaration: it has none.

Covers ConsumerWidget, ConsumerStatefulWidget and HookConsumerWidget. The quick fix rewrites the base class and removes the unused ref parameter.

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

See also: Riverpod consumers

The usual origin: the ref.watch moved to the parent, and the base class was left as it was.

class Greeting extends ConsumerWidget {
const Greeting({required this.name, super.key});
final String name;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Text('Hello $name');
}
}
class Greeting extends StatelessWidget {
const Greeting({required this.name, super.key});
final String name;
@override
Widget build(BuildContext context) {
return Text('Hello $name');
}
}

A stateful consumer is judged on the whole State class

Section titled “A stateful consumer is judged on the whole State class”

ConsumerStatefulWidget keeps its ref as a getter on the companion ConsumerState, not as a build parameter — so a ref used in initState, a lifecycle method or an event handler counts just as much as one used in build:

// Not reported: the ref is used, just not in build
class _EditPageState extends ConsumerState<EditPage> {
@override
void initState() {
super.initState();
ref.read(analyticsProvider).log('opened');
}
@override
Widget build(BuildContext context) => const Text('Edit');
}

Sharing provider access through a mixin leaves the state body looking ref-free while the widget genuinely needs the container. That is recognised and not reported:

mixin AnalyticsMixin<T extends ConsumerStatefulWidget> on ConsumerState<T> {
void track(String event) {
ref.read(analyticsProvider).log(event);
}
}
class _EditPageState extends ConsumerState<EditPage> with AnalyticsMixin {
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => track('saved'),
child: const Text('Save'),
);
}
}

For a mixin declared in another file, the recognition is by its on ConsumerState<T> constraint — that constraint is why it can reach a ref at all.

It drops only the Riverpod half; the build may still call hooks, so the fix does not go all the way to StatelessWidget:

// Don't
class Counter extends HookConsumerWidget {
const Counter({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = useState(0);
return Text('${count.value}');
}
}
// Do
class Counter extends HookWidget {
const Counter({super.key});
@override
Widget build(BuildContext context) {
final count = useState(0);
return Text('${count.value}');
}
}

A HookConsumerWidget using neither hooks nor ref is reported here and by avoid_unnecessary_hook_widgets — the two answer different questions and each points at a different node.

Usage is detected by the name ref, anywhere in the relevant body. A widget that renames its parameter, or one whose body happens to mention an unrelated ref, is judged on the name rather than on resolution.

A ConsumerWidget whose build has no ref parameter at all is not reported — there is nothing to remove.

many_lints.yaml
rules:
avoid_unnecessary_consumer_widgets: false

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