Skip to content

protected_notifier_properties

v0.8.0 Warning Riverpod State

Flags access to state, stateOrNull, future or ref on a Notifier from outside the notifier that owns them. These members are part of a notifier’s internal API.

Reading notifier.state bypasses the provider system: the value is read once, and the reader is never rebuilt when it changes. Writing it from outside moves state transitions out of the notifier, which is where the rest of the codebase expects to find them. Going through the provider gives correct reactivity and keeps mutations in one place.

See also: Riverpod providers

A widget reaching into the notifier for the current value. It compiles, renders the right number once, and then never updates — notifier.state is a plain field read, not a subscription:

class CartNotifier extends Notifier<int> {
@override
int build() => 0;
void add() => state = state + 1;
}
final cartProvider = NotifierProvider<CartNotifier, int>(CartNotifier.new);
class CartBadge extends ConsumerWidget {
const CartBadge({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifier = ref.watch(cartProvider.notifier);
return Text('${notifier.state}'); // LINT
}
}

Writing it from outside is worse: the transition happens somewhere no reader of CartNotifier will look for it.

void resetCart(WidgetRef ref) {
ref.read(cartProvider.notifier).state = 0; // LINT
}

Read the value through the provider, so the widget is subscribed:

class CartBadge extends ConsumerWidget {
const CartBadge({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(cartProvider);
return Text('$count');
}
}

Mutate through a method the notifier exposes, so every transition lives in the notifier:

class CartNotifier extends Notifier<int> {
@override
int build() => 0;
void add() => state = state + 1;
void reset() => state = 0;
}
void resetCart(WidgetRef ref) {
ref.read(cartProvider.notifier).reset();
}

Inside the notifier itself the members are free to use — that is what they are for:

class CartNotifier extends Notifier<int> {
@override
int build() => 0;
Future<void> syncFromServer() async {
final remote = await ref.read(apiProvider).fetchCartCount();
state = remote;
}
}

The exemption is exact: access is allowed when the target’s type is the very class the code sits in. A helper mixin on the notifier, or a subclass reading super’s state through a differently-typed variable, is still reported.

Reads through a variable whose type is not resolved are not reported, since the rule matches the receiver by resolved type rather than by name.

This rule is in the opinionated preset, so it is on with preset: opinionated, or by name:

many_lints.yaml
rules:
protected_notifier_properties: true

To turn it off again:

many_lints.yaml
rules:
protected_notifier_properties: false

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