blob: 79929519f7cc1548d182a27678cec9f76e1ea941 [file] [log] [blame]
part of '../core.dart';
class Computed<T> extends Atom implements Derivation, ObservableValue<T> {
/// Creates a computed value with an optional [name].
///
/// The passed in function: [fn], is used to give back the computed value.
/// Computed values can depend on other observables and computed values!
/// This makes them both an *observable* and an *observer*.
/// Computed values are also referred to as _derived-values_ because they inherently _derive_ their
/// value from other observables. Don't underestimate the power of the **computed**.
/// They are possibly the most powerful observables in your application.
///
/// A computed's value is read with the `value` property.
///
/// It is possible to override equality comparison (when deciding whether to notify observers)
/// by providing an [equals] comparator.
///
/// ```
/// var x = Observable(10);
/// var y = Observable(10);
/// var total = Computed((){
/// return x.value + y.value;
/// });
///
/// x.value = 100; // recomputes total
/// y.value = 100; // recomputes total again
///
/// print('total = ${total.value}'); // prints "total = 200"
/// ```
///
/// A computed value is _cached_ and it recomputes only when the dependent observables actually
/// change. This makes them fast and you are free to use them throughout your application. Internally
/// MobX uses a 2-phase change propagation that ensures no unnecessary computations are performed.
factory Computed(T Function() fn,
{String? name,
ReactiveContext? context,
EqualityComparer<T>? equals}) =>
Computed._(context ?? mainContext, fn, name: name, equals: equals);
Computed._(ReactiveContext context, this._fn, {String? name, this.equals})
: super._(context, name: name ?? context.nameFor('Computed'));
final EqualityComparer<T>? equals;
@override
MobXCaughtException? _errorValue;
@override
MobXCaughtException? get errorValue => _errorValue;
@override
// ignore: prefer_final_fields
Set<Atom> _observables = {};
@override
Set<Atom>? _newObservables;
T Function() _fn;
@override
// ignore: prefer_final_fields
DerivationState _dependenciesState = DerivationState.notTracking;
T? _value;
bool _isComputing = false;
@override
T get value {
if (_isComputing) {
throw MobXCyclicReactionException(
'Cycle detected in computation $name: $_fn');
}
if (!_context.isWithinBatch && _observers.isEmpty) {
if (_context._shouldCompute(this)) {
_context.startBatch();
_value = computeValue(track: false);
_context.endBatch();
}
} else {
reportObserved();
if (_context._shouldCompute(this)) {
if (_trackAndCompute()) {
_context._propagateChangeConfirmed(this);
}
}
}
if (_context._hasCaughtException(this)) {
throw _errorValue!;
}
return _value as T;
}
T? computeValue({required bool track}) {
_isComputing = true;
_context._pushComputation();
T? value;
if (track) {
value = _context.trackDerivation(this, _fn);
} else {
if (_context.config.disableErrorBoundaries == true) {
value = _fn();
} else {
try {
value = _fn();
_errorValue = null;
} on Object catch (e, s) {
_errorValue = MobXCaughtException(e, stackTrace: s);
}
}
}
_context._popComputation();
_isComputing = false;
return value;
}
@override
void _suspend() {
_context._clearObservables(this);
_value = null;
}
@override
void _onBecomeStale() {
_context._propagatePossiblyChanged(this);
}
bool _trackAndCompute() {
if (_context.isSpyEnabled) {
_context.spyReport(ComputedValueSpyEvent(this, name: name));
}
final oldValue = _value;
final wasSuspended = _dependenciesState == DerivationState.notTracking;
final hadCaughtException = _context._hasCaughtException(this);
final newValue = computeValue(track: true);
final changedException =
hadCaughtException != _context._hasCaughtException(this);
final changed =
wasSuspended || changedException || !_isEqual(oldValue, newValue);
if (changed) {
_value = newValue;
}
return changed;
}
bool _isEqual(T? x, T? y) => equals == null ? x == y : equals!(x, y);
void Function() observe(
void Function(ChangeNotification<T>) handler,
{@Deprecated('fireImmediately has no effect anymore. It is on by default.')
bool? fireImmediately}) {
T? prevValue;
void notifyChange() {
_context.untracked(() {
handler(ChangeNotification(
type: OperationType.update,
object: this,
oldValue: prevValue,
newValue: value));
});
}
return autorun((_) {
final newValue = value;
notifyChange();
prevValue = newValue;
}, context: _context)
.call;
}
}