Add EquatableMixin (#5)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 281b700..bd7ac22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,30 +1,22 @@
-# 0.1.0
+# 0.2.0
-Initial Version of the library.
+Add `EquatableMixin` and `EquatableMixinBase`
-- Includes the ability to extend `Equatable` and not have to override `==` and `hashCode`.
+# 0.1.10
-# 0.1.1
+Enhancements to `toString` override
-Minor Updates to Documentation.
+# 0.1.9
-# 0.1.2
+equatable has 0 dependencies
-Additional Updates to Documentation.
+# 0.1.8
-- Logo Added
+Support `Iterable` props
-# 0.1.3
+# 0.1.7
-Bug Fixes
-
-# 0.1.4
-
-Performance Optimizations
-
-# 0.1.5
-
-Additional Performance Optimizations & Documentation Updates
+Added `toString` override
# 0.1.6
@@ -32,18 +24,30 @@
- Performance Tests
-# 0.1.7
+# 0.1.5
-Added `toString` override
+Additional Performance Optimizations & Documentation Updates
-# 0.1.8
+# 0.1.4
-Support `Iterable` props
+Performance Optimizations
-# 0.1.9
+# 0.1.3
-equatable has 0 dependencies
+Bug Fixes
-# 0.1.10
+# 0.1.2
-Enhancements to `toString` override
+Additional Updates to Documentation.
+
+- Logo Added
+
+# 0.1.1
+
+Minor Updates to Documentation.
+
+# 0.1.0
+
+Initial Version of the library.
+
+- Includes the ability to extend `Equatable` and not have to override `==` and `hashCode`.
diff --git a/README.md b/README.md
index 5b2fae7..a2c10bc 100644
--- a/README.md
+++ b/README.md
@@ -162,6 +162,59 @@
}
```
+## EquatableMixin
+
+Sometimes it isn't possible to extend `Equatable` because your class already has a superclass.
+In this case, you can still get the benefits of `Equatable` by using the `EquatableMixin`.
+
+### Usage
+
+Let's say we want to make an `EquatableDateTime` class, we can use `EquatableMixinBase` and `EquatableMixin` like so:
+
+```dart
+class EquatableDateTime extends DateTime
+ with EquatableMixinBase, EquatableMixin {
+ EquatableDateTime(
+ int year, [
+ int month = 1,
+ int day = 1,
+ int hour = 0,
+ int minute = 0,
+ int second = 0,
+ int millisecond = 0,
+ int microsecond = 0,
+ ]) : super(year, month, day, hour, minute, second, millisecond, microsecond);
+
+ @override
+ List get props {
+ return [year, month, day, hour, minute, second, millisecond, microsecond];
+ }
+}
+```
+
+Now if we want to create a subclass of `EquatableDateTime`, we can continue to just use the `EquatableMixin` and override `props`.
+
+```dart
+class EquatableDateTimeSubclass extends EquatableDateTime with EquatableMixin {
+ final int century;
+
+ EquatableDateTime(
+ this.century,
+ int year,[
+ int month = 1,
+ int day = 1,
+ int hour = 0,
+ int minute = 0,
+ int second = 0,
+ int millisecond = 0,
+ int microsecond = 0,
+ ]) : super(year, month, day, hour, minute, second, millisecond, microsecond);
+
+ @override
+ List get props => super.props..addAll([century]);
+}
+```
+
## Performance
You might be wondering what the performance impact will be if you use `Equatable`.
diff --git a/example/main.dart b/example/main.dart
index 7f1c0ec..8a11bd4 100644
--- a/example/main.dart
+++ b/example/main.dart
@@ -7,17 +7,53 @@
Credentials({this.username, this.password}) : super([username, password]);
}
+class EquatableDateTime extends DateTime
+ with EquatableMixinBase, EquatableMixin {
+ EquatableDateTime(
+ int year, [
+ int month = 1,
+ int day = 1,
+ int hour = 0,
+ int minute = 0,
+ int second = 0,
+ int millisecond = 0,
+ int microsecond = 0,
+ ]) : super(year, month, day, hour, minute, second, millisecond, microsecond);
+
+ @override
+ List get props {
+ return [year, month, day, hour, minute, second, millisecond, microsecond];
+ }
+}
+
void main() {
+ // Extending Equatable
final credentialsA = Credentials(username: 'Joe', password: 'password123');
final credentialsB = Credentials(username: 'Bob', password: 'password!');
final credentialsC = Credentials(username: 'Bob', password: 'password!');
print(credentialsA == credentialsA); // true
print(credentialsB == credentialsB); // true
+ print(credentialsC == credentialsC); // true
print(credentialsA == credentialsB); // false
print(credentialsB == credentialsC); // true
print(credentialsA); // [Joe, password123]
print(credentialsB); // [Bob, password!]
print(credentialsC); // [Bob, password!]
+
+ // Equatable Mixin
+ final dateTimeA = EquatableDateTime(2019);
+ final dateTimeB = EquatableDateTime(2019, 2, 20, 19, 46);
+ final dateTimeC = EquatableDateTime(2019, 2, 20, 19, 46);
+
+ print(dateTimeA == dateTimeA); // true
+ print(dateTimeB == dateTimeB); // true
+ print(dateTimeC == dateTimeC); // true
+ print(dateTimeA == dateTimeB); // false
+ print(dateTimeB == dateTimeC); // true
+
+ print(dateTimeA); // 2019-01-01 00:00:00.000
+ print(dateTimeB); // 2019-02-20 19:46:00.000
+ print(dateTimeC); // 2019-02-20 19:46:00.000
}
diff --git a/lib/equatable.dart b/lib/equatable.dart
index 8daeacc..5e54832 100644
--- a/lib/equatable.dart
+++ b/lib/equatable.dart
@@ -1,3 +1,4 @@
library equatable;
export './src/equatable.dart';
+export './src/equatable_mixin.dart';
diff --git a/lib/src/equatable.dart b/lib/src/equatable.dart
index 17b3fb7..e10d5b3 100644
--- a/lib/src/equatable.dart
+++ b/lib/src/equatable.dart
@@ -1,3 +1,5 @@
+import './equatable_utils.dart';
+
/// A class that helps implement equality
/// without needing to explicitly override == and [hashCode].
/// Equatables override their own == and [hashCode] based on
@@ -18,35 +20,10 @@
identical(this, other) ||
other is Equatable &&
runtimeType == other.runtimeType &&
- _equals(props, other.props);
+ equals(props, other.props);
@override
- int get hashCode => runtimeType.hashCode ^ _propsHashCode;
-
- int get _propsHashCode {
- int hashCode = 0;
-
- props.forEach((prop) {
- hashCode = hashCode ^ prop.hashCode;
- });
-
- return hashCode;
- }
-
- bool _equals(list1, list2) {
- if (identical(list1, list2)) return true;
- if (list1 == null || list2 == null) return false;
- int length = list1.length;
- if (length != list2.length) return false;
- for (int i = 0; i < length; i++) {
- if (list1[i] is Iterable) {
- if (!_equals(list1[i], list2[i])) return false;
- } else {
- if (list1[i] != list2[i]) return false;
- }
- }
- return true;
- }
+ int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props);
@override
String toString() => props.isNotEmpty ? props.toString() : super.toString();
diff --git a/lib/src/equatable_mixin.dart b/lib/src/equatable_mixin.dart
new file mode 100644
index 0000000..cdb092b
--- /dev/null
+++ b/lib/src/equatable_mixin.dart
@@ -0,0 +1,33 @@
+import './equatable_utils.dart';
+
+/// You must define the [EquatableMixinBase] on the class
+/// which you want to make Equatable.
+/// `class EquatableDateTime extends DateTime with EquatableMixinBase, EquatableMixin { ... }`
+/// This exposes the `props` getter which can then be overridden to include custom props in subclasses.
+/// The `props` getter is used to override `==` and `hashCode` in the [EquatableMixin].
+mixin EquatableMixinBase on Object {
+ List get props => [];
+
+ @override
+ String toString() => super.toString();
+}
+
+/// You must define the [EquatableMixin] on the class
+/// which you want to make Equatable and the class
+/// must also be a descendent of [EquatableMixinBase].
+/// [EquatableMixin] does the override of the `==` operator as well as `hashCode`.
+mixin EquatableMixin on EquatableMixinBase {
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ other is EquatableMixin &&
+ runtimeType == other.runtimeType &&
+ equals(props, other.props);
+ }
+
+ @override
+ int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props);
+
+ @override
+ String toString() => props.isNotEmpty ? props.toString() : super.toString();
+}
diff --git a/lib/src/equatable_utils.dart b/lib/src/equatable_utils.dart
new file mode 100644
index 0000000..d3a6e8b
--- /dev/null
+++ b/lib/src/equatable_utils.dart
@@ -0,0 +1,24 @@
+int mapPropsToHashCode(List props) {
+ int hashCode = 0;
+
+ props.forEach((prop) {
+ hashCode = hashCode ^ prop.hashCode;
+ });
+
+ return hashCode;
+}
+
+bool equals(dynamic list1, dynamic list2) {
+ if (identical(list1, list2)) return true;
+ if (list1 == null || list2 == null) return false;
+ int length = list1.length;
+ if (length != list2.length) return false;
+ for (int i = 0; i < length; i++) {
+ if (list1[i] is Iterable) {
+ if (!equals(list1[i], list2[i])) return false;
+ } else {
+ if (list1[i] != list2[i]) return false;
+ }
+ }
+ return true;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 2b9804f..5c5f7df 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,11 +1,11 @@
name: equatable
description: An abstract class that helps to implement equality without needing to explicitly override == and hashCode.
-version: 0.1.10
+version: 0.2.0
author: felix.angelov <felangelov@gmail.com>
homepage: https://github.com/felangel/equatable
environment:
- sdk: ">=2.0.0-dev.28.0 <3.0.0"
+ sdk: ">=2.1.0 <3.0.0"
dev_dependencies:
test: ">=1.3.0 <2.0.0"
diff --git a/test/equatable_mixin_test.dart b/test/equatable_mixin_test.dart
new file mode 100644
index 0000000..b4be0a3
--- /dev/null
+++ b/test/equatable_mixin_test.dart
@@ -0,0 +1,397 @@
+import 'package:test/test.dart';
+
+import 'package:equatable/equatable.dart';
+
+class NonEquatable {}
+
+class EquatableBase with EquatableMixinBase, EquatableMixin {}
+
+class EmptyEquatable extends EquatableBase with EquatableMixin {}
+
+class SimpleEquatable<T> extends EquatableBase with EquatableMixin {
+ final T data;
+
+ SimpleEquatable(this.data);
+
+ @override
+ List get props => super.props..addAll([data]);
+}
+
+class MultipartEquatable<T> extends EquatableBase with EquatableMixin {
+ final T d1;
+ final T d2;
+
+ MultipartEquatable(this.d1, this.d2);
+
+ @override
+ List get props => super.props..addAll([d1, d2]);
+}
+
+class OtherEquatable extends EquatableBase with EquatableMixin {
+ final String data;
+
+ OtherEquatable(this.data);
+
+ @override
+ List get props => super.props..addAll([data]);
+}
+
+enum Color { blonde, black, brown }
+
+class ComplexEquatable extends EquatableBase with EquatableMixin {
+ final String name;
+ final int age;
+ final Color hairColor;
+ final List<String> children;
+
+ ComplexEquatable({this.name, this.age, this.hairColor, this.children});
+
+ @override
+ List get props => super.props..addAll([name, age, hairColor, children]);
+}
+
+class EquatableData extends EquatableBase with EquatableMixin {
+ final String key;
+ final dynamic value;
+
+ EquatableData({this.key, this.value});
+
+ @override
+ List get props => super.props..addAll([key, value]);
+}
+
+void main() {
+ group('Empty Equatable', () {
+ test('should correct toString', () {
+ final instance = EmptyEquatable();
+ expect(instance.toString(), "Instance of 'EmptyEquatable'");
+ });
+
+ test('should return true when instance is the same', () {
+ final instance = EmptyEquatable();
+ expect(instance == instance, true);
+ });
+
+ test('should return correct hashCode', () {
+ final instance = EmptyEquatable();
+ expect(instance.hashCode, instance.runtimeType.hashCode);
+ });
+
+ test('should return true when instances are different', () {
+ final instanceA = EmptyEquatable();
+ final instanceB = EmptyEquatable();
+ expect(instanceA == instanceB, true);
+ });
+
+ test('should return false when compared to non-equatable', () {
+ final instanceA = EmptyEquatable();
+ final instanceB = NonEquatable();
+ expect(instanceA == instanceB, false);
+ });
+ });
+
+ group('Simple Equatable (string)', () {
+ test('should correct toString', () {
+ final instance = SimpleEquatable('simple');
+ expect(instance.toString(), '[simple]');
+ });
+
+ test('should return true when instance is the same', () {
+ final instance = SimpleEquatable('simple');
+ expect(instance == instance, true);
+ });
+
+ test('should return correct hashCode', () {
+ final instance = SimpleEquatable('simple');
+ expect(
+ instance.hashCode,
+ instance.runtimeType.hashCode ^ instance.data.hashCode,
+ );
+ });
+
+ test('should return true when instances are different', () {
+ final instanceA = SimpleEquatable('simple');
+ final instanceB = SimpleEquatable('simple');
+ expect(instanceA == instanceB, true);
+ });
+
+ test('should return false when compared to non-equatable', () {
+ final instanceA = SimpleEquatable('simple');
+ final instanceB = NonEquatable();
+ expect(instanceA == instanceB, false);
+ });
+
+ test('should return false when compared to different equatable', () {
+ final instanceA = SimpleEquatable('simple');
+ final instanceB = OtherEquatable('simple');
+ expect(instanceA == instanceB, false);
+ });
+
+ test('should return false when values are different', () {
+ final instanceA = SimpleEquatable('simple');
+ final instanceB = SimpleEquatable('Simple');
+ expect(instanceA == instanceB, false);
+ });
+ });
+
+ group('Simple Equatable (number)', () {
+ test('should correct toString', () {
+ final instance = SimpleEquatable(0);
+ expect(instance.toString(), '[0]');
+ });
+
+ test('should return true when instance is the same', () {
+ final instance = SimpleEquatable(0);
+ expect(instance == instance, true);
+ });
+
+ test('should return correct hashCode', () {
+ final instance = SimpleEquatable(0);
+ expect(
+ instance.hashCode,
+ instance.runtimeType.hashCode ^ instance.data.hashCode,
+ );
+ });
+
+ test('should return true when instances are different', () {
+ final instanceA = SimpleEquatable(0);
+ final instanceB = SimpleEquatable(0);
+ expect(instanceA == instanceB, true);
+ });
+
+ test('should return false when compared to non-equatable', () {
+ final instanceA = SimpleEquatable(0);
+ final instanceB = NonEquatable();
+ expect(instanceA == instanceB, false);
+ });
+
+ test('should return false when values are different', () {
+ final instanceA = SimpleEquatable(0);
+ final instanceB = SimpleEquatable(1);
+ expect(instanceA == instanceB, false);
+ });
+ });
+
+ group('Simple Equatable (bool)', () {
+ test('should correct toString', () {
+ final instance = SimpleEquatable(true);
+ expect(instance.toString(), '[true]');
+ });
+
+ test('should return true when instance is the same', () {
+ final instance = SimpleEquatable(true);
+ expect(instance == instance, true);
+ });
+
+ test('should return correct hashCode', () {
+ final instance = SimpleEquatable(true);
+ expect(
+ instance.hashCode,
+ instance.runtimeType.hashCode ^ instance.data.hashCode,
+ );
+ });
+
+ test('should return true when instances are different', () {
+ final instanceA = SimpleEquatable(true);
+ final instanceB = SimpleEquatable(true);
+ expect(instanceA == instanceB, true);
+ });
+
+ test('should return false when compared to non-equatable', () {
+ final instanceA = SimpleEquatable(true);
+ final instanceB = NonEquatable();
+ expect(instanceA == instanceB, false);
+ });
+
+ test('should return false when values are different', () {
+ final instanceA = SimpleEquatable(true);
+ final instanceB = SimpleEquatable(false);
+ expect(instanceA == instanceB, false);
+ });
+ });
+
+ group('Simple Equatable (Equatable)', () {
+ test('should correct toString', () {
+ final instance = SimpleEquatable(EquatableData(
+ key: 'foo',
+ value: 'bar',
+ ));
+ expect(instance.toString(), '[[foo, bar]]');
+ });
+ test('should return true when instance is the same', () {
+ final instance = SimpleEquatable(EquatableData(
+ key: 'foo',
+ value: 'bar',
+ ));
+ expect(instance == instance, true);
+ });
+
+ test('should return correct hashCode', () {
+ final instance = SimpleEquatable(EquatableData(
+ key: 'foo',
+ value: 'bar',
+ ));
+ expect(
+ instance.hashCode,
+ instance.runtimeType.hashCode ^ instance.data.hashCode,
+ );
+ });
+
+ test('should return true when instances are different', () {
+ final instanceA = SimpleEquatable(EquatableData(
+ key: 'foo',
+ value: 'bar',
+ ));
+ final instanceB = SimpleEquatable(EquatableData(
+ key: 'foo',
+ value: 'bar',
+ ));
+ expect(instanceA == instanceB, true);
+ });
+
+ test('should return false when compared to non-equatable', () {
+ final instanceA = SimpleEquatable(EquatableData(
+ key: 'foo',
+ value: 'bar',
+ ));
+ final instanceB = NonEquatable();
+ expect(instanceA == instanceB, false);
+ });
+
+ test('should return false when values are different', () {
+ final instanceA = SimpleEquatable(EquatableData(
+ key: 'foo',
+ value: 'bar',
+ ));
+ final instanceB = SimpleEquatable(EquatableData(
+ key: 'foo',
+ value: 'barz',
+ ));
+ expect(instanceA == instanceB, false);
+ });
+ });
+
+ group('Multipart Equatable', () {
+ test('should correct toString', () {
+ final instance = MultipartEquatable("s1", "s2");
+ expect(instance.toString(), '[s1, s2]');
+ });
+ test('should return true when instance is the same', () {
+ final instance = MultipartEquatable("s1", "s2");
+ expect(instance == instance, true);
+ });
+
+ test('should return correct hashCode', () {
+ final instance = MultipartEquatable("s1", "s2");
+ expect(
+ instance.hashCode,
+ instance.runtimeType.hashCode ^
+ instance.d1.hashCode ^
+ instance.d2.hashCode,
+ );
+ });
+
+ test('should return true when instances are different', () {
+ final instanceA = MultipartEquatable("s1", "s2");
+ final instanceB = MultipartEquatable("s1", "s2");
+ expect(instanceA == instanceB, true);
+ });
+
+ test('should return false when compared to non-equatable', () {
+ final instanceA = MultipartEquatable("s1", "s2");
+ final instanceB = NonEquatable();
+ expect(instanceA == instanceB, false);
+ });
+
+ test('should return false when values are different', () {
+ final instanceA = MultipartEquatable("s1", "s2");
+ final instanceB = MultipartEquatable("s2", "s1");
+ expect(instanceA == instanceB, false);
+
+ final instanceC = MultipartEquatable("s1", "s1");
+ final instanceD = MultipartEquatable("s2", "s1");
+ expect(instanceC == instanceD, false);
+ });
+ });
+
+ group('Complex Equatable', () {
+ test('should correct toString', () {
+ final instance = ComplexEquatable(
+ name: 'Joe',
+ age: 40,
+ hairColor: Color.black,
+ children: ['Bob'],
+ );
+ expect(instance.toString(), '[Joe, 40, Color.black, [Bob]]');
+ });
+ test('should return true when instance is the same', () {
+ final instance = ComplexEquatable(
+ name: 'Joe',
+ age: 40,
+ hairColor: Color.black,
+ children: ['Bob'],
+ );
+ expect(instance == instance, true);
+ });
+
+ test('should return correct hashCode', () {
+ final instance = ComplexEquatable(
+ name: 'Joe',
+ age: 40,
+ hairColor: Color.black,
+ children: ['Bob'],
+ );
+ expect(
+ instance.hashCode,
+ instance.runtimeType.hashCode ^
+ instance.name.hashCode ^
+ instance.age.hashCode ^
+ instance.hairColor.hashCode ^
+ instance.children.hashCode,
+ );
+ });
+
+ test('should return true when instances are different', () {
+ final instanceA = ComplexEquatable(
+ name: 'Joe',
+ age: 40,
+ hairColor: Color.black,
+ children: ['Bob'],
+ );
+ final instanceB = ComplexEquatable(
+ name: 'Joe',
+ age: 40,
+ hairColor: Color.black,
+ children: ['Bob'],
+ );
+ expect(instanceA == instanceB, true);
+ });
+
+ test('should return false when compared to non-equatable', () {
+ final instanceA = ComplexEquatable(
+ name: 'Joe',
+ age: 40,
+ hairColor: Color.black,
+ children: ['Bob'],
+ );
+ final instanceB = NonEquatable();
+ expect(instanceA == instanceB, false);
+ });
+
+ test('should return false when values are different', () {
+ final instanceA = ComplexEquatable(
+ name: 'Joe',
+ age: 40,
+ hairColor: Color.black,
+ children: ['Bob'],
+ );
+ final instanceB = ComplexEquatable(
+ name: 'John',
+ age: 40,
+ hairColor: Color.brown,
+ children: ['Bobby'],
+ );
+ expect(instanceA == instanceB, false);
+ });
+ });
+}