Equality and immutability
In Dart the default == is reference equality: two objects are equal only when they are the same instance. That is why User('Ayan') == User('Ayan') is false.
Distinguish three concepts:
identical(a, b)— is it the same object? A reference comparison that cannot be overridden.a == b— logical equality, which a class may override.a.hashCode— an integer for hash-based collections (Set,Mapkeys).
The contract is simple, and breaking it leads to silent bugs:
- If
a == bis true, thena.hashCode == b.hashCodemust be true - Equal hash codes do not imply
==(collisions are normal) ==must be reflexive, symmetric, transitive and stable
The most common mistake is overriding == and forgetting hashCode. The code compiles (with only a lint warning), but the object appears twice in a Set and cannot be found in a Map — because the collection consults the hash first.
| Approach | What it gives | Cost |
|---|---|---|
| Default (no override) | Reference equality | Useless for value objects |
| Hand-written `==` + `hashCode` | Full control | Easy to forget when a field is added |
| `Object.hash(a, b, ...)` | Correct hash combination | Still hand-written |
| Record | Structural equality for free | No name and no methods |
| `package:equatable` | Automatic from a list of fields | A dependency; the `props` list is still manual |
| `freezed` (code generation) | `==`, `hashCode`, `copyWith`, `toString` | A build runner and generated files |
A value object is a type defined by its value: Money(100, 'AZN'), Coordinates(40.4, 49.8), EmailAddress('[email protected]'). Three things come together for such types: overriding ==/hashCode, immutability (final fields), and a const constructor where possible.
Why immutability? Because a mutable object's hashCode can change. If an object was added to a Set and then one of its fields changed, it stays in its old bucket and set.contains(obj) returns `false` even though the object is in there. This is one of the hardest bugs to track down.
A `const` constructor adds canonicalisation on top: const Money(100, 'AZN') is a single object across the program, so identical(const Money(100, 'AZN'), const Money(100, 'AZN')) is true. The comparison then resolves at the reference level without ever entering the == body.
The requirements for a const constructor: all fields final, an empty body, and every field value a compile-time constant.
The `copyWith` pattern is the standard way to work with an immutable model: instead of mutating the object, you create a modified copy.
The naive implementation has a well-known problem: in the signature copyWith({String? name}), null means two different things — "leave this field alone" and "set this field to null". For nullable fields that ambiguity is a real bug. The fixes: a sentinel object (Object? name = _unset), or the generated copyWith from the freezed package.
Why does Flutter lean on immutability? Because the rebuild cycle is comparison-based: the framework compares the old and new configuration to decide what changed. That comparison is only cheap and correct when the objects cannot change — a mutable object can produce "same reference, different contents", and the change is lost. A const constructor lowers the comparison to the identical() level. The same logic applies to state management: whether a new state object differs from the old one is decided by ==.
Interview tip. "You overrode ==; why must you also override hashCode?" — answer with the mechanism: "`Set` and `Map` first locate a bucket by `hashCode` and only then call `==`. If the hashes differ, `==` is never called — an equal object shows up twice in a set and cannot be found in a map." This is not a theoretical rule but a concrete bug, and it is exactly what the interviewer wants to hear.
The second classic: "what is the difference between == and identical()?" — the short answer: "`identical` tests whether it is the same object and cannot be overridden; `==` is logical equality and can be. Because `const` objects are canonicalised, `identical` is also `true` for the same `const` expression."
📚 Sources and documentation
- Object.operator ==officialapi.dart.dev
- Effective Dart: Designofficialdart.dev