Skip to content

prefer_immutable_state

v1.0.0WarningFixConfigurableState Management

This rule flags a class whose name marks it as state and which is missing the @immutable annotation. Which names count is decided by name_pattern — State$ by default — and the check is then widened to every subclass and implementor.

This rule is in the opinionated preset.

State that can be mutated in place changes without anything observing the change. A Riverpod notifier that assigns to a field of its own state, rather than replacing the state object, updates nothing: the reference is the same, so no listener rebuilds and no widget redraws.

@immutable moves that failure to analysis time. The analyzer reports a non-final field where it is declared, instead of leaving the bug to surface as a screen that will not refresh.

This rule is deliberately state-management-agnostic: it covers Riverpod, a hand-rolled store, and any plain ...State value object equally.

See also: @immutable

A notifier’s state object with a mutable field. Assigning to it changes the value in place, so the reference never changes and nothing rebuilds:

class LoginFormState {
LoginFormState({this.email = '', this.isSubmitting = false});
String email;
bool isSubmitting;
}

@immutable makes the analyzer report the non-final field where it is declared, instead of leaving the bug to surface as a screen that will not refresh:

import 'package:meta/meta.dart';
@immutable
class LoginFormState {
const LoginFormState({this.email = '', this.isSubmitting = false});
final String email;
final bool isSubmitting;
LoginFormState copyWith({String? email, bool? isSubmitting}) =>
LoginFormState(
email: email ?? this.email,
isSubmitting: isSubmitting ?? this.isSubmitting,
);
}

name_pattern decides which names count. A project whose value objects end in Status as well:

analysis_options.yaml
many_lints:
rules:
prefer_immutable_state:
name_pattern: '(State|Status)$'
class UploadStatus { // reported under the pattern above
UploadStatus({this.progress = 0});
double progress;
}

Flutter State subclasses are never reported. Every State<T> is named ...State and every one of them is meant to hold mutable fields — controllers, setState values, subscriptions. They are excluded by type, not by name, and which base classes count follows the shared state_base_classes option.

Classes that already inherit @immutable are never reported. Flutter annotates Widget @immutable, so widget names ending in State — EmptyState, ErrorState, LoadingState — already carry what this rule asks for. The check is on supertypes, so it is not Flutter-specific: any base class annotated @immutable passes it down.

// Not reported: StatelessWidget inherits @immutable from Widget.
class PersonPickerEmptyState extends StatelessWidget {
const PersonPickerEmptyState({super.key});
@override
Widget build(BuildContext context) => const SizedBox.shrink();
}

A name matched in its entirety is not reported, so a class named exactly State — the bare affix — is never flagged.

For Bloc and Cubit, prefer_immutable_bloc_state recognises the state class through the Bloc<E, S> type argument instead, which is exact rather than a name match.

To disable this rule:

many_lints.yaml
rules:
prefer_immutable_state: false

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

Option Type Default Description
name_pattern regex State$ Pattern identifying state classes by name. A name matched in its entirety is not reported, so the bare affix itself is never flagged