Skip to content

avoid_build_context_in_providers

v0.8.0 Warning Riverpod State

Flags a BuildContext parameter on a @riverpod provider — either a functional provider or a method of a @riverpod class.

Providers outlive the widgets that read them. A BuildContext held by a provider can easily refer to a widget that has already been unmounted, and using it then throws dependOnInheritedWidgetOfExactType was called on a defunct widget — or quietly reads stale inherited data. Passing the value you need instead keeps the provider independent of the widget tree, which is also what makes it testable without pumping a widget.

See also: Riverpod families

A provider that takes the context so it can read the locale itself. The context it captured belongs to a widget that may already be gone by the time the provider rebuilds:

@riverpod
Future<List<Article>> articles(Ref ref, BuildContext context) { // LINT
final locale = Localizations.localeOf(context);
return api.fetchArticles(locale.languageCode);
}

Every method of a @riverpod class is checked, not just build — an action method that takes a context to show a snackbar afterwards is the same problem:

@riverpod
class Checkout extends _$Checkout {
@override
CheckoutState build() => const CheckoutState.idle();
Future<void> submit(BuildContext context) async { // LINT
await api.submit();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Order placed')),
);
}
}

Pass the value the provider needs, and let the family key on it:

@riverpod
Future<List<Article>> articles(Ref ref, String languageCode) =>
api.fetchArticles(languageCode);
// At the call site, where a context is legitimately in scope:
class ArticleList extends ConsumerWidget {
const ArticleList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = Localizations.localeOf(context);
final articles = ref.watch(articlesProvider(locale.languageCode));
return Text('${articles.valueOrNull?.length}');
}
}

Let the notifier report the outcome through its state, and let the widget do the UI work:

@riverpod
class Checkout extends _$Checkout {
@override
CheckoutState build() => const CheckoutState.idle();
Future<void> submit() async {
state = const CheckoutState.submitting();
await api.submit();
state = const CheckoutState.placed();
}
}
class CheckoutButton extends ConsumerWidget {
const CheckoutButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(checkoutProvider, (previous, next) {
if (next is CheckoutPlaced) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Order placed')),
);
}
});
return ElevatedButton(
onPressed: () => ref.read(checkoutProvider.notifier).submit(),
child: const Text('Place order'),
);
}
}

Only @riverpod declarations are checked. An ordinary function or class taking a BuildContext is left alone:

Locale readLocale(BuildContext context) => Localizations.localeOf(context);

The parameter type must resolve to exactly BuildContext. A subtype, or a context wrapped in some project-specific holder, is not reported.

This rule is in the recommended preset, so it is on with preset: recommended or preset: opinionated. Add it to preset: core with avoid_build_context_in_providers: true.

To turn it off:

many_lints.yaml
rules:
avoid_build_context_in_providers: false

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