From 1172b5e89e071ff8f874cd0be31ea01f76eba7e4 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 27 Dec 2023 16:10:12 +0900 Subject: [PATCH] feat: add scheduler option to autorun and reaction --- mobx/CHANGELOG.md | 4 +++ mobx/lib/src/api/reaction.dart | 26 +++++++++++++++++--- mobx/lib/src/core/reaction_helper.dart | 23 +++++++++++------ mobx/lib/version.dart | 2 +- mobx/pubspec.yaml | 2 +- mobx/test/autorun_test.dart | 34 ++++++++++++++++++++++++++ mobx/test/reaction_test.dart | 27 ++++++++++++++++++++ 7 files changed, 104 insertions(+), 14 deletions(-) diff --git a/mobx/CHANGELOG.md b/mobx/CHANGELOG.md index 1a3c5edef..82364c36b 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 d23d4a543..c95a751a4 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 3ac39dcd8..cb0227066 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 26649ca45..5c81252f7 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 0417f0868..9bfc8d508 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 3330d8a02..274ec62f5 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 3fe6feb56..f16490395 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;