flutter
/

Equatable with Bloc: Simplify State Equality in Flutter

Last Sync: Today

On this page

12
0%
Intermediate
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutterIntermediate

Equatable with Bloc: Simplify State Equality in Flutter

When using Bloc or Cubit, one of the most common performance pitfalls is unnecessary UI rebuilds. This happens because Bloc compares states using ==, and without proper equality implementation, two states that are logically identical may be considered different. Equatable solves this by providing a simple way to implement value‑based equality, ensuring your UI only rebuilds when the state actually changes.

The Problem: Reference Equality

By default, Dart uses reference equality for objects. Two separate instances with the same values are not considered equal:

DARTRead-only
1
class UserState {
  final String name;
  UserState(this.name);
}

final state1 = UserState('Alice');
final state2 = UserState('Alice');
print(state1 == state2); // false – different instances

When Bloc emits a new state, it compares the new state with the previous one using ==. If they are not equal, BlocBuilder and BlocListener trigger rebuilds. If you always create a new state instance (even when the data is the same), you'll cause unnecessary rebuilds, hurting performance.

What is Equatable?

Equatable is a Dart package that overrides == and hashCode for you. You simply extend Equatable and list the properties you want to be compared in a props getter. Then, two objects with the same values for those properties will be considered equal, regardless of whether they are the same instance.

Installation

YAMLRead-only
1
dependencies:
  equatable: ^2.0.5

Using Equatable in Bloc States

DARTRead-only
1
import 'package:equatable/equatable.dart';

abstract class AuthState extends Equatable {
  const AuthState();

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

class AuthInitial extends AuthState {}

class AuthLoading extends AuthState {}

class AuthSuccess extends AuthState {
  final String userId;
  const AuthSuccess(this.userId);

  @override
  List<Object?> get props => [userId];
}

class AuthFailure extends AuthState {
  final String message;
  const AuthFailure(this.message);

  @override
  List<Object?> get props => [message];
}

Using Equatable in Events

Events can also benefit from Equatable, especially when using event transformers or when you want to avoid duplicate event handling. It's not strictly required, but highly recommended.

DARTRead-only
1
import 'package:equatable/equatable.dart';

abstract class AuthEvent extends Equatable {
  const AuthEvent();

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

class LoginRequested extends AuthEvent {
  final String email;
  final String password;
  const LoginRequested(this.email, this.password);

