feat: EquatableConfig for global stringify configuration (#69)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a390e6..1c1279b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 1.2.0
+
+- Added `EquatableConfig` for global `stringify` configuration
+
 # 1.1.1
 
 - Updates to `EquatableUtils` documentation
diff --git a/README.md b/README.md
index 131b5a0..d9b1cb7 100644
--- a/README.md
+++ b/README.md
@@ -170,7 +170,9 @@
 ```
 
 ### `toString` Implementation
+
 Equatable can implement `toString` method including all the given props. If you want that behaviour, just include the following:
+
 ```dart
   @override
   bool get stringify => true;
@@ -188,19 +190,30 @@
 
   @override
   List<Object> get props => [name];
-  
+
   @override
   bool get stringify => true;
 }
 ```
-For the name `Bob`, the outuput will be:
-```
-Person(Bob)
-```
+
+For the name `Bob`, the output will be:
+
+`Person(Bob)`
+
 This flag by default is false and `toString` will return just the type:
+
+`Person`
+
+#### EquatableConfig
+
+`stringify` can also be configured globally for all `Equatable` instances via `EquatableConfig`
+
+```dart
+EquatableConfig.stringify = true;
 ```
-Person
-```
+
+If `stringify` is overridden for a specific `Equatable` class, then the value of `EquatableConfig.stringify` is ignored.
+In other words, the local configuration always takes precedence over the global configuration.
 
 ## Recap
 
diff --git a/lib/equatable.dart b/lib/equatable.dart
index 5e54832..3dc9fb0 100644
--- a/lib/equatable.dart
+++ b/lib/equatable.dart
@@ -1,4 +1,5 @@
 library equatable;
 
 export './src/equatable.dart';
+export './src/equatable_config.dart';
 export './src/equatable_mixin.dart';
diff --git a/lib/src/equatable.dart b/lib/src/equatable.dart
index aa0735c..ed85511 100644
--- a/lib/src/equatable.dart
+++ b/lib/src/equatable.dart
@@ -1,5 +1,6 @@
 import 'package:meta/meta.dart';
 
+import './equatable_config.dart';
 import './equatable_utils.dart';
 
 /// A base class to facilitate [operator==] and [hashCode] overrides.
@@ -22,7 +23,8 @@
 
   /// If the value is [true], the `toString` method will be overrided to print
   /// the equatable `props`.
-  bool get stringify => false;
+  // ignore: avoid_returning_null
+  bool get stringify => null;
 
   /// A class that helps implement equality
   /// without needing to explicitly override == and [hashCode].
@@ -41,6 +43,16 @@
   int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props);
 
   @override
-  String toString() =>
-      stringify ? mapPropsToString(runtimeType, props) : '$runtimeType';
+  String toString() {
+    switch (stringify) {
+      case true:
+        return mapPropsToString(runtimeType, props);
+      case false:
+        return '$runtimeType';
+      default:
+        return EquatableConfig.stringify == true
+            ? mapPropsToString(runtimeType, props)
+            : '$runtimeType';
+    }
+  }
 }
diff --git a/lib/src/equatable_config.dart b/lib/src/equatable_config.dart
new file mode 100644
index 0000000..17c823c
--- /dev/null
+++ b/lib/src/equatable_config.dart
@@ -0,0 +1,10 @@
+// ignore: avoid_classes_with_only_static_members
+/// Global [Equatable] configuration settings
+class EquatableConfig {
+  /// Global [stringify] setting for all [Equatable] instances.
+  /// If [stringify] is overridden for a particular [Equatable] instance,
+  /// then the local [stringify] value takes precendence
+  /// over `EquatableConfig.stringify`.
+  /// This value defaults to false.
+  static bool stringify = false;
+}
diff --git a/lib/src/equatable_mixin.dart b/lib/src/equatable_mixin.dart
index c4850dd..55c1d44 100644
--- a/lib/src/equatable_mixin.dart
+++ b/lib/src/equatable_mixin.dart
@@ -1,4 +1,5 @@
-import './equatable_utils.dart';
+import 'equatable_config.dart';
+import 'equatable_utils.dart';
 
 /// You must define the [EquatableMixin] on the class
 /// which you want to make Equatable.
@@ -26,6 +27,16 @@
   int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props);
 
   @override
-  String toString() =>
-      stringify ? mapPropsToString(runtimeType, props) : '$runtimeType';
+  String toString() {
+    switch (stringify) {
+      case true:
+        return mapPropsToString(runtimeType, props);
+      case false:
+        return '$runtimeType';
+      default:
+        return EquatableConfig.stringify == true
+            ? mapPropsToString(runtimeType, props)
+            : '$runtimeType';
+    }
+  }
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index e2475e3..70b9b9b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
 name: equatable
 description: An abstract class that helps to implement equality without needing to explicitly override == and hashCode.
-version: 1.1.1
+version: 1.2.0
 homepage: https://github.com/felangel/equatable
 
 environment:
diff --git a/test/equatable_config_test.dart b/test/equatable_config_test.dart
new file mode 100644
index 0000000..961e84f
--- /dev/null
+++ b/test/equatable_config_test.dart
@@ -0,0 +1,114 @@
+import 'package:equatable/equatable.dart';
+import 'package:test/test.dart';
+
+class Credentials extends Equatable {
+  final String username;
+  final String password;
+  final bool shouldStringify;
+
+  const Credentials({this.username, this.password, this.shouldStringify});
+
+  @override
+  List<Object> get props => [username, password];
+
+  @override
+  bool get stringify => shouldStringify;
+}
+
+abstract class EquatableBase with EquatableMixin {}
+
+class CredentialsMixin extends EquatableBase {
+  final String username;
+  final String password;
+  final bool shouldStringify;
+
+  CredentialsMixin({this.username, this.password, this.shouldStringify});
+
+  @override
+  List get props => [username, password];
+
+  @override
+  bool get stringify => shouldStringify;
+}
+
+void main() {
+  group('EquatableConfig', () {
+    tearDown(() {
+      EquatableConfig.stringify = false;
+    });
+
+    group('stringify', () {
+      test('defaults to false', () {
+        expect(EquatableConfig.stringify, isFalse);
+      });
+
+      test('is used when stringify is not overridden at the instance (false)',
+          () {
+        EquatableConfig.stringify = false;
+        expect(
+          Credentials(username: 'joe', password: 'pass').toString(),
+          'Credentials',
+        );
+        expect(
+          CredentialsMixin(username: 'joe', password: 'pass').toString(),
+          'CredentialsMixin',
+        );
+      });
+
+      test('is not used when stringify is overridden at the instance (true)',
+          () {
+        EquatableConfig.stringify = false;
+        expect(
+          Credentials(
+            username: 'joe',
+            password: 'pass',
+            shouldStringify: true,
+          ).toString(),
+          'Credentials(joe, pass)',
+        );
+        expect(
+          CredentialsMixin(
+            username: 'joe',
+            password: 'pass',
+            shouldStringify: true,
+          ).toString(),
+          'CredentialsMixin(joe, pass)',
+        );
+      });
+
+      test('is used when stringify is not overridden at the instance (true)',
+          () {
+        EquatableConfig.stringify = true;
+        expect(
+          Credentials(username: 'joe', password: 'pass').toString(),
+          'Credentials(joe, pass)',
+        );
+        expect(
+          CredentialsMixin(username: 'joe', password: 'pass').toString(),
+          'CredentialsMixin(joe, pass)',
+        );
+      });
+
+      test('is not used when stringify is overridden at the instance (true)',
+          () {
+        EquatableConfig.stringify = true;
+        expect(
+          Credentials(
+            username: 'joe',
+            password: 'pass',
+            shouldStringify: false,
+          ).toString(),
+          'Credentials',
+        );
+        expect(
+          CredentialsMixin(
+            username: 'joe',
+            password: 'pass',
+            shouldStringify: false,
+          ).toString(),
+          'CredentialsMixin',
+        );
+      });
+    });
+  });
+}
diff --git a/test/equatable_mixin_test.dart b/test/equatable_mixin_test.dart
index b7873b1..67ae5c2 100644
--- a/test/equatable_mixin_test.dart
+++ b/test/equatable_mixin_test.dart
@@ -103,6 +103,20 @@
   bool get stringify => true;
 }
 
+class ExplicitStringifyFalse extends ComplexEquatable {
+  final String name;
+  final int age;
+  final Color hairColor;
+
+  ExplicitStringifyFalse({this.name, this.age, this.hairColor});
+
+  @override
+  List get props => [name, age, hairColor];
+
+  @override
+  bool get stringify => false;
+}
+
 class NullProps extends Equatable {
   NullProps();
 
@@ -114,6 +128,7 @@
 }
 
 void main() {
+  EquatableConfig.stringify = false;
   group('Empty Equatable', () {
     test('should correct toString', () {
       final instance = EmptyEquatable();
@@ -581,6 +596,17 @@
       expect(instanceB.toString(), 'ComplexStringify(Bob, , Color.black)');
       expect(instanceC.toString(), 'ComplexStringify(Joe, 50, Color.blonde)');
     });
+
+    test('with ExplicitStringifyFalse stringify', () {
+      final instanceA = ExplicitStringifyFalse();
+      final instanceB =
+          ExplicitStringifyFalse(name: "Bob", hairColor: Color.black);
+      final instanceC =
+          ExplicitStringifyFalse(name: "Joe", age: 50, hairColor: Color.blonde);
+      expect(instanceA.toString(), 'ExplicitStringifyFalse');
+      expect(instanceB.toString(), 'ExplicitStringifyFalse');
+      expect(instanceC.toString(), 'ExplicitStringifyFalse');
+    });
   });
 
   group('Null props Equatable', () {
diff --git a/test/equatable_test.dart b/test/equatable_test.dart
index b43aac9..0b874b8 100644
--- a/test/equatable_test.dart
+++ b/test/equatable_test.dart
@@ -105,6 +105,20 @@
   bool get stringify => true;
 }
 
+class ExplicitStringifyFalse extends Equatable {
+  final String name;
+  final int age;
+  final Color hairColor;
+
+  ExplicitStringifyFalse({this.name, this.age, this.hairColor});
+
+  @override
+  List get props => [name, age, hairColor];
+
+  @override
+  bool get stringify => false;
+}
+
 class NullProps extends Equatable {
   NullProps();
 
@@ -116,6 +130,7 @@
 }
 
 void main() {
+  EquatableConfig.stringify = false;
   group('Empty Equatable', () {
     test('should correct toString', () {
       final instance = EmptyEquatable();
@@ -764,6 +779,17 @@
       expect(instanceB.toString(), 'ComplexStringify(Bob, , Color.black)');
       expect(instanceC.toString(), 'ComplexStringify(Joe, 50, Color.blonde)');
     });
+
+    test('with ExplicitStringifyFalse stringify', () {
+      final instanceA = ExplicitStringifyFalse();
+      final instanceB =
+          ExplicitStringifyFalse(name: "Bob", hairColor: Color.black);
+      final instanceC =
+          ExplicitStringifyFalse(name: "Joe", age: 50, hairColor: Color.blonde);
+      expect(instanceA.toString(), 'ExplicitStringifyFalse');
+      expect(instanceB.toString(), 'ExplicitStringifyFalse');
+      expect(instanceC.toString(), 'ExplicitStringifyFalse');
+    });
   });
 
   group('Null props Equatable', () {