state_notifier 0.5.0

Dart native js
Flutter Android iOS web

ValueNotifier, but outside Flutter and with some extra perks

example/lib/state_notifier.dart

library state_notifier;

import 'dart:async';
import 'dart:collection';

import 'package:meta/meta.dart';

/// A listener that can be added to a [StateNotifier] using
/// [StateNotifier.addListener].
///
/// This callback receives the current [StateNotifier.state] as parameter.
typedef Listener<T> = void Function(T state);

/// A callback that can be used to remove a listener added with
/// [StateNotifier.addListener].
///
/// It is safe to call this callback multiple times.
///
/// Calling this callback multiple times will remove the listener only once,
/// even if [StateNotifier.addListener] was called multiple times with the exact
/// same listener.
typedef RemoveListener = void Function();

/// A callback that can be passed to [StateNotifier.onError].
///
/// This callback should not throw.
///
/// It exists merely for error reporting (mainly `FlutterError.onError`), and
/// should not be used otherwise.\
/// If you need an error status, consider adding an error property on
/// your custom [StateNotifier.state].
typedef ErrorListener = void Function(dynamic error, StackTrace stackTrace);

/// A function that allows obtaining other objects.
///
/// This is usually equivalent to `Provider.of`, but with no dependency on Flutter
///
/// May throw a [DependencyNotFoundException].
typedef Locator = T Function<T>();

/// An observable class that stores a single immutable [state].
///
/// This class can be considered as a fork of `ValueNotifier` from Flutter, with
/// subtle API changes.
///
/// # Reading the current [state]:
///
/// The [state] property is protected and should be used only by the subclasses
/// of [StateNotifier].\
/// If you want to obtain the current [state] from outside of [StateNotifier],
/// consider using [addListener] instead.
///
/// All listeners added with [addListener] are called **immediately** on addition
/// with the current [state] as parameter, or whenever [state] changes.
///
/// # The differences with `ValueNotifier`
///
/// [StateNotifier] is not a one to one reimplementation of `ValueNotifier`.
/// It has a few notable differences:
///
/// - it does not depend on Flutter
/// - both the getter and setter of [state] are protected.
/// - adding and removing listeners is O(1) (vs O(N) for `ValueNotifier`)
/// - there is no `notifyListeners` method.
/// - listeners cannot add more listeners.
/// - calling all listeners is O(N) (vs O(N^2) for `ValueNotifier`)
/// - there is no `removeListener` function.
///   Instead, [addListener] returns a function that allows removing the listener.
/// - error reporting is done through [onError].
/// - [StateNotifier] exposes a [mounted] boolean, to know if [dispose] was
///   called or not.
/// - listeners added with [addListener] receives the current [state] as parameter
/// - calling [addListener] immediately calls the listener with the current [state].
abstract class StateNotifier<T> {
  /// Initialize [state].
  StateNotifier(this._state);

  final _listeners = LinkedList<_ListenerEntry<T>>();

  /// A callback for error reporting if one of the listeners added with [addListener] throws.
  ///
  /// This callback should not throw.
  ///
  /// It exists for error reporting (mainly `FlutterError.onError`), and
  /// should not be used otherwise.\
  /// If you need an error status, consider adding an error property on
  /// your custom [state].
  /// 
  /// If no [onError] is specified, fallbacks to [Zone.current.handleUncaughtError].
  ErrorListener onError;

  bool _mounted = true;

  /// Whether [dispose] was called or not.
  bool get mounted => _mounted;

  bool _debugCanAddListeners = true;

  bool _debugSetCanAddListeners(bool value) {
    assert(() {
      _debugCanAddListeners = value;
      return true;
    }());
    return true;
  }

  bool _debugIsMounted() {
    assert(() {
      if (!_mounted) {
        throw StateError('''
Tried to use $runtimeType after `dispose` was called.

Consider checking `mounted`.
''');
      }
      return true;
    }());
    return true;
  }

  T _state;

  /// The current "state" of this [StateNotifier].
  ///
  /// Updating this variable will synchronously call all the listeners.
  ///
  /// Updating the state will throw if at least one listener throws.
  @protected
  T get state {
    assert(_debugIsMounted());
    return _state;
  }

  /// A development-only way to access [state] outside of [StateNotifier].
  ///
  /// The only difference with [state] is that [debugState] is not "protected".\
  /// Will not work in release mode.
  ///
  /// This is useful for tests.
  T get debugState {
    T result;
    assert(() {
      result = _state;
      return true;
    }());
    return result;
  }

  @protected
  set state(T value) {
    assert(_debugIsMounted());
    _state = value;

    var didThrow = false;
    for (final listenerEntry in _listeners) {
      try {
        listenerEntry.listener(value);
      } catch (error, stackTrace) {
        didThrow = true;
        if (onError != null) {
          onError(error, stackTrace);
        } else {
          Zone.current.handleUncaughtError(error, stackTrace);
        }
      }
    }
    if (didThrow) {
      throw Error();
    }
  }

  /// If a listener has been added using [addListener] and hasn't been removed yet.
  bool get hasListeners {
    assert(_debugIsMounted());
    return _listeners.isNotEmpty;
  }

  /// Subscribes to this object.
  ///
  /// The [listener] callback will be called immediately on addition and
  /// synchronously whenever [state] changes.
  /// Set [fireImmediately] to false if you want to skip the first,
  /// immediate execution of the [listener].
  ///
  ///
  /// To remove this [listener], call the function returned by [addListener]:
  ///
  /// ```dart
  /// StateNotifier<Model> example;
  /// final removeListener = example.addListener((value) => ...);
  /// removeListener();
  /// ```
  ///
  /// Listeners cannot add other listeners.
  ///
  /// Adding and removing listeners is O(1).
  RemoveListener addListener(
    Listener<T> listener, {
    bool fireImmediately = true,
  }) {
    assert(() {
      if (!_debugCanAddListeners) {
        throw ConcurrentModificationError();
      }
      return true;
    }());
    assert(_debugIsMounted());
    final listenerEntry = _ListenerEntry(listener);
    _listeners.add(listenerEntry);
    try {
      assert(_debugSetCanAddListeners(false));
      if (fireImmediately) {
        listener(state);
      }
    } catch (err, stack) {
      listenerEntry.unlink();
      onError?.call(err, stack);
      rethrow;
    } finally {
      assert(_debugSetCanAddListeners(true));
    }

    return () {
      if (listenerEntry.list != null) {
        listenerEntry.unlink();
      }
    };
  }

  /// Frees all the resources associated to this object.
  ///
  /// This marks the object as no longer usable and will make all methods/properties
  /// besides [mounted] inaccessible.
  @mustCallSuper
  void dispose() {
    assert(_debugIsMounted());
    _listeners.clear();
    _mounted = false;
  }
}

class _ListenerEntry<T> extends LinkedListEntry<_ListenerEntry<T>> {
  _ListenerEntry(this.listener);

  final Listener<T> listener;
}

/// A mixin that adds service location capability to an object.
///
/// This makes the object aware of things like `Provider.of` or `GetIt`, without
/// actually depending on those.\
/// It also provides testing utilities to be able to mock dependencies.
///
/// In the context of Flutter + `provider`, adding that mixin to an object
/// makes it impossible to shared one instance across multiple "providers".
///
/// This mix-in does not do anything by itself.\
/// It is simply an interface for 3rd party libraries such as provider to implement
/// the logic.
///
/// See also:
///
/// - [read], to read objects
/// - [debugMockDependency], to mock dependencies returned by [read]
/// - [update], a new life-cycle to synchronize this object with external objects.
/// - [debugUpdate], a method to test [update].
mixin LocatorMixin {
  // ignore: prefer_function_declarations_over_variables
  Locator _locator = <T>() => throw DependencyNotFoundException<T>();

  /// A function that allows obtaining other objects.
  ///
  /// It is typically equivalent to `Provider.of(context, listen: false)` when
  /// using `provider`, but it could also be `GetIt.get` for example.
  ///
  /// **DON'T** modify [read] manually.\
  /// The only reason why [read] is not `final` is so that it can be
  /// initialized by providers from `flutter_notifier`.
  ///
  /// May throw a [DependencyNotFoundException] if the looked-up type is not found.
  @protected
  Locator get read {
    assert(_debugIsNotifierMounted());
    return _locator;
  }

  set read(Locator read) {
    assert(_debugIsNotifierMounted());
    _locator = read;
  }

  bool _debugIsNotifierMounted() {
    assert(() {
      if (this is StateNotifier) {
        final instance = this as StateNotifier;
        assert(instance._debugIsMounted());
      }
      return true;
    }());
    return true;
  }

  /// Overrides [read] to mock its behavior in development.
  ///
  /// This does nothing in release mode and is useful only for test purpose.
  ///
  /// A typical usage would be:
  ///
  /// ```dart
  /// class MyServiceMock extends Mock implements MyService {}
  ///
  /// test('mock dependency', () {
  ///   final myStateNotifier = MyStateNotifier();
  ///   myStateNotifier.debugMockDependency<MyService>(MyServiceMock());
  /// });
  /// ```
  void debugMockDependency<Dependency>(Dependency value) {
    assert(_debugIsNotifierMounted());
    assert(() {
      final previousLocator = read;
      read = <Target>() {
        assert(_debugIsNotifierMounted());
        if (Dependency == Target) {
          return value as Target;
        }
        return previousLocator<Target>();
      };
      return true;
    }());
  }

  /// A life-cycle that allows initializing the [StateNotifier] based on values
  /// coming from [read].
  ///
  /// This method will be called once, right before [update].
  ///
  /// It is useful as constructors cannot access [read].
  @protected
  void initState() {}

  /// A life-cycle that allows listening to updates on another object.
  ///
  /// This is equivalent to what "ProxyProviders" do using `provider`, but
  /// implemented with no dependency on Flutter.
  ///
  /// The property [read] is not accessible while inside the body of [update].
  /// Use the parameter passed to [update] instead, which will not just read the
  /// object but also watch for changes.
  @protected
  void update(Locator watch) {}

  var _debugDidInitState = false;

  /// A test utility to test the behavior of your [initState]/[update] method.
  ///
  /// The first time [debugUpdate] is called, this will call [initState] + [update].
  /// Further calls will only call [update].
  ///
  /// While inside [update], [read] will be disabled as it would be in production.
  void debugUpdate() {
    assert(() {
      if (!_debugDidInitState) {
        _debugDidInitState = true;
        initState();
      }

      final locator = read;
      read = <T>() => throw StateError('Cannot use `read` inside `update`');
      try {
        update(locator);
      } finally {
        read = locator;
      }
      return true;
    }());
  }
}

/// Thrown when tried to call [LocatorMixin.read<T>()], but the [T] was not found.s
class DependencyNotFoundException<T> implements Exception {}
89
likes
100
pub points
96%
popularity

Publisher

dash-overflow.net

ValueNotifier, but outside Flutter and with some extra perks

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (LICENSE)

Dependencies

meta

More

Packages that depend on state_notifier