blob: 0bd034e4301ff77e4971a1ab448816beec9f60dc [file] [log] [blame]
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:mobx/mobx.dart';
// ignore: implementation_imports
import 'package:mobx/src/core.dart' show ReactionImpl;
/// Whether to warn when there is no observables in the builder function
bool enableWarnWhenNoObservables = true;
/// Observer observes the observables used in the `build` method and rebuilds
/// the Widget whenever any of them change. There is no need to do any other
/// wiring besides simply referencing the required observables.
///
/// Internally, [ObserverWidgetMixin] uses a [Reaction] around the `build`
/// method.
///
/// If your `build` method does not contain any observables,
/// [ObserverWidgetMixin] will print a warning on the console. This is a
/// debug-time hint to let you know that you are not observing any observables.
mixin ObserverWidgetMixin on Widget {
/// An identifiable name that can be overriden for debugging.
String getName();
/// The context within which its reaction should be run. It is the
/// [mainContext] in most cases.
ReactiveContext getContext() => mainContext;
/// A convenience method used for testing.
@visibleForTesting
Reaction createReaction(
Function() onInvalidate, {
Function(Object, Reaction)? onError,
}) =>
ReactionImpl(
getContext(),
onInvalidate,
name: getName(),
onError: onError,
);
/// Convenience method to output console messages as debugging output. Logging
/// usually happens when some internal error needs to be surfaced to the user.
void log(String msg) {
debugPrint(msg);
}
/// Whether to warn when there is no observables in the builder function
/// null means true
bool? get warnWhenNoObservables => null;
// We don't override `createElement` to specify that it should return a
// `ObserverElementMixin` as it'd make the mixin impossible to use.
}
/// A mixin that overrides [build] to listen to the observables used by
/// [ObserverWidgetMixin].
mixin ObserverElementMixin on ComponentElement {
ReactionImpl get reaction => _reaction!;
// null means it is unmounted
ReactionImpl? _reaction;
// Not using the original `widget` getter as it would otherwise make the mixin
// impossible to use
ObserverWidgetMixin get _widget => widget as ObserverWidgetMixin;
@override
void mount(Element? parent, dynamic newSlot) {
_reaction = _widget.createReaction(invalidate, onError: (e, _) {
FlutterError.reportError(FlutterErrorDetails(
library: 'flutter_mobx',
exception: e,
stack: e is Error ? e.stackTrace : null,
context: ErrorDescription(
'From reaction of ${_widget.getName()} of type $runtimeType.'),
));
}) as ReactionImpl;
super.mount(parent, newSlot);
}
void invalidate() => _markNeedsBuildImmediatelyOrDelayed();
void _markNeedsBuildImmediatelyOrDelayed() async {
// reference
// 1. https://github.com/mobxjs/mobx.dart/issues/768
// 2. https://stackoverflow.com/a/64702218/4619958
// 3. https://stackoverflow.com/questions/71367080
// if there's a current frame,
// ignore: unnecessary_non_null_assertion
final schedulerPhase = SchedulerBinding.instance.schedulerPhase;
final shouldWait =
// surely, `idle` is ok
schedulerPhase != SchedulerPhase.idle &&
// By experience, it is safe to do something like
// `SchedulerBinding.addPostFrameCallback((_) => someObservable.value = newValue)`
// So it is safe if we are in this phase
schedulerPhase != SchedulerPhase.postFrameCallbacks;
if (shouldWait) {
// uncomment to log
// print('hi wait phase=$schedulerPhase');
// wait for the end of that frame.
// ignore: unnecessary_non_null_assertion
await SchedulerBinding.instance.endOfFrame;
// If it is disposed after this frame, we should no longer call `markNeedsBuild`
if (_reaction == null) return;
}
markNeedsBuild();
}
@override
Widget build() {
Widget? built;
reaction.track(() {
built = super.build();
});
if (enableWarnWhenNoObservables &&
(_widget.warnWhenNoObservables ?? true) &&
!reaction.hasObservables) {
_widget.log(
'No observables detected in the build method of ${reaction.name}',
);
}
// This "throw" is better than a "LateInitializationError"
// which confused the user. Please see #780 for details.
if (built == null) {
throw Exception(
'Error happened when building ${_widget.runtimeType}, but it was captured since disableErrorBoundaries==true');
}
return built!;
}
@override
void unmount() {
_reaction!.dispose();
_reaction = null;
super.unmount();
}
}