  @override
  List<Object?> get props => [email, password];
}

How Equatable Prevents Unnecessary Rebuilds

Consider a scenario where you fetch data and then refresh it with the same data. Without Equatable, you'd create a new DataLoaded instance with the same list, but since it's a new object, Bloc thinks the state changed and rebuilds the UI. With Equatable, if the list contents are identical (and you've implemented equality correctly, e.g., using DeepEquality or Equatable for the list items), the states will be considered equal and no rebuild will occur.

DARTRead-only
1
class DataLoaded extends Equatable {
  final List<int> numbers;
  const DataLoaded(this.numbers);

  @override
  List<Object?> get props => [numbers];
}

// If numbers are [1,2,3] in both instances, states are considered equal.
final state1 = DataLoaded([1,2,3]);
final state2 = DataLoaded([1,2,3]);
print(state1 == state2); // true

Deep Equality with Collections

Equatable only compares the list references by default, not the contents. To compare the contents of collections, you need to either use immutable collections (like List.unmodifiable) or a deep equality helper. For most cases, if you're using immutable models with Equatable, the list contents will be compared if each item is also Equatable. Alternatively, you can use the ListEquality from the collection package.

DARTRead-only
1
import 'package:collection/collection.dart';

class DataLoaded extends Equatable {
  final List<int> numbers;
  const DataLoaded(this.numbers);

  @override
  List<Object?> get props => [const ListEquality().hash(numbers)];
}

Equatable with Freezed

freezed is a code generator that automatically creates immutable classes with copyWith, toString, and equality. It is often used as an alternative to manual Equatable implementations. Both work, but Freezed offers more features and less boilerplate for complex states.

DARTRead-only
1
import 'package:freezed_annotation/freezed_annotation.dart';

part 'auth_state.freezed.dart';

@freezed
class AuthState with _$AuthState {
  const factory AuthState.initial() = _AuthInitial;
  const factory AuthState.loading() = _AuthLoading;
  const factory AuthState.success(String userId) = _AuthSuccess;
  const factory AuthState.failure(String message) = _AuthFailure;
}

Performance Considerations

  • Use Equatable for all states and events – Prevents unnecessary rebuilds and makes debugging easier.
  • Keep the props list small – Only include fields that determine equality. Avoid including large objects that rarely change if they are not part of the logical state.
  • Be careful with collections – If you use mutable lists, the content may change without triggering a state change. Prefer immutable collections or ensure you emit a new state with a new list when content changes.
  • Use const constructors when possible – This can help with performance and prevents accidental instance duplication.

Common Mistakes

  • ❌ Forgetting to include props in subclasses – Causes equality to only compare the base class (which may have no fields), leading to incorrect equality. ✅ Always override props in each concrete state class.
  • ❌ Including non‑final fields – If fields are mutable, equality can become inconsistent. ✅ Make all fields final.
  • ❌ Omitting Equatable in events – If you use event transformers, you may miss events that are the same but different instances. ✅ Use Equatable for events as well.
  • ❌ Assuming deep equality for lists – Equatable compares references for lists. If you modify the list contents without creating a new list, the state won't be considered changed. ✅ Use immutable collections or a custom deep equality.

Next Steps

Now that you understand how to implement proper equality, explore more about Bloc state management:

  • Bloc States – Designing effective state classes.
  • Cubit vs Bloc – Choosing the right approach.
  • Bloc Testing – Writing tests for your blocs.

Conclusion

Equatable is a small but essential tool when working with Bloc or Cubit. By enabling value‑based equality, it prevents unnecessary UI rebuilds, reduces bugs, and makes your code more predictable. Always include it in your state and event classes for a smooth, performant Bloc experience.

Try it yourself

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

void main() => runApp(MyApp());

// State with Equatable
class CounterState extends Equatable {
  final int value;
  const CounterState(this.value);

  @override
  List<Object?> get props => [value];
}

class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(const CounterState(0));

  void increment() => emit(CounterState(state.value + 1));
  void decrement() => emit(CounterState(state.value - 1));

  // This will emit the same value multiple times
  void emitSame() => emit(CounterState(state.value));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  int rebuildCount = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Equatable Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BlocBuilder<CounterCubit, CounterState>(
              builder: (context, state) {
                rebuildCount++;
                return Column(
                  children: [
                    Text('Count: ${state.value}', style: TextStyle(fontSize: 24)),
                    Text('Builder rebuild count: $rebuildCount'),
                  ],
                );
              },
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => context.read<CounterCubit>().increment(),
              child: const Text('Increment'),
            ),
            ElevatedButton(
              onPressed: () => context.read<CounterCubit>().decrement(),
              child: const Text('Decrement'),
            ),
            ElevatedButton(
              onPressed: () => context.read<CounterCubit>().emitSame(),
              child: const Text('Emit Same Value'),
            ),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

What is the main benefit of using Equatable with Bloc?

A
It makes states serializable
B
It prevents unnecessary UI rebuilds by enabling value-based equality
C
It automatically generates copyWith methods
D
It makes code run faster
Q2
of 3

How do you implement Equatable in a state class?

A
Extend `Equatable` and override `hashCode`
B
Extend `Equatable` and override `props` getter
C
Mix in `EquatableMixin`
D
Use `@equatable` annotation
Q3
of 3

Why is it important to include all relevant fields in the `props` list?

A
To make the class immutable
B
To ensure two instances with the same field values are considered equal
C
To generate toString()
D
To improve performance

Frequently Asked Questions

Is Equatable required for Bloc/Cubit to work?

No, it's not required, but it's highly recommended. Without it, Bloc may rebuild the UI unnecessarily because every new state instance is considered different, even if the data is the same.

Can I use Equatable with Freezed?

Yes, Freezed generates equality code automatically, so you don't need to manually extend Equatable. Freezed is a popular alternative for immutable classes.

How do I handle equality for complex nested objects?

Ensure all nested classes also extend Equatable (or use Freezed). Then, Equatable will compare them properly when they are included in the props list of the parent state.

Does Equatable work with sealed classes?

Yes, you can extend Equatable on a sealed class and its subclasses. Just add props for each subclass accordingly. Dart 3's sealed classes work well with Equatable.

What's the performance impact of Equatable?

Equatable adds a small overhead for computing hash codes and equality. This is negligible compared to the benefit of avoiding unnecessary UI rebuilds.

Previous

bloc emit

Next

bloc provider

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.