diff --git a/mobx/CHANGELOG.md b/mobx/CHANGELOG.md index 1a3c5ede..82364c36 100644 --- a/mobx/CHANGELOG.md +++ b/mobx/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.0 + +- Add `scheduler` to `reaction` and `autorun` to allow customizing the scheduler used to schedule the reaction. By [@amondnet]((https://github.com/amondnet). + ## 2.3.3+1 - 2.3.3+2 - Analyzer fixes diff --git a/mobx/lib/src/api/reaction.dart b/mobx/lib/src/api/reaction.dart index d23d4a54..c95a751a 100644 --- a/mobx/lib/src/api/reaction.dart +++ b/mobx/lib/src/api/reaction.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:mobx/src/api/context.dart'; import 'package:mobx/src/core.dart'; @@ -6,7 +8,10 @@ import 'package:mobx/src/core.dart'; /// /// Optional configuration: /// * [name]: debug name for this reaction -/// * [delay]: throttling delay in milliseconds +/// * [delay]: Number of milliseconds that can be used to throttle the effect function. If zero (default), no throttling happens. +/// * [context]: the [ReactiveContext] to use. By default the [mainContext] is used. +/// * [scheduler]: Set a custom scheduler to determine how re-running the autorun function should be scheduled. It takes a function that should be invoked at some point in the future. +/// * [onError]: By default, any exception thrown inside an reaction will be logged, but not further thrown. This is to make sure that an exception in one reaction does not prevent the scheduled execution of other, possibly unrelated reactions. This also allows reactions to recover from exceptions. Throwing an exception does not break the tracking done by MobX, so subsequent runs of the reaction might complete normally again if the cause for the exception is removed. This option allows overriding that behavior. It is possible to set a global error handler or to disable catching errors completely using [ReactiveConfig]. /// /// ``` /// var x = Observable(10); @@ -27,13 +32,21 @@ ReactionDisposer autorun(Function(Reaction) fn, {String? name, int? delay, ReactiveContext? context, + Timer Function(void Function())? scheduler, void Function(Object, Reaction)? onError}) => createAutorun(context ?? mainContext, fn, - name: name, delay: delay, onError: onError); + name: name, delay: delay, scheduler: scheduler, onError: onError); /// Executes the [fn] function and tracks the observables used in it. Returns /// a function to dispose the reaction. /// +/// Optional configuration: +/// * [name]: debug name for this reaction +/// * [delay]: Number of milliseconds that can be used to throttle the effect function. If zero (default), no throttling happens. +/// * [context]: the [ReactiveContext] to use. By default the [mainContext] is used. +/// * [scheduler]: Set a custom scheduler to determine how re-running the autorun function should be scheduled. It takes a function that should be invoked at some point in the future. +/// * [onError]: By default, any exception thrown inside an reaction will be logged, but not further thrown. This is to make sure that an exception in one reaction does not prevent the scheduled execution of other, possibly unrelated reactions. This also allows reactions to recover from exceptions. Throwing an exception does not break the tracking done by MobX, so subsequent runs of the reaction might complete normally again if the cause for the exception is removed. This option allows overriding that behavior. It is possible to set a global error handler or to disable catching errors completely using [ReactiveConfig]. +/// /// The [fn] is supposed to return a value of type T. When it changes, the /// [effect] function is executed. /// @@ -43,20 +56,25 @@ ReactionDisposer autorun(Function(Reaction) fn, /// [fireImmediately] if you want to invoke the effect immediately without waiting for /// the [fn] to change its value. It is possible to define a custom [equals] function /// to override the default comparison for the value returned by [fn], to have fined -/// grained control over when the reactions should run. +/// grained control over when the reactions should run. By default, the [mainContext] +/// is used, but you can also pass in a custom [context]. +/// You can also pass in an optional [onError] handler for errors thrown during the [fn] execution. +/// You can also pass in an optional [scheduler] to schedule the [effect] execution. ReactionDisposer reaction(T Function(Reaction) fn, void Function(T) effect, {String? name, int? delay, bool? fireImmediately, EqualityComparer? equals, ReactiveContext? context, + Timer Function(void Function())? scheduler, void Function(Object, Reaction)? onError}) => createReaction(context ?? mainContext, fn, effect, name: name, delay: delay, equals: equals, fireImmediately: fireImmediately, - onError: onError); + onError: onError, + scheduler: scheduler); /// A one-time reaction that auto-disposes when the [predicate] becomes true. It also /// executes the [effect] when the predicate turns true. diff --git a/mobx/lib/src/core/reaction_helper.dart b/mobx/lib/src/core/reaction_helper.dart index 3ac39dcd..cb022706 100644 --- a/mobx/lib/src/core/reaction_helper.dart +++ b/mobx/lib/src/core/reaction_helper.dart @@ -23,19 +23,24 @@ class ReactionDisposer { /// An internal helper function to create a [autorun] ReactionDisposer createAutorun( ReactiveContext context, Function(Reaction) trackingFn, - {String? name, int? delay, void Function(Object, Reaction)? onError}) { + {String? name, + int? delay, + Timer Function(void Function())? scheduler, + void Function(Object, Reaction)? onError}) { late ReactionImpl rxn; final rxnName = name ?? context.nameFor('Autorun'); + final runSync = scheduler == null && delay == null; - if (delay == null) { + if (runSync) { // Use a sync-scheduler. rxn = ReactionImpl(context, () { rxn.track(() => trackingFn(rxn)); }, name: rxnName, onError: onError); } else { - // Use a delayed scheduler. - final scheduler = createDelayedScheduler(delay); + // Use a scheduler or delayed scheduler. + final schedulerFromOptions = + scheduler ?? (delay != null ? createDelayedScheduler(delay) : null); var isScheduled = false; Timer? timer; @@ -46,7 +51,7 @@ ReactionDisposer createAutorun( timer?.cancel(); timer = null; - timer = scheduler(() { + timer = schedulerFromOptions!(() { isScheduled = false; if (!rxn.isDisposed) { rxn.track(() => trackingFn(rxn)); @@ -69,6 +74,7 @@ ReactionDisposer createReaction( int? delay, bool? fireImmediately, EqualityComparer? equals, + Timer Function(void Function())? scheduler, void Function(Object, Reaction)? onError}) { late ReactionImpl rxn; @@ -77,8 +83,9 @@ ReactionDisposer createReaction( final effectAction = Action((T? value) => effect(value as T), name: '$rxnName-effect'); - final runSync = delay == null; - final scheduler = delay != null ? createDelayedScheduler(delay) : null; + final runSync = scheduler == null && delay == null; + final schedulerFromOptions = + scheduler ?? (delay != null ? createDelayedScheduler(delay) : null); var firstTime = true; T? value; @@ -124,7 +131,7 @@ ReactionDisposer createReaction( timer?.cancel(); timer = null; - timer = scheduler!(() { + timer = schedulerFromOptions!(() { isScheduled = false; if (!rxn.isDisposed) { reactionRunner(); diff --git a/mobx/lib/version.dart b/mobx/lib/version.dart index 26649ca4..5c81252f 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.3.3+2'; +const version = '2.4.0'; diff --git a/mobx/pubspec.yaml b/mobx/pubspec.yaml index 0417f086..9bfc8d50 100644 --- a/mobx/pubspec.yaml +++ b/mobx/pubspec.yaml @@ -1,5 +1,5 @@ name: mobx -version: 2.3.3+2 +version: 2.4.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." repository: https://github.com/mobxjs/mobx.dart diff --git a/mobx/test/autorun_test.dart b/mobx/test/autorun_test.dart index 3330d8a0..274ec62f 100644 --- a/mobx/test/autorun_test.dart +++ b/mobx/test/autorun_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fake_async/fake_async.dart'; import 'package:mobx/mobx.dart'; import 'package:mocktail/mocktail.dart' as mock; @@ -101,6 +103,38 @@ void main() { dispose(); }); + test('with custom scheduler', () { + late Function dispose; + const delayMs = 5000; + + final x = Observable(10); + var value = 0; + + fakeAsync((async) { + dispose = autorun((_) { + value = x.value + 1; + }, scheduler: (f) { + return Timer(const Duration(milliseconds: delayMs), f); + }).call; + + async.elapse(const Duration(milliseconds: 2500)); + + expect(value, 0); // autorun() should not have executed at this time + + async.elapse(const Duration(milliseconds: 2500)); + + expect(value, 11); // autorun() should have executed + + x.value = 100; + + expect(value, 11); // should still retain the last value + async.elapse(const Duration(milliseconds: delayMs)); + expect(value, 101); // should change now + }); + + dispose(); + }); + test('with pre-mature disposal in tracking function', () { final x = Observable(10); diff --git a/mobx/test/reaction_test.dart b/mobx/test/reaction_test.dart index 3fe6feb5..f1649039 100644 --- a/mobx/test/reaction_test.dart +++ b/mobx/test/reaction_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fake_async/fake_async.dart'; import 'package:mobx/mobx.dart' hide when; import 'package:mobx/src/core.dart'; @@ -109,6 +111,31 @@ void main() { }); }); + test('works with scheduler', () { + final x = Observable(10); + var executed = false; + + final d = reaction((_) => x.value > 10, (isGreaterThan10) { + executed = true; + }, scheduler: (fn) => Timer(const Duration(milliseconds: 1000), fn)); + + fakeAsync((async) { + x.value = 11; + + // Even though tracking function has changed, effect should not be executed + expect(executed, isFalse); + async.elapse(const Duration(milliseconds: 500)); + expect( + executed, isFalse); // should still be false as 1s has not elapsed + + async.elapse( + const Duration(milliseconds: 500)); // should now trigger effect + expect(executed, isTrue); + + d(); + }); + }); + test('that fires immediately', () { final x = Observable(10); var executed = false;