Sparround

Immutable state: freezed, Equatable, == and rebuilds

Immutability is not an aesthetic choice in Flutter — it is the precondition for every optimisation mechanism. The reason is simple: Flutter and the state management libraries decide "what changed" using ==.

Each of these mechanisms depends on ==:

  • InheritedWidget.updateShouldNotify — whether dependents wake.
  • ValueNotifier — its value setter compares with ==.
  • Selector and context.select in Provider — no rebuild when the selected value is unchanged.
  • select in Riverpod and the recomputation of providers.
  • Bloc's duplicate-state rule — no change occurs when state == nextState.
  • BlocSelector — the docs require the selected value to be immutable.

With a mutable object the same instance is modified: == sees no difference, the filter never fires, and you end up at one of two extremes — the UI never updates, or everything rebuilds all the time.

ToolWhat it givesWhat to watch
Hand-written `==`/`hashCode`No dependencyEasy to forget when a field is added
`package:equatable`A short `==` via a `props` listIf a new field is missing from `props`, the comparison lies
`package:freezed``==`, `hashCode`, `copyWith` and sealed unions via code generation`build_runner` build time; generated files must be managed in the repo
Dart `record`sValue semantics out of the box, no code generationNo named type; not suited to large models

An important update about freezed. The package's documentation now marks the when/map methods as legacy, explaining that as of Dart 3, Dart has built-in pattern matching using sealed classes, so you no longer need to rely on Freezed's generated methods. The recommended path is switch expressions and if-case statements.

copyWith, meanwhile, is still fully generated and works in a deep (nested) form as well.

Knowing this detail helps in an interview: code using when/map still works, but new code should prefer Dart 3 pattern matching.

dart
// 1) Equatable: props siyahısı — yeni sahə əlavə edildikdə buraya da yazılmalıdır.
class CartState extends Equatable {
  const CartState({this.items = const [], this.promo});
  final List<Item> items;
  final String? promo;

  CartState copyWith({List<Item>? items, String? promo}) =>
      CartState(items: items ?? this.items, promo: promo ?? this.promo);

  @override
  List<Object?> get props => [items, promo];
}

// 2) freezed: ==, hashCode, copyWith generasiya olunur; sealed union-lar dəstəklənir.
@freezed
sealed class CheckoutState with _$CheckoutState {
  const factory CheckoutState.idle() = CheckoutIdle;
  const factory CheckoutState.submitting() = CheckoutSubmitting;
  const factory CheckoutState.success(Receipt receipt) = CheckoutSuccess;
  const factory CheckoutState.failure(String message) = CheckoutFailure;
}

// Dart 3 pattern matching — freezed sənədinin tövsiyə etdiyi yol
// (when/map artıq legacy sayılır).
Widget build(BuildContext context) => switch (state) {
      CheckoutIdle() => const CheckoutForm(),
      CheckoutSubmitting() => const CircularProgressIndicator(),
      CheckoutSuccess(:final receipt) => ReceiptView(receipt: receipt),
      CheckoutFailure(:final message) => ErrorView(message: message),
    };

// 3) record: kiçik, adsız dəyərlər üçün — kod generasiyası lazım deyil.
typedef SearchFilter = ({String query, int page});
final filter = (query: 'telefon', page: 1); // == dəyər üzrə işləyir

Immutable state, three ways.

Immutable state has a real cost too: every change creates a new object. In large lists, writing [...items, item] on every emit means an O(n) copy. For small lists that is irrelevant; with thousands of items you need pagination, ListView.builder and a structure that limits the change surface (keeping the list as an id → item map, for instance). Noting that balance is more mature than "immutable is always better".

📚 Sources and documentation

  • package:freezedofficialpub.dev

    The legacy status of when/map and the Dart 3 pattern-matching recommendation are documented here.

  • package:equatableofficialpub.dev
  • package:json_serializableofficialpub.dev

    Serialising immutable models — usually used together with freezed.

  • Bloc: modeling stateofficialbloclibrary.dev

    The source of the @immutable, copyWith, const and Equatable recommendations.