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);
+    });
+  });
+}