Equatable: value equality in Dart without writing the boilerplate
A Dart package that helps to implement value based equality without needing to explicitly override == and hashCode.
At a glance
- What is it?
- Equatable is a Dart package that gives a class value based equality by extending it and listing the fields that matter in `props`, so the `==` and `hashCode` overrides are written for you. It is MIT licensed, needs no code generation step, and requires your fields to be final, which is the trade it asks for in exchange for removing a recurring source of bugs.
- Who is it for?
- Adopt Equatable if you write Dart or Flutter and keep finding yourself copying the same `==` and `hashCode` override between your model classes, since the package removes that boilerplate without a build step and gives you consistent equality semantics across the model layer. Do not adopt it for mutable classes, because the package is designed to work only with immutable objects and a mutable field in `props` will break hash based collections.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 37 days ago.
- What is it written in?
- Mainly Dart, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The boilerplate Equatable removes
Comparing objects in Dart usually means overriding two members. The README states the problem directly: being able to compare objects often involves having to override the `==` operator as well as `hashCode`, and failing to do so can lead to inefficient code that does not behave as expected.
The reason is the default. By default `==` returns true if two objects are the same instance, so a `Person` class holding a name compares false against another `Person` with the same name. That is correct identity semantics and exactly what you do not want for a value type. The fix is a hand written override:
class Person {
const Person(this.name);
final String name;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Person &&
runtimeType == other.runtimeType &&
name == other.name;
@override
int get hashCode => name.hashCode;
}That block is a pile of ceremony per class, and it is the kind of code that gets copied from class to class with one field forgotten. Forget one field and the class compares equal when it should not, or the other way round. Forget `hashCode` and the class breaks inside sets and maps. Equatable exists to make that mistake impossible rather than merely less likely.
The user is a Dart or Flutter developer with a model layer, which is a large part of the Flutter ecosystem, and the package is on pub.dev with the homepage there. The value is not clever, it is uniform: every model class in the project gets the same equality semantics written the same way.
props is the contract, and final fields are the price
The mechanism is one overridden getter. You extend `Equatable` and list the fields that determine identity:
import 'package:equatable/equatable.dart';
class Person extends Equatable {
const Person(this.name);
final String name;
@override
List<Object> get props => [name];
}`props` is the whole contract. The base class derives `==` and `hashCode` from it, so the field list is the single place where a reader can see what makes two instances of the class the same value.
There is a constraint that the README is explicit about, and it is not a suggestion. Equatable is designed to only work with immutable objects, so all member variables must be final. The package points at the reason, noting that overriding a `hashCode` with a mutable value can break hash based collections.
That is the correct reason, and it is worth internalising rather than working around. A `hashCode` computed from a mutable field changes while the object is inside a `HashMap` or a `HashSet`, and the collection silently loses track of the entry. This is not a limitation of the package, it is a property of hash based collections in every language. A mutable model class has to solve that problem somehow, and the two real options are to make it immutable or to override equality by identity and accept the consequences.
The package also supports nullable fields, where the getter type widens to `List<Object?>`:
class Person extends Equatable {
const Person(this.name, [this.age]);
final String name;
final int? age;
@override
List<Object?> get props => [name, age];
}And it supports `const` constructors, which matters more than it first appears. A `const` constructor combined with final fields means instances can be created at compile time and compared cheaply, and it is what lets the class participate in constant expressions.
Installing it: one line in pubspec and a pub get
The installation is two steps, and both are spelled out in the README.
First, add the dependency to `pubspec.yaml`:
dependencies:
equatable: ^3.0.0The constraint matters. The current release line is v3, and a caret constraint on a major version means the project is accepting 3.x and not 4.x. Pinning the floor at `^3.0.0` is the documented form, and matching whatever your other packages require is worth doing before you take the dependency, since a major version bump in a package like this tends to come with breaking changes to the API.
Then install the dependencies:
# Dart
dart pub get
# Flutter
flutter packages getBoth commands are shown because the package works in plain Dart and in Flutter, and the toolchain differs. Nothing else is needed: no build runner, no generated files, no extra configuration.
That last point is the package's main claim, and it is worth examining rather than accepting. The README notes that other packages will actually generate the boilerplate for you, but that you still have to run the code generation step, which is not ideal. With Equatable there is no code generation needed.
The comparison is fair but narrower than it first appears. A code generation approach can produce more than equality, for example `copyWith`, `toJson` and `fromJson`, and generated code is faster at runtime than an inherited implementation. What Equatable trades away is the build step and the generated file, and what it trades in is a runtime cost for the equality check and a dependency on the props list staying correct. Whether that is the right trade depends on whether your team runs a build step comfortably, which many Dart projects already do for their own tooling.
The repository itself shows the package is taken seriously as a library. There is a `test/` directory, an `example/` directory with a `main.dart` and its own `pubspec.yaml`, a `benchmarks/` directory, a `coverage_badge.svg` and an `analysis_options.yaml`, and the README carries a build status badge and a code coverage badge.
stringify, toString and the global EquatableConfig
Equality is not the only thing a model class does in a log statement, and Equatable handles the second thing too.
By default, the `toString` implementation of a Dart object gives you `Instance of Person`, which is useless in a log line or a test failure message. Equatable can implement `toString` to include all the given props, and you opt in per class with a flag:
@override
bool get stringify => true;With that flag on a `Person` holding the name `Bob`, the output becomes `Person(Bob)` rather than `Instance of Person`. The README notes the flag defaults to false, so `toString` falls back to the default implementation unless you ask for it.
The same behaviour can be turned on globally for every `Equatable` instance:
EquatableConfig.stringify = true;And the precedence rule is stated clearly: if `stringify` is overridden for a specific `Equatable` class, then the value of `EquatableConfig.stringify` is ignored. The local configuration always takes precedence over the global one. That ordering is the sensible one, since a class that has explicitly declared its preference should not be overruled by a setting made somewhere else in the app.
One subtlety is worth knowing, and it is a source of confusion when output differs between a debug build and a release build. The README notes that `EquatableConfig.stringify` defaults to true in debug mode and false in release mode. So a class that does not override the flag will print `Person(Bob)` while you develop and `Instance of Person` in a shipped build. If you rely on readable output in production logs, set the flag explicitly rather than relying on the default.
A worked example in the README combines the pieces, showing a class with `props` and `stringify` together.
The mixin path when a superclass already exists
Extending `Equatable` is the straightforward route, and it is the one the README uses for every example. It is not always possible, though, because a Dart class can only extend one class, and if your class already has a superclass you cannot also extend `Equatable`.
The package's answer is a mixin. The README states that in this case you can still get the benefits of `Equatable` by using it as a `mixin`, and shows the shape with an `EquatableDateTime` class extending `DateTime`.
This matters more in practice than it might first appear, because well designed model classes often extend something. A domain object that wraps a `DateTime`, a class that extends a third party type, or a class that already extends another base class in your own codebase all hit this constraint. The mixin gives those classes the same equality behaviour without forcing a redesign of the inheritance tree.
The alternative approach in those cases is to write the `==` and `hashCode` override by hand, which brings back the original problem, or to restructure the class so the equality-bearing data lives in a separate object that can extend `Equatable`. The mixin is the least disruptive of the three, and it is a good example of the package accommodating real codebases rather than an idealised one where every model class is free to pick its parent.
Version 3, the MIT licence, and the mutable-object trap
The version history is short and tells you what to expect. v3.0.0 was tagged on 2026-09-15, v2.1.0 on 2026-07-05 and v2.0.8 on 2026-01-04, and the last push to the default branch `master` was on 2026-09-16. The repository is not archived, and the release cadence on the 2.x line was regular enough to suggest a maintained package rather than an abandoned one.
The v3.0.0 tag is a major version, and the dependency constraint in the README is `^3.0.0`, so a project moving from the 2.x line should expect to review its usage against the changelog before taking the bump. For a package whose entire API surface is a base class, a `props` getter and a `stringify` flag, the surface area for breakage is small, but the constraint is still a major one and worth reading about.
The licence is MIT, and the README carries the MIT badge alongside the build and coverage badges. For a package this small and this widely used in the Flutter ecosystem, that is the expected choice, and it means a project can vendor or modify it with only the notice obligation. There is a `CODE_OF_CONDUCT.md` and a `CONTRIBUTING.md` in the tree, and a Discord link in the README for support.
The one real trap is the immutability rule, and it is the thing to check in a code review. If a class extends `Equatable` and has a mutable field, the equality and hash behaviour is wrong in a way that will not throw. A field that changes after the object is in a `HashSet` changes the hash, and the set can no longer find it. A field that is excluded from `props` while another field of the same class is included means two instances that differ in the excluded field compare equal, which then produces a false positive in a `contains` check or a widget rebuild decision in Flutter.
The review checklist is therefore short. Every field is `final`. Every field that affects identity is in `props`, and every field that does not affect identity is deliberately absent. And the `props` list order is consistent, since it feeds the hash computation and a changing order between runs is a subtle way to break caching.
Editorial conclusion
Adopt Equatable if you write Dart or Flutter and keep finding yourself copying the same `==` and `hashCode` override between your model classes, since the package removes that boilerplate without a build step and gives you consistent equality semantics across the model layer. Do not adopt it for mutable classes, because the package is designed to work only with immutable objects and a mutable field in `props` will break hash based collections. Verify three things first: that every field you list in `props` is `final`, that your expectations about `toString` output match the `stringify` flag, which defaults to true in debug mode and false in release mode, and that the version you pin matches the Dart SDK constraint, since v3.0.0 is a major release and the dependency line in the README is `equatable: ^3.0.0`.
Frequently asked questions
What does the Equatable Dart package do?
It overrides `==` and `hashCode` for your classes so you do not have to write the boilerplate yourself. You extend `Equatable` and list the fields that determine identity in a `props` getter, and no code generation step is required.
How do I add Equatable to a Dart or Flutter project?
Add `equatable: ^3.0.0` under dependencies in your pubspec.yaml, then run `dart pub get` for a Dart project or `flutter packages get` for a Flutter project. The package supports const constructors and nullable fields, whose props list is typed as `List<Object?>`.
Does Equatable work with mutable classes?
No. The package is designed to work only with immutable objects, so all member variables must be final. The reason given is that overriding a hashCode with a mutable value can break hash based collections, since the hash changes while the object sits inside a set or map.
How do I use Equatable when my class already has a superclass?
You cannot extend both, so the package can be used as a mixin instead. The README shows this with an `EquatableDateTime` class extending `DateTime`, which keeps the equality behaviour without changing the inheritance tree.
How do I get a readable toString from Equatable classes?
Override `bool get stringify => true;` on the class, or set `EquatableConfig.stringify = true` globally. A class level override takes precedence over the global setting, and the global option defaults to true in debug mode and false in release mode.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/felangel-equatable)Was this useful?