-
-
Notifications
You must be signed in to change notification settings - Fork 286
Description
Describe the bug
EqualUnmodifiableListView implies that it overrides the == operator such that it should return true if two EqualUnmodifiableListView objects match ie. they both contain a list of matching items. However, the == operator returns false when the lists are different objects but contain matching items.
This causes an inconsistency where for a freezed List equality is determined on content, but for a freezed Object containing a List, equality is determined on being the same object
The same issues also affects EqualUnmodifiableSetView and EqualUnmodifiableMapView.
To Reproduce
Using the code from test/equal_test.dart:
@freezed
abstract class ObjectWithOtherProperty with _$ObjectWithOtherProperty {
factory ObjectWithOtherProperty(List<int> other) = _ObjectWithOtherProperty;
}
var objectWithOtherProperty1 = ObjectWithOtherProperty([3, 5]);
var objectWithOtherProperty2 = ObjectWithOtherProperty([3, 5]);
var other1 = objectWithOtherProperty1.other;
var other2 = objectWithOtherProperty2.other;
if (other1 == other2) { print("Match"} else {print("No match")};
This will print "No match" even though the two lists have the same items.
Note: expect(other1, other2) will pass because it compares the lists by contents and not equality.
Expected behavior
It is expected that == will return true if the list items match.
Riverpod ref.watch(Provider).select() is implemented on the basis of EqualUnmodifiableListView returning true when the lists match. This bug causes Riverpod to notify listeners even though the contents of the watched list have not changed. This results in an inconsistency when using Freezed objects where listeners are notified when an Object List property is replaced with a matching List, but are not notified if a List is replaced with a matching List.
Analysis
Looking at the code in freezed_annotation.dart:
/// An [UnmodifiableListView] which overrides ==
class EqualUnmodifiableListView<T> extends UnmodifiableListView<T> {
/// An [UnmodifiableListView] which overrides ==
EqualUnmodifiableListView(this._source) : super(_source);
final Iterable<T> _source;
@override
bool operator ==(Object other) {
return other is EqualUnmodifiableListView<T> &&
other.runtimeType == runtimeType &&
other._source == _source;
}
@override
int get hashCode => Object.hash(runtimeType, _source);
}
_source is the underlying modifiable List, so the == operator will return true only if two EqualUnmodifiableListView objects are derived from the same List object, and not if they contain matching items. This override effectively duplicates the behaviour of the default implementation of the == operator.
However, for a freezed List, the == operator is overridden with:
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is GenericIterable<T> &&
const DeepCollectionEquality().equals(other.value, value));
}
As a result, the operation of the == operator is inconsistent between a freezed List and a freezed Object containing a List.
Note that freezed_annotation.dart actually imports DeepCollectionEquality but never uses it.