Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds custom equals for creating observables. #907

Merged
merged 3 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
package: ["mobx_codegen", "mobx"]
version: ["stable", "beta"]
version: ["stable"]

steps:
- uses: actions/checkout@v3
Expand Down
10 changes: 9 additions & 1 deletion mobx/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
## 2.2.0

- Allows a reaction to be fired even if the value hasn't changed by [@amondnet](https://github.com/amondnet) in [#907](https://github.com/mobxjs/mobx.dart/pull/907)
- Adds custom `equals` for creating observables. [@amondnet](https://github.com/amondnet) in [#907](https://github.com/mobxjs/mobx.dart/pull/907)

## 2.1.4

- Allow users to bypass observability system for performance by [@fzyzcjy](https://github.com/fzyzcjy) in [#844](https://github.com/mobxjs/mobx.dart/pull/844)
- Avoid unnecessary observable notifications of @observable fields of Stores by [@fzyzcjy](https://github.com/fzyzcjy) in [#844](https://github.com/mobxjs/mobx.dart/pull/844)
- Fix Reaction lacks toString, so cannot see which reaction causes the error by [@fzyzcjy](https://github.com/fzyzcjy) in [#844](https://github.com/mobxjs/mobx.dart/pull/844)
- Add StackTrace to reactions in debug mode to easily spot which reaction it is by [@fzyzcjy](https://github.com/fzyzcjy) in [#844](https://github.com/mobxjs/mobx.dart/pull/844)

Breaking changes:

- Avoid unnecessary observable notifications of @observable fields of Stores by [@fzyzcjy](https://github.com/fzyzcjy) in [#844](https://github.com/mobxjs/mobx.dart/pull/844)

## 2.1.2 - 2.1.3+1

- Fix tests in dart 2.19 - [@amondnet](https://github.com/amondnet)
Expand Down
9 changes: 8 additions & 1 deletion mobx/lib/mobx.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ library mobx;

export 'package:mobx/src/api/action.dart';
export 'package:mobx/src/api/annotations.dart'
show action, computed, readonly, observable, StoreConfig;
show
action,
computed,
readonly,
observable,
StoreConfig,
MakeObservable,
alwaysNotify, observableAlwaysNotEqual;
export 'package:mobx/src/api/async.dart'
show
ObservableFuture,
Expand Down
27 changes: 24 additions & 3 deletions mobx/lib/src/api/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,48 @@

class StoreConfig {
const StoreConfig({this.hasToString = true});

final bool hasToString;
}

/// Internal class only used for code-generation with `mobx_codegen`.
///
/// During code-generation, this type is detected to identify an `Observable`
/// [readOnly] indicates that the field is only modifiable within the Store.
/// It is possible to override equality comparison of new values with [equals].
/// ```
///
/// bool _alwaysNotEqual(_, __) => false;
///
/// @MakeObservable(equals: _alwaysNotEqual)
/// String alwaysNotifyObservable = 'hello';
///
/// bool _equals(oldValue, newValue) => oldValue == newValue;
///
/// @MakeObservable(equals: _equals)
/// String withEquals = 'world';
/// ```
class MakeObservable {
const MakeObservable._({this.readOnly = false});
const MakeObservable({this.readOnly = false, this.equals});

final bool readOnly;
final Function? equals;
amondnet marked this conversation as resolved.
Show resolved Hide resolved
}

bool observableAlwaysNotEqual(_, __) => false;

/// Declares a class field as an observable. See the `Observable` class for full
/// documentation
const MakeObservable observable = MakeObservable._();
const MakeObservable observable = MakeObservable();

/// Declares a class field as an observable. See the `Observable` class for full
/// documentation.
///
/// But, it's only modifiable within the Store
const MakeObservable readonly = MakeObservable._(readOnly: true);
const MakeObservable readonly = MakeObservable(readOnly: true);

/// Allows a reaction to be fired even if the value hasn't changed.
const MakeObservable alwaysNotify = MakeObservable(equals: observableAlwaysNotEqual);

/// Internal class only used for code-generation with `mobx_codegen`.
///
Expand Down
13 changes: 13 additions & 0 deletions mobx/lib/src/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,31 @@ import '../mobx.dart';
import 'utils.dart';

part 'core/action.dart';

part 'core/atom.dart';

part 'core/computed.dart';

part 'core/context.dart';

part 'core/context_extensions.dart';

part 'core/derivation.dart';

part 'core/notification_handlers.dart';

part 'core/observable.dart';

part 'core/observable_value.dart';

part 'core/reaction.dart';

part 'core/reaction_helper.dart';

part 'core/spy.dart';

part 'interceptable.dart';

part 'listenable.dart';

/// An Exception class to capture MobX specific exceptions
Expand Down
8 changes: 6 additions & 2 deletions mobx/lib/src/core/atom_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ extension AtomSpyReporter on Atom {
reportObserved();
}

void reportWrite<T>(T newValue, T oldValue, void Function() setNewValue) {
void reportWrite<T>(T newValue, T oldValue, void Function() setNewValue,
{EqualityComparer<T>? equals}) {
final areEqual =
equals == null ? oldValue == newValue : equals(oldValue, newValue);

// Avoid unnecessary observable notifications of @observable fields of Stores
if (newValue == oldValue) {
if (areEqual) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion mobx/lib/version.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!!

/// The current version as per `pubspec.yaml`.
const version = '2.1.4';
const version = '2.2.0';
2 changes: 1 addition & 1 deletion mobx/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: mobx
version: 2.1.4
version: 2.2.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
Expand Down
8 changes: 8 additions & 0 deletions mobx/test/annotations_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@ void main() {
expect(StoreConfig, isNotNull);
expect(readonly, isNotNull);
});

test('observableAlwaysNotEqual should return false', () {
expect(observableAlwaysNotEqual(1, 2), isFalse);
expect(observableAlwaysNotEqual(1, 1), isFalse);
expect(observableAlwaysNotEqual('a', 'a'), isFalse);
expect(observableAlwaysNotEqual(true, true), isFalse);
expect(observableAlwaysNotEqual(false, false), isFalse);
});
}
80 changes: 80 additions & 0 deletions mobx/test/atom_extensions_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,57 @@ void main() {

expect(autorunResults, ['first']);
});

test(
'when write to @alwaysNotify field with unchanged value, should trigger notifications for downstream',
() {
final store = _ExampleStore();

final autorunResults = <String>[];
autorun((_) => autorunResults.add(store.value2));

expect(autorunResults, ['first']);

store.value2 = store.value2;

expect(autorunResults, ['first', 'first']);
});

test(
'when write to @MakeObservable(equals: "a?.length == b?.length") field with changed value and not equals, should trigger notifications for downstream',
() {
final store = _ExampleStore();

final autorunResults = <String>[];
autorun((_) => autorunResults.add(store.value3));

expect(autorunResults, ['first']); // length: 5

// length: 5, should not trigger
store.value3 = 'third';

expect(autorunResults, ['first']);

// length: 6, should trigger
store.value3 = 'second';

expect(autorunResults, ['first', 'second']);
});
}

class _ExampleStore = __ExampleStore with _$_ExampleStore;

bool _equals(String? oldValue, String? newValue) => (oldValue == newValue);

abstract class __ExampleStore with Store {
@observable
String value = 'first';

@alwaysNotify
String value2 = 'first';

@MakeObservable(equals: _equals)
String value3 = 'first';
}

// This is what typically a mobx codegen will generate.
Expand All @@ -58,4 +102,40 @@ mixin _$_ExampleStore on __ExampleStore, Store {
super.value = value;
});
}

// ignore: non_constant_identifier_names
late final _$value2Atom =
Atom(name: '__ExampleStore.value2', context: context);

@override
String get value2 {
_$value2Atom.reportRead();
return super.value2;
}

@override
set value2(String value) {
_$value2Atom.reportWrite(value, super.value2, () {
super.value2 = value;
}, equals: (String? oldValue, String? newValue) => false);
}

// ignore: non_constant_identifier_names
late final _$value3Atom =
Atom(name: '__ExampleStore.value3', context: context);

@override
String get value3 {
_$value3Atom.reportRead();
return super.value3;
}

@override
set value3(String value) {
_$value3Atom.reportWrite(value, super.value3, () {
super.value3 = value;
},
equals: (String? oldValue, String? newValue) =>
oldValue?.length == newValue?.length);
}
}
5 changes: 5 additions & 0 deletions mobx_codegen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.3.0

- Adds `@alwaysNotify` annotation support for creating always notify observables. [@amondnet](https://github.com/amondnet) in [#907](https://github.com/mobxjs/mobx.dart/pull/907)
- Adds custom `equals` for creating observables. [@amondnet](https://github.com/amondnet) in [#907](https://github.com/mobxjs/mobx.dart/pull/907)

## 2.2.0

- Adds support for annotations `@protected`, `@visibleForTesting` and `@visibleForOverriding` for actions, observables futures and observables stream.
Expand Down
7 changes: 7 additions & 0 deletions mobx_codegen/lib/src/store_class_visitor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:analyzer/dart/element/visitor.dart';
import 'package:build/build.dart';
import 'package:meta/meta.dart';
import 'package:mobx/mobx.dart';

// ignore: implementation_imports
import 'package:mobx/src/api/annotations.dart'
show ComputedMethod, MakeAction, MakeObservable, StoreConfig;
Expand Down Expand Up @@ -101,6 +102,7 @@ class StoreClassVisitor extends SimpleElementVisitor {
name: element.name,
isPrivate: element.isPrivate,
isReadOnly: _isObservableReadOnly(element),
equals: _getEquals(element),
);

_storeTemplate.observables.add(template);
Expand All @@ -114,6 +116,11 @@ class StoreClassVisitor extends SimpleElementVisitor {
?.toBoolValue() ??
false;

ExecutableElement? _getEquals(FieldElement element) => _observableChecker
.firstAnnotationOfExact(element)
?.getField('equals')
?.toFunctionValue();

bool _fieldIsNotValid(FieldElement element) => _any([
errors.staticObservables.addIf(element.isStatic, element.name),
errors.finalObservables.addIf(element.isFinal, element.name),
Expand Down
5 changes: 4 additions & 1 deletion mobx_codegen/lib/src/template/observable.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:meta/meta.dart';
import 'package:mobx_codegen/src/template/store.dart';
import 'package:mobx_codegen/src/utils/non_private_name_extension.dart';
Expand All @@ -10,6 +11,7 @@ class ObservableTemplate {
required this.name,
this.isReadOnly = false,
this.isPrivate = false,
this.equals,
});

final StoreTemplate storeTemplate;
Expand All @@ -18,6 +20,7 @@ class ObservableTemplate {
final String name;
final bool isPrivate;
final bool isReadOnly;
final ExecutableElement? equals;

/// Formats the `name` from `_foo_bar` to `foo_bar`
/// such that the getter gets public
Expand Down Expand Up @@ -58,6 +61,6 @@ ${_buildGetters()}
set $name($type value) {
$atomName.reportWrite(value, super.$name, () {
super.$name = value;
});
}${equals != null ? ', equals: ${equals!.name}' : ''});
}""";
}
4 changes: 2 additions & 2 deletions mobx_codegen/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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.2.0
version: 2.3.0

homepage: https://github.com/mobxjs/mobx.dart
issue_tracker: https://github.com/mobxjs/mobx.dart/issues
Expand All @@ -13,7 +13,7 @@ dependencies:
build: ^2.2.1
build_resolvers: ^2.0.6
meta: ^1.3.0
mobx: ^2.0.7
mobx: ^2.2.0
path: ^1.8.0
source_gen: ^1.2.1

Expand Down
8 changes: 8 additions & 0 deletions mobx_codegen/test/generator_usage_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ part 'generator_usage_test.g.dart';
// ignore: library_private_types_in_public_api
class TestStore = _TestStore with _$TestStore;

bool customEquals(String? oldValue, String? newValue) => oldValue != newValue;

abstract class _TestStore with Store {
// ignore: unused_element
_TestStore(this.field1, {this.field2});
Expand All @@ -28,6 +30,12 @@ abstract class _TestStore with Store {
@observable
String stuff = 'stuff';

@alwaysNotify
String always = 'stuff';

@MakeObservable(equals: customEquals)
String custom = 'stuff';

@action
Future<void> loadStuff() async {
stuff = 'stuff0';
Expand Down
Loading