Skip to content

Commit e5e72e5

Browse files
committed
feat: Added automatic generation of == and hashCode method with @StoreConfig annotation
1 parent 52515a1 commit e5e72e5

21 files changed

+156
-11
lines changed

mobx/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.3.0
2+
3+
- Added automatic generation of `==` and `hashCode` method with `@StoreConfig` annotation
4+
15
## 2.2.3
26

37
- 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)

mobx/lib/src/api/annotations.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
/// Currently the only configuration used is boolean to indicate generation of toString method (true), or not (false)
33
44
class StoreConfig {
5-
const StoreConfig({this.hasToString = true});
5+
const StoreConfig({this.hasToString = true, this.hasEqualsAndHashCode = true});
66

77
final bool hasToString;
8+
final bool hasEqualsAndHashCode;
89
}
910

1011
/// Internal class only used for code-generation with `mobx_codegen`.

mobx/lib/src/api/store.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:mobx/mobx.dart';
2+
import 'package:mobx/src/utils.dart';
23

34
/// The `Store` mixin is primarily meant for code-generation and used as part of the
45
/// `mobx_codegen` package.
@@ -9,4 +10,18 @@ import 'package:mobx/mobx.dart';
910
mixin Store {
1011
/// Override this method to use a custom context.
1112
ReactiveContext get context => mainContext;
13+
14+
List<Object?> props = [];
15+
16+
@override
17+
bool operator ==(Object other) {
18+
return identical(this, other) ||
19+
other is Store &&
20+
runtimeType == other.runtimeType &&
21+
equatable(props, other.props);
22+
}
23+
24+
@override
25+
int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props);
26+
1227
}

mobx/lib/src/utils.dart

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'dart:async';
22

3-
import 'package:collection/collection.dart' show DeepCollectionEquality;
3+
import 'package:collection/collection.dart' show DeepCollectionEquality, IterableExtension;
44

55
const Duration ms = Duration(milliseconds: 1);
66

@@ -38,3 +38,40 @@ bool equatable<T>(T a, T b) {
3838
}
3939

4040
const DeepCollectionEquality _equality = DeepCollectionEquality();
41+
42+
/// Returns a `hashCode` for [props].
43+
int mapPropsToHashCode(Iterable<Object?>? props) {
44+
return _finish(props == null ? 0 : props.fold(0, _combine));
45+
}
46+
47+
/// Jenkins Hash Functions
48+
/// https://en.wikipedia.org/wiki/Jenkins_hash_function
49+
int _combine(int hash, Object? object) {
50+
if (object is Map) {
51+
object.keys
52+
.sorted((Object? a, Object? b) => a.hashCode - b.hashCode)
53+
.forEach((Object? key) {
54+
hash = hash ^ _combine(hash, [key, (object! as Map)[key]]);
55+
});
56+
return hash;
57+
}
58+
if (object is Set) {
59+
object = object.sorted((Object? a, Object? b) => a.hashCode - b.hashCode);
60+
}
61+
if (object is Iterable) {
62+
for (final value in object) {
63+
hash = hash ^ _combine(hash, value);
64+
}
65+
return hash ^ object.length;
66+
}
67+
68+
hash = 0x1fffffff & (hash + object.hashCode);
69+
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
70+
return hash ^ (hash >> 6);
71+
}
72+
73+
int _finish(int hash) {
74+
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
75+
hash = hash ^ (hash >> 11);
76+
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
77+
}

mobx/lib/version.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!!
22

33
/// The current version as per `pubspec.yaml`.
4-
const version = '2.2.3';
4+
const version = '2.3.0';

mobx/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: mobx
2-
version: 2.2.3
2+
version: 2.3.0
33
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."
44

55
homepage: https://github.com/mobxjs/mobx.dart

mobx_codegen/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.5.0
2+
3+
- Added automatic generation of `==` and `hashCode` method with `@StoreConfig` annotation
4+
15
## 2.4.0
26

37
- Require `analyzer: ^5.12.0`

mobx_codegen/lib/src/store_class_visitor.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class StoreClassVisitor extends SimpleElementVisitor {
7373
}
7474
// if the class is annotated to generate toString() method we add the information to the _storeTemplate
7575
_storeTemplate.generateToString = hasGeneratedToString(options, element);
76+
_storeTemplate.generateEquals = hasGeneratedEquals(options, element);
7677
}
7778

7879
@override
@@ -283,6 +284,22 @@ bool hasGeneratedToString(BuilderOptions options, ClassElement? classElement) {
283284
return true;
284285
}
285286

287+
bool hasGeneratedEquals(BuilderOptions options, ClassElement? classElement) {
288+
const fieldKey = 'hasEqualsAndHashCode';
289+
290+
if (classElement != null && isStoreConfigAnnotatedStoreClass(classElement)) {
291+
final annotation =
292+
_toStringAnnotationChecker.firstAnnotationOfExact(classElement);
293+
return annotation?.getField(fieldKey)?.toBoolValue() ?? false;
294+
}
295+
296+
if (options.config.containsKey(fieldKey)) {
297+
return options.config[fieldKey]! as bool;
298+
}
299+
300+
return true;
301+
}
302+
286303
bool _any(List<bool> list) => list.any(_identity);
287304

288305
T _identity<T>(T value) => value;

mobx_codegen/lib/src/template/store.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ abstract class StoreTemplate {
3434
final Rows<ObservableFutureTemplate> observableFutures = Rows();
3535
final Rows<ObservableStreamTemplate> observableStreams = Rows();
3636
final List<String> toStringList = [];
37+
final List<String> props = [];
3738

3839
bool generateToString = false;
40+
bool generateEquals = true;
3941
String? _actionControllerName;
4042
String get actionControllerName =>
4143
_actionControllerName ??= '_\$${parentTypeName}ActionController';
@@ -72,6 +74,24 @@ ${allStrings.join(',\n')}
7274
''';
7375
}
7476

77+
String get propsMethod {
78+
if (!generateEquals) {
79+
return '';
80+
}
81+
82+
final allObservables = observables.templates
83+
.map((current) => 'super.${current.name}');
84+
85+
final allProps = props..addAll(allObservables);
86+
87+
// The indents have been kept to ensure each field comes on a separate line without any tabs/spaces
88+
return '''
89+
@override
90+
List<Object?> get props => [${allProps.join(', ')}];
91+
''';
92+
}
93+
94+
7595
String get storeBody => '''
7696
$computeds
7797
@@ -88,5 +108,7 @@ ${allStrings.join(',\n')}
88108
$actions
89109
90110
$toStringMethod
111+
112+
$propsMethod
91113
''';
92114
}

mobx_codegen/lib/version.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!!
22

33
/// The current version as per `pubspec.yaml`.
4-
const version = '2.4.0';
4+
const version = '2.5.0';

0 commit comments

Comments
 (0)