diff --git a/mobx/CHANGELOG.md b/mobx/CHANGELOG.md index 1a2639f0..bee85cf7 100644 --- a/mobx/CHANGELOG.md +++ b/mobx/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.0 + +- Added automatic generation of `==` and `hashCode` method with `@StoreConfig` annotation + ## 2.2.3 - Avoid unnecessary observable notifications of `@observable` `Iterable` or `Map` fields of Stores by [@amondnet](https://github.com/amondnet) in [#951](https://github.com/mobxjs/mobx.dart/pull/951) diff --git a/mobx/lib/src/api/annotations.dart b/mobx/lib/src/api/annotations.dart index 602ce7a3..9e85a0e5 100644 --- a/mobx/lib/src/api/annotations.dart +++ b/mobx/lib/src/api/annotations.dart @@ -2,9 +2,10 @@ /// Currently the only configuration used is boolean to indicate generation of toString method (true), or not (false) class StoreConfig { - const StoreConfig({this.hasToString = true}); + const StoreConfig({this.hasToString = true, this.hasEqualsAndHashCode = true}); final bool hasToString; + final bool hasEqualsAndHashCode; } /// Internal class only used for code-generation with `mobx_codegen`. diff --git a/mobx/lib/src/api/store.dart b/mobx/lib/src/api/store.dart index 6d610882..dc5386cf 100644 --- a/mobx/lib/src/api/store.dart +++ b/mobx/lib/src/api/store.dart @@ -1,4 +1,5 @@ import 'package:mobx/mobx.dart'; +import 'package:mobx/src/utils.dart'; /// The `Store` mixin is primarily meant for code-generation and used as part of the /// `mobx_codegen` package. @@ -9,4 +10,18 @@ import 'package:mobx/mobx.dart'; mixin Store { /// Override this method to use a custom context. ReactiveContext get context => mainContext; + + List props = []; + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is Store && + runtimeType == other.runtimeType && + equatable(props, other.props); + } + + @override + int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props); + } diff --git a/mobx/lib/src/utils.dart b/mobx/lib/src/utils.dart index 80c30b82..d234974d 100644 --- a/mobx/lib/src/utils.dart +++ b/mobx/lib/src/utils.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:collection/collection.dart' show DeepCollectionEquality; +import 'package:collection/collection.dart' show DeepCollectionEquality, IterableExtension; const Duration ms = Duration(milliseconds: 1); @@ -38,3 +38,40 @@ bool equatable(T a, T b) { } const DeepCollectionEquality _equality = DeepCollectionEquality(); + +/// Returns a `hashCode` for [props]. +int mapPropsToHashCode(Iterable? props) { + return _finish(props == null ? 0 : props.fold(0, _combine)); +} + +/// Jenkins Hash Functions +/// https://en.wikipedia.org/wiki/Jenkins_hash_function +int _combine(int hash, Object? object) { + if (object is Map) { + object.keys + .sorted((Object? a, Object? b) => a.hashCode - b.hashCode) + .forEach((Object? key) { + hash = hash ^ _combine(hash, [key, (object! as Map)[key]]); + }); + return hash; + } + if (object is Set) { + object = object.sorted((Object? a, Object? b) => a.hashCode - b.hashCode); + } + if (object is Iterable) { + for (final value in object) { + hash = hash ^ _combine(hash, value); + } + return hash ^ object.length; + } + + hash = 0x1fffffff & (hash + object.hashCode); + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); +} + +int _finish(int hash) { + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); +} \ No newline at end of file diff --git a/mobx/lib/version.dart b/mobx/lib/version.dart index 2a69223b..56d7242b 100644 --- a/mobx/lib/version.dart +++ b/mobx/lib/version.dart @@ -1,4 +1,4 @@ // Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!! /// The current version as per `pubspec.yaml`. -const version = '2.2.3'; +const version = '2.3.0'; diff --git a/mobx/pubspec.yaml b/mobx/pubspec.yaml index 4c59a2ea..5407e292 100644 --- a/mobx/pubspec.yaml +++ b/mobx/pubspec.yaml @@ -1,5 +1,5 @@ name: mobx -version: 2.2.3 +version: 2.3.0 description: "MobX is a library for reactively managing the state of your applications. Use the power of observables, actions, and reactions to supercharge your Dart and Flutter apps." homepage: https://github.com/mobxjs/mobx.dart diff --git a/mobx_codegen/CHANGELOG.md b/mobx_codegen/CHANGELOG.md index 6f58bd96..0c31753f 100644 --- a/mobx_codegen/CHANGELOG.md +++ b/mobx_codegen/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.5.0 + +- Added automatic generation of `==` and `hashCode` method with `@StoreConfig` annotation + ## 2.4.0 - Require `analyzer: ^5.12.0` diff --git a/mobx_codegen/lib/src/store_class_visitor.dart b/mobx_codegen/lib/src/store_class_visitor.dart index 17564bf7..ef064f75 100644 --- a/mobx_codegen/lib/src/store_class_visitor.dart +++ b/mobx_codegen/lib/src/store_class_visitor.dart @@ -73,6 +73,7 @@ class StoreClassVisitor extends SimpleElementVisitor { } // if the class is annotated to generate toString() method we add the information to the _storeTemplate _storeTemplate.generateToString = hasGeneratedToString(options, element); + _storeTemplate.generateEquals = hasGeneratedEquals(options, element); } @override @@ -283,6 +284,22 @@ bool hasGeneratedToString(BuilderOptions options, ClassElement? classElement) { return true; } +bool hasGeneratedEquals(BuilderOptions options, ClassElement? classElement) { + const fieldKey = 'hasEqualsAndHashCode'; + + if (classElement != null && isStoreConfigAnnotatedStoreClass(classElement)) { + final annotation = + _toStringAnnotationChecker.firstAnnotationOfExact(classElement); + return annotation?.getField(fieldKey)?.toBoolValue() ?? false; + } + + if (options.config.containsKey(fieldKey)) { + return options.config[fieldKey]! as bool; + } + + return true; +} + bool _any(List list) => list.any(_identity); T _identity(T value) => value; diff --git a/mobx_codegen/lib/src/template/store.dart b/mobx_codegen/lib/src/template/store.dart index 0efdec35..8c235a22 100644 --- a/mobx_codegen/lib/src/template/store.dart +++ b/mobx_codegen/lib/src/template/store.dart @@ -34,8 +34,10 @@ abstract class StoreTemplate { final Rows observableFutures = Rows(); final Rows observableStreams = Rows(); final List toStringList = []; + final List props = []; bool generateToString = false; + bool generateEquals = true; String? _actionControllerName; String get actionControllerName => _actionControllerName ??= '_\$${parentTypeName}ActionController'; @@ -72,6 +74,24 @@ ${allStrings.join(',\n')} '''; } + String get propsMethod { + if (!generateEquals) { + return ''; + } + + final allObservables = observables.templates + .map((current) => 'super.${current.name}'); + + final allProps = props..addAll(allObservables); + + // The indents have been kept to ensure each field comes on a separate line without any tabs/spaces + return ''' + @override + List get props => [${allProps.join(', ')}]; + '''; + } + + String get storeBody => ''' $computeds @@ -88,5 +108,7 @@ ${allStrings.join(',\n')} $actions $toStringMethod + + $propsMethod '''; } diff --git a/mobx_codegen/lib/version.dart b/mobx_codegen/lib/version.dart index 5c81252f..bc245f3f 100644 --- a/mobx_codegen/lib/version.dart +++ b/mobx_codegen/lib/version.dart @@ -1,4 +1,4 @@ // Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!! /// The current version as per `pubspec.yaml`. -const version = '2.4.0'; +const version = '2.5.0'; diff --git a/mobx_codegen/pubspec.yaml b/mobx_codegen/pubspec.yaml index 6e988cdd..2e39e364 100644 --- a/mobx_codegen/pubspec.yaml +++ b/mobx_codegen/pubspec.yaml @@ -1,6 +1,6 @@ name: mobx_codegen description: Code generator for MobX that adds support for annotating your code with @observable, @computed, @action and also creating Store classes. -version: 2.4.0 +version: 2.5.0 homepage: https://github.com/mobxjs/mobx.dart issue_tracker: https://github.com/mobxjs/mobx.dart/issues @@ -13,7 +13,7 @@ dependencies: build: ^2.2.1 build_resolvers: ^2.0.6 meta: ^1.3.0 - mobx: ^2.2.0 + mobx: ^2.3.0 path: ^1.8.0 source_gen: ^1.2.1 diff --git a/mobx_codegen/test/data/annotations_test_class_output.dart b/mobx_codegen/test/data/annotations_test_class_output.dart index 455608dd..6f3d370c 100644 --- a/mobx_codegen/test/data/annotations_test_class_output.dart +++ b/mobx_codegen/test/data/annotations_test_class_output.dart @@ -73,4 +73,7 @@ mixin _$AnnotationsTestClass on AnnotationsTestClassBase, Store { foo: ${foo} '''; } + + @override + List get props => [super.foo]; } diff --git a/mobx_codegen/test/data/valid_generic_store_input.dart b/mobx_codegen/test/data/valid_generic_store_input.dart index 4cad8aa3..d2ae49a3 100644 --- a/mobx_codegen/test/data/valid_generic_store_input.dart +++ b/mobx_codegen/test/data/valid_generic_store_input.dart @@ -6,7 +6,7 @@ part 'generator_sample.g.dart'; class Item = _Item with _$Item; -@StoreConfig(hasToString: false) +@StoreConfig(hasToString: false, hasEqualsAndHashCode: false) abstract class _Item with Store { @observable T value1; diff --git a/mobx_codegen/test/data/valid_import_prefixed_input.dart b/mobx_codegen/test/data/valid_import_prefixed_input.dart index ac1baa10..79ca5336 100644 --- a/mobx_codegen/test/data/valid_import_prefixed_input.dart +++ b/mobx_codegen/test/data/valid_import_prefixed_input.dart @@ -9,7 +9,7 @@ part 'generator_sample.g.dart'; class User = UserBase with _$User; -@StoreConfig(hasToString: false) +@StoreConfig(hasToString: false, hasEqualsAndHashCode: false) abstract class UserBase with Store { UserBase(); diff --git a/mobx_codegen/test/data/valid_input.dart b/mobx_codegen/test/data/valid_input.dart index 5290e280..bd0dd99e 100644 --- a/mobx_codegen/test/data/valid_input.dart +++ b/mobx_codegen/test/data/valid_input.dart @@ -10,7 +10,7 @@ typedef VoidCallback = void Function(); class User = UserBase with _$User; -@StoreConfig(hasToString: false) +@StoreConfig(hasToString: false, hasEqualsAndHashCode: false) abstract class UserBase with Store { UserBase(this.id); diff --git a/mobx_codegen/test/data/valid_late_variables_input.dart b/mobx_codegen/test/data/valid_late_variables_input.dart index 153d8371..9550d8e7 100644 --- a/mobx_codegen/test/data/valid_late_variables_input.dart +++ b/mobx_codegen/test/data/valid_late_variables_input.dart @@ -6,7 +6,7 @@ part 'generator_sample.g.dart'; class TestStore = _TestStore with _$TestStore; -@StoreConfig(hasToString: false) +@StoreConfig(hasToString: false, hasEqualsAndHashCode: false) abstract class _TestStore with Store { @observable late String username; diff --git a/mobx_codegen/test/data/valid_output_annotation_store_config_has_to_string.dart b/mobx_codegen/test/data/valid_output_annotation_store_config_has_to_string.dart index a151b3b3..42a234b3 100644 --- a/mobx_codegen/test/data/valid_output_annotation_store_config_has_to_string.dart +++ b/mobx_codegen/test/data/valid_output_annotation_store_config_has_to_string.dart @@ -338,4 +338,19 @@ fullName: ${fullName}, fullNameNullable: ${fullNameNullable} '''; } + + @override + List get props => [ + super.firstName, + super.firstNameNullable, + super.middleName, + super.lastName, + super.friend, + super.friendNullable, + super.callback, + super.callbackNullable, + super.callback2, + super.callback2Nullable, + super._testUsers + ]; } diff --git a/mobx_codegen/test/generator_usage_test.g.dart b/mobx_codegen/test/generator_usage_test.g.dart index 09d94af6..ae83b697 100644 --- a/mobx_codegen/test/generator_usage_test.g.dart +++ b/mobx_codegen/test/generator_usage_test.g.dart @@ -266,4 +266,18 @@ fields: ${fields}, batchedItems: ${batchedItems} '''; } + + @override + List get props => [ + super.field1, + super.field2, + super.stuff, + super.always, + super.custom, + super.batchItem1, + super.batchItem2, + super.batchItem3, + super.batchItem4, + super.errorField + ]; } diff --git a/mobx_codegen/test/nested_store.g.dart b/mobx_codegen/test/nested_store.g.dart index 6599ebde..0446385f 100644 --- a/mobx_codegen/test/nested_store.g.dart +++ b/mobx_codegen/test/nested_store.g.dart @@ -30,4 +30,7 @@ mixin _$NestedStore on _NestedStore, Store { name: ${name} '''; } + + @override + List get props => [super.name]; } diff --git a/mobx_codegen/test/store_class_visitor_test.dart b/mobx_codegen/test/store_class_visitor_test.dart index 01f2609c..99b75700 100644 --- a/mobx_codegen/test/store_class_visitor_test.dart +++ b/mobx_codegen/test/store_class_visitor_test.dart @@ -73,5 +73,12 @@ void main() { true, ); }); + + test('hasGeneratedEquals understands BuilderOptions', () { + expect( + hasGeneratedEquals(BuilderOptions({'hasEqualsAndHashCode': true}), null), + true, + ); + }); }); } diff --git a/mobx_codegen/test/store_with_custom_context.g.dart b/mobx_codegen/test/store_with_custom_context.g.dart index aaea811b..0941b011 100644 --- a/mobx_codegen/test/store_with_custom_context.g.dart +++ b/mobx_codegen/test/store_with_custom_context.g.dart @@ -31,4 +31,7 @@ mixin _$CustomContextStore on _CustomContextStore, Store { name: ${name} '''; } + + @override + List get props => [super.name]; }