Answer
Overview
Equatable is a Dart package that simplifies value equality by automatically implementing
text
==text
hashCodeThe Problem Without Equatable
Dart uses referential equality by default (compares memory addresses, not values).
dartclass User { final String name; final int age; User(this.name, this.age); } void main() { final user1 = User('Alice', 25); final user2 = User('Alice', 25); print(user1 == user2); // false ❌ (different instances) // Without overriding ==, BLoC can't detect identical states if (previousState == currentState) { /* Never enters! */ } }
Solution: Equatable Package
Add to
text
pubspec.yamlyamldependencies: equatable: ^2.0.5
Usage:
dartimport 'package:equatable/equatable.dart'; class User extends Equatable { final String name; final int age; const User(this.name, this.age); List<Object?> get props => [name, age]; // Define which fields matter } void main() { final user1 = User('Alice', 25); final user2 = User('Alice', 25); print(user1 == user2); // true ✅ (same values) }
How It Works
Equatable overrides
text
==text
hashCodedart// What Equatable does internally (simplified): bool operator ==(Object other) { if (identical(this, other)) return true; return other is User && other.name == name && other.age == age; } int get hashCode => name.hashCode ^ age.hashCode;
With BLoC State Management
Equatable prevents unnecessary widget rebuilds.
dart// ❌ Without Equatable class CounterState { final int count; CounterState(this.count); } // BLoC emits new state emit(CounterState(5)); emit(CounterState(5)); // ❌ Rebuilds widget even though count is same! // ✅ With Equatable class CounterState extends Equatable { final int count; const CounterState(this.count); List<Object?> get props => [count]; } emit(CounterState(5)); emit(CounterState(5)); // ✅ No rebuild (Equatable detects same value)
Advanced Features
Ignore Specific Fields
dartclass User extends Equatable { final String id; final String name; final DateTime lastLogin; // Don't compare this const User(this.id, this.name, this.lastLogin); List<Object?> get props => [id, name]; // lastLogin not included }
Nullable Fields
dartclass Profile extends Equatable { final String username; final String? bio; // Nullable const Profile(this.username, this.bio); List<Object?> get props => [username, bio]; // Handles nulls }
Stringify (Debugging)
dartclass User extends Equatable { final String name; final int age; const User(this.name, this.age); List<Object?> get props => [name, age]; bool get stringify => true; // Auto-generate toString() } print(User('Alice', 25)); // User(Alice, 25) ✅
With Collections
dartclass TaskList extends Equatable { final List<String> tasks; const TaskList(this.tasks); List<Object?> get props => [tasks]; } final list1 = TaskList(['a', 'b']); final list2 = TaskList(['a', 'b']); print(list1 == list2); // true ✅ (deep equality)
Comparison Table
| Without Equatable | With Equatable |
|---|---|
| Manual text | Automatic via text |
| Manual text | Auto-generated |
| Error-prone | Less bugs |
| Verbose boilerplate | Concise |
| BLoC over-rebuilds | Optimized rebuilds |
Best Practices
dart// ✅ Use const constructors with Equatable class User extends Equatable { final String name; const User(this.name); // const = compile-time constant List<Object?> get props => [name]; } // ✅ Include all relevant fields in props List<Object?> get props => [id, name, email]; // All important fields // ❌ Don't include derived/computed fields class User extends Equatable { final String firstName; final String lastName; String get fullName => '$firstName $lastName'; // Don't add to props List<Object?> get props => [firstName, lastName]; // Only stored fields }
When to Use
- ✅ BLoC states/events — Prevents duplicate state emissions
- ✅ Riverpod providers — Correctly detects state changes
- ✅ Data models — User, Product, etc. (value objects)
- ✅ Testing — Compare expected vs actual results
Learn more: Equatable Package