Created with <3 with dartpad.dev.
Created
August 15, 2022 13:41
-
-
Save qronos-ai/3a06ad834034ff74cebf38fd4866fa7d to your computer and use it in GitHub Desktop.
[Creator Example] Counter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:async'; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:flutter/material.dart'; | |
| // A counter app shows basic Creator/Watcher usage. | |
| // Creator creates a stream of data. | |
| final counter = Creator.value(0); | |
| void main() { | |
| // Wrap the app with a creator graph. | |
| runApp(CreatorGraph(child: const MyApp())); | |
| } | |
| class MyApp extends StatelessWidget { | |
| const MyApp({Key? key}) : super(key: key); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| home: Scaffold( | |
| appBar: AppBar(title: const Text('Counter example')), | |
| body: Center( | |
| // Watcher will rebuild whenever counter changes. | |
| child: Watcher((context, ref, _) { | |
| return Text('${ref.watch(counter)}'); | |
| }), | |
| ), | |
| floatingActionButton: FloatingActionButton( | |
| // Update state is easy. | |
| onPressed: () => | |
| context.ref.update<int>(counter, (count) => count + 1), | |
| child: const Icon(Icons.add), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // Below is a copy of creator source code. Since it is small and has zero | |
| // outside dependency, we can copy it here to make DartPad works. | |
| // ----------------------------------------------------------------------------- | |
| enum AsyncDataStatus { waiting, active } | |
| /// AsyncData is either in waiting state, or in active state with data. | |
| class AsyncData<T> { | |
| const AsyncData._(this.status, this.data); | |
| const AsyncData.waiting() : this._(AsyncDataStatus.waiting, null); | |
| const AsyncData.withData(T data) : this._(AsyncDataStatus.active, data); | |
| final AsyncDataStatus status; | |
| final T? data; | |
| @override | |
| int get hashCode => data.hashCode; | |
| @override | |
| bool operator ==(Object other) => other is AsyncData<T> && data == other.data; | |
| @override | |
| String toString() => | |
| status == AsyncDataStatus.waiting ? 'waiting' : 'active($data)'; | |
| } | |
| /// Base class of creators. Creators describe the graph dependencies. | |
| /// Also see [ElementBase] | |
| abstract class CreatorBase<T> { | |
| const CreatorBase({this.name, this.keepAlive = false, this.args}); | |
| /// Name for logging purpose. | |
| final String? name; | |
| String get infoName => name ?? _shortHash(this); | |
| String get debugName => '${name ?? ''}(${_shortHash(this)})'; | |
| /// Whether to keep the creator alive even if it loses all its watchers. | |
| final bool keepAlive; | |
| /// Creator with the same args are considered the same. args need to be | |
| /// unique within the graph. | |
| /// | |
| /// When a creator is defined as a local variable like this: | |
| /// ```dart | |
| /// final text = Creator((ref) { | |
| /// final double = Creator((ref) => ref.watch(number) * 2); | |
| /// return 'double: ${ref.watch(double)}'; | |
| /// }) | |
| /// ``` | |
| /// Here double is a local variable, it has different instances whenever text | |
| /// is recreated. The internal graph could change from number -> double_A -> | |
| /// text to number -> double_B -> text as the number changes. text still | |
| /// generates correct data, but there is extra cost to swap the node in the | |
| /// graph. Because the change is localized to only one node, the cost can be | |
| /// ignored as long as the create function is simple. | |
| /// | |
| /// Or we can set [args] to ask the framework to find an existing creator with | |
| /// the same args in the graph, to avoid the extra cost. | |
| /// | |
| /// Internally, args powers these features: | |
| /// * Creator group. | |
| /// profileCreator('userA') is a creator with args [profileCreator, 'userA']. | |
| /// * Async data. | |
| /// userCreator.asyncData is a creator with args [userCreator, 'asyncData']. | |
| /// * Change. | |
| /// number.change is a creator with args [number, 'change']. | |
| final List<Object?>? args; | |
| /// See [args]. | |
| @override | |
| bool operator ==(dynamic other) => | |
| other is CreatorBase<T> && | |
| (args != null ? _listEqual(args!, other.args ?? []) : super == other); | |
| /// See [args]. | |
| @override | |
| int get hashCode => (args != null) ? Object.hashAll(args!) : super.hashCode; | |
| /// Create its element. | |
| ElementBase<T> _createElement(Ref ref); | |
| } | |
| /// Base class of elements. Element holds the actual state. Creator and element | |
| /// always exist as a pair in the graph. | |
| /// | |
| /// Life cycle of creator/element: | |
| /// - It is added to the graph when firstly being watched. | |
| /// - It can be removed from the graph manually by [Ref.dispose]. | |
| /// - If it has watchers, it is automatically removed from the graph when losing | |
| /// all its watchers, unless keepAlive property is set. | |
| abstract class ElementBase<T> { | |
| ElementBase(this.ref, this.creator, this.state) | |
| : assert(ref._owner == creator); | |
| final Ref ref; // ref._owner is the creator | |
| final CreatorBase<T> creator; | |
| T state; | |
| T? prevState; | |
| Object? error; // Capture the exception happened during create. | |
| /// Get error-aware state. | |
| T getState() { | |
| if (error != null) { | |
| if (T is Future) { | |
| return Future.error(error!) as T; | |
| } else { | |
| throw error!; | |
| } | |
| } | |
| return state; | |
| } | |
| /// Whether the creator has been created at least once. | |
| bool created = false; | |
| /// Allow the creator to update its state. Called when: | |
| /// * the element is firstly added to the graph; | |
| /// * one of its dependencies' state changes; | |
| /// * user manually demands with [Ref.recreate]. | |
| /// | |
| /// The implementation should log its status by calling | |
| /// [Ref._onCreateStart], [Ref._onCreateFinish], [Ref._onStateChange] and | |
| /// [Ref._onError]. | |
| /// | |
| /// If [autoDispose] is set, it should be remove from the graph after create. | |
| /// This parameter is set to true for [Ref.read]. | |
| void recreate({bool autoDispose = false}); | |
| } | |
| /// Creator creates a stream of T. It is able to return a valid state when | |
| /// firstly being watched. | |
| class Creator<T> extends CreatorBase<T> { | |
| const Creator(this.create, {super.name, super.keepAlive, super.args}); | |
| /// Create a [Creator] with an initial value. | |
| Creator.value(T t, | |
| {String? name, bool keepAlive = false, List<Object?>? args}) | |
| : this((ref) => t, name: name, keepAlive: keepAlive, args: args); | |
| final T Function(Ref) create; | |
| static const arg1 = _CreatorWithArg1(); | |
| static const arg2 = _CreatorWithArg2(); | |
| static const arg3 = _CreatorWithArg3(); | |
| @override | |
| CreatorElement<T> _createElement(Ref ref) => CreatorElement<T>(ref, this); | |
| } | |
| /// Creator's element. | |
| class CreatorElement<T> extends ElementBase<T> { | |
| // Note here we call create function to get the initial state. | |
| CreatorElement(ref, Creator<T> creator) | |
| : super(ref, creator, creator.create(ref)); | |
| @override | |
| void recreate({CreatorBase? reason, bool autoDispose = false}) { | |
| error = null; | |
| // No need to recreate if initializing, since create is called in | |
| // constructor already. | |
| if (created) { | |
| ref._onCreateStart(); | |
| final prevState = this.prevState; // Save in case of error | |
| this.prevState = state; | |
| try { | |
| state = (creator as Creator<T>).create(ref); | |
| } catch (error) { | |
| this.error = error; | |
| this.prevState = prevState; | |
| } | |
| } | |
| if (error != null) { | |
| ref._onError(creator, error); | |
| } else if (prevState != state) { | |
| ref._onStateChange(creator, prevState, state); | |
| } | |
| if (autoDispose) { | |
| ref.dispose(creator); | |
| } | |
| if (created) { | |
| ref._onCreateFinish(); | |
| } | |
| created = true; | |
| } | |
| } | |
| /// Emitter creates a stream of Future<T>. | |
| /// | |
| /// When an emitter firstly being watched, it create an empty future, which is | |
| /// completed by the first emit call. Subsequence emit call will update its | |
| /// state to Future.value. | |
| /// | |
| /// This means that when emitter notifies its watcher about a state change, its | |
| /// watcher gets a Future.value, which can resolves immediately. | |
| class Emitter<T> extends CreatorBase<Future<T>> { | |
| const Emitter(this.create, {super.name, super.keepAlive, super.args}); | |
| /// Create an [Emitter] from an existing stream. It works both sync and async. | |
| /// | |
| /// ```dart | |
| /// final authCreator = Emitter.stream( | |
| /// (ref) => FirebaseAuth.instance.authStateChanges()); | |
| /// final userCreator = Emitter.stream((ref) async { | |
| /// final uid = await ref.watch( | |
| /// authCreator.where((auth) => auth != null)).map((auth) => auth!.uid); | |
| /// return FirebaseFirestore.instance.collection('users').doc(uid).snapshots(); | |
| /// }); | |
| /// ``` | |
| Emitter.stream(FutureOr<Stream<T>> Function(Ref) stream, | |
| {String? name, bool keepAlive = false, List<Object?>? args}) | |
| : this((ref, emit) async { | |
| final cancel = | |
| (await stream(ref)).listen((value) => emit(value)).cancel; | |
| ref.onClean(cancel); | |
| }, name: name, keepAlive: keepAlive, args: args); | |
| /// User provided create function. It can use ref to get data from the graph | |
| /// then call emit to push data to the graph. emit can be called multiple | |
| /// times. | |
| final FutureOr<void> Function(Ref ref, void Function(T) emit) create; | |
| static const arg1 = _EmitterWithArg1(); | |
| static const arg2 = _EmitterWithArg2(); | |
| static const arg3 = _EmitterWithArg3(); | |
| @override | |
| EmitterElement<T> _createElement(Ref ref) => | |
| EmitterElement<T>(ref, this, Completer()); | |
| } | |
| /// Emitter's element. | |
| class EmitterElement<T> extends ElementBase<Future<T>> { | |
| EmitterElement(ref, Emitter<T> creator, this.completer) | |
| : super(ref, creator, completer.future); | |
| /// This Completer produces a empty future in the constructor, then complete | |
| /// the future when the first state is emitted. In other words, an | |
| /// Emitter generates these states in sequence: | |
| /// 1. Future<T> (completer.future, completed by the first emit() call) | |
| /// 2. Future.value<T> (a new future, with data from the second emit() call) | |
| /// 3. ... then always Future.value<T>, unless error happens. | |
| final Completer<T> completer; | |
| /// Emitted value. Base class already saves Future<T> already, we save T here. | |
| T? value; | |
| T? prevValue; | |
| @override | |
| Future<void> recreate({bool autoDispose = false}) async { | |
| // Return if creation is not allowed, i.e. there is another job in progress. | |
| if (!ref._onCreateStart()) { | |
| return; | |
| } | |
| error = null; | |
| try { | |
| await (creator as Emitter<T>).create(ref, (newValue) { | |
| if (!created) { | |
| // emit is called the first time, let's wake up awaiting watchers. | |
| completer.complete(newValue); | |
| } else { | |
| prevState = state; | |
| state = Future<T>.value(newValue); | |
| } | |
| prevValue = value; | |
| value = newValue; | |
| if (!created || prevValue != value) { | |
| ref._onStateChange(creator, prevValue, value); | |
| } | |
| // Emitter is considered created as long as emit is called once. | |
| created = true; | |
| }); | |
| } catch (error) { | |
| this.error = error; | |
| ref._onError(creator, error); | |
| } | |
| if (autoDispose) { | |
| ref.dispose(creator); | |
| } | |
| ref._onCreateFinish(); | |
| } | |
| } | |
| /// Change wraps current state and previous state. | |
| class Change<T> { | |
| const Change(this.before, this.after); | |
| final T? before; | |
| final T after; | |
| @override | |
| int get hashCode => Object.hashAll([before, after]); | |
| @override | |
| bool operator ==(dynamic other) => | |
| other is Change<T> && before == other.before && after == other.after; | |
| @override | |
| String toString() => '$before->$after'; | |
| } | |
| extension CreatorChange<T> on Creator<T> { | |
| /// Return Creator<Change<T>>, which has both prev state and current state. | |
| Creator<Change<T>> get change { | |
| return Creator((ref) { | |
| ref.watch(this); | |
| final element = ref._element(this) as CreatorElement<T>; | |
| return Change(element.prevState, element.state); | |
| }, name: '${infoName}_change', args: [this, 'change']); | |
| } | |
| } | |
| extension EmitterChange<T> on Emitter<T> { | |
| /// Return Emitter<Change<T>>, which has both prev state and current state. | |
| Emitter<Change<T>> get change { | |
| return Emitter((ref, emit) async { | |
| await ref.watch(this); // Wait to ensure first value is emitted | |
| final element = ref._element(this) as EmitterElement<T>; | |
| emit(Change(element.prevValue, element.value as T)); | |
| }, name: '${infoName}_change', args: [this, 'change']); | |
| } | |
| } | |
| extension EmitterAsyncData<T> on Emitter<T> { | |
| /// Creator of AsyncData<T>, whose state can be "waiting" if the emitter has | |
| /// no data yet. Use AsyncData<T> instead T? because T could be nullable. | |
| Creator<AsyncData<T>> get asyncData { | |
| return Creator((ref) { | |
| ref.watch(this); | |
| final element = ref._element(this) as EmitterElement<T>; | |
| return element.completer.isCompleted | |
| ? AsyncData.withData(element.value as T) | |
| : const AsyncData.waiting(); | |
| }, name: '${infoName}_asyncData', args: [this, 'asyncData']); | |
| } | |
| } | |
| /// Copied from Flutter. [Object.hashCode]'s 20 least-significant bits. | |
| String _shortHash(Object? object) => | |
| object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0'); | |
| /// Check whether two lists are equal. | |
| bool _listEqual(List<Object?> a, List<Object?> b) { | |
| if (a.length != b.length) { | |
| return false; | |
| } | |
| for (var i = 0; i < a.length; i++) { | |
| if (a[i] != b[i]) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /// Ref holds the creator graph data, including states and dependencies. | |
| class Ref { | |
| Ref._({ | |
| CreatorBase? owner, | |
| CreatorObserver? observer, | |
| Map<CreatorBase, ElementBase>? elements, | |
| Graph<CreatorBase>? graph, | |
| Map<CreatorBase, Set<CreatorBase>>? before, | |
| Map<CreatorBase, Set<CreatorBase>>? after, | |
| Set<CreatorBase>? toCreate, | |
| final Map<CreatorBase, void Function()>? onClean, | |
| }) : _owner = owner, | |
| _observer = observer, | |
| _elements = elements ?? {}, | |
| _graph = graph ?? | |
| Graph(name: (c) => c.infoName, keepAlive: (c) => c.keepAlive), | |
| _before = before ?? {}, | |
| _after = after ?? {}, | |
| _toCreate = toCreate ?? {}, | |
| _clean = onClean ?? {}; | |
| Ref({CreatorObserver? observer}) : this._(observer: observer); | |
| /// Create a lightweight copy of Ref with a owner. The copy points to | |
| /// the same internal graph. See [_owner]. | |
| Ref _copy(CreatorBase owner) { | |
| return Ref._( | |
| owner: owner, | |
| observer: _observer, | |
| elements: _elements, | |
| graph: _graph, | |
| before: _before, | |
| after: _after, | |
| toCreate: _toCreate, | |
| onClean: _clean, | |
| ); | |
| } | |
| /// Owner of this Ref. It is used to establish the dependency when [watch]. | |
| /// | |
| /// ```dart | |
| /// Ref ref; // ref._owner is null | |
| /// final secondCreator = Creator((ref) { // ref._owner is secondCreator | |
| /// // When this watch is called, we can establish the dependency of | |
| /// // firstCreator -> ref._owner (secondCreator). | |
| /// final first = ref.watch(firstCreator); | |
| /// return first * 2; | |
| /// }); | |
| /// ``` | |
| final CreatorBase? _owner; | |
| /// Observer is called when state changes. | |
| final CreatorObserver? _observer; | |
| /// Elements which hold state. | |
| final Map<CreatorBase, ElementBase> _elements; | |
| /// Dependency graph. Think this as a directional graph. A -> [B, C] means if | |
| /// A changes, B and C need change too. | |
| final Graph<CreatorBase> _graph; | |
| /// Map of <creator being recreated, its dependency before recreating>. | |
| /// It is needed to allow dynamic dependencies. | |
| /// | |
| /// ```dart | |
| /// final C = Creator((ref) { | |
| /// final value = ref.watch(A); | |
| /// return value >= 0 ? value : ref.watch(B); | |
| /// }); | |
| /// ``` | |
| /// | |
| /// In this example, A -> C always exists, B -> C may or may not exist. | |
| /// | |
| /// Before we recreate C, we keep a copy of its old dependencies in [_before]. | |
| /// During recreation we record new dependencies in [_after]. Then we compare | |
| /// them once recreation is finished. | |
| /// | |
| /// Recreation might take a while if it is waiting for async task. While it is | |
| /// waiting, its dependency might changes and trigger a second recreation job. | |
| /// If this happens, we will simply add the creator to [_toCreate]. Later we | |
| /// can start the second job when first job is finished. | |
| final Map<CreatorBase, Set<CreatorBase>> _before; | |
| /// See [_before]. | |
| final Map<CreatorBase, Set<CreatorBase>> _after; | |
| /// See [_before]. | |
| final Set<CreatorBase> _toCreate; | |
| /// User provided call back function, which should be called before next | |
| /// recreate call or after creator is disposed. | |
| final Map<CreatorBase, void Function()> _clean; | |
| /// Get or create an element for creator. We call [ElementBase.recreate] | |
| /// following create the element, unless [recreate] is set to false. | |
| /// [autoDispose] makes creator remove itself from the graph after creating | |
| /// element and getting state. | |
| ElementBase<T> _element<T>(CreatorBase<T> creator, | |
| {bool recreate = true, bool autoDispose = false}) { | |
| if (_elements.containsKey(creator)) { | |
| return _elements[creator]! as ElementBase<T>; | |
| } | |
| // Note _copy is called here to get a copy of Ref. | |
| final element = creator._createElement(_copy(creator)); | |
| _elements[creator] = element; | |
| if (recreate) { | |
| element.recreate(autoDispose: autoDispose); | |
| } | |
| return element; | |
| } | |
| /// Read the current state of the creator. | |
| /// - If creator is already in the graph, its value is returned. | |
| /// - If creator is not in the graph, it is added to the graph then removed. | |
| T read<T>(CreatorBase<T> creator) { | |
| return _element<T>(creator, autoDispose: true).getState(); | |
| } | |
| /// Read the current state of the creator, also establish the dependency | |
| /// [creator] -> [_owner]. | |
| T watch<T>(CreatorBase<T> creator) { | |
| if (_owner != null) { | |
| _graph.addEdge(creator, _owner!); | |
| _after[_owner!]?.add(creator); | |
| } | |
| return _element<T>(creator).getState(); | |
| } | |
| /// Set state of the creator. Typically this is used to set the state for | |
| /// creator with no dependency, but the framework allows setting state for any | |
| /// creator. No-op if the state doesn't change. | |
| void set<T>(CreatorBase<T> creator, T state) { | |
| final element = _element<T>(creator, recreate: false); | |
| final before = element.state; | |
| if (before != state) { | |
| element.prevState = element.state; | |
| element.state = state; | |
| element.error = null; | |
| _onStateChange(creator, before, state); | |
| } | |
| } | |
| /// Set state of creator using an update function. See [set]. | |
| void update<T>(CreatorBase<T> creator, T Function(T) update) { | |
| set<T>(creator, update(_element(creator).state)); | |
| } | |
| /// Recreate the state of a creator. It is typically used when things outside | |
| /// the graph changes. For example, click to retry after a network error. | |
| /// If you use this method in a creative way, let us know. | |
| void recreate<T>(CreatorBase<T> creator) { | |
| _element<T>(creator, recreate: false).recreate(); | |
| } | |
| /// Try delete the creator from the graph. No-op for creators with watchers. | |
| /// It can delete creator even if it has keepAlive set. | |
| void dispose(CreatorBase creator) { | |
| if (_graph.from(creator).isNotEmpty) { | |
| return; | |
| } | |
| _delete(_graph.delete(creator)); | |
| _delete([creator]); // In case it has no dependency and no watcher. | |
| } | |
| /// Delete all creators. Ref will become empty after this call. | |
| void disposeAll() { | |
| for (final creator in _elements.keys.toSet()) { | |
| _delete(_graph.delete(creator)); | |
| _delete([creator]); | |
| } | |
| } | |
| /// Provide a callback which the framework will call before next create call | |
| /// and when the creator is disposed. | |
| void onClean(void Function() onClean) { | |
| assert(_owner != null, 'onClean is called outside of create method'); | |
| _clean[_owner!] = onClean; | |
| } | |
| /// Delete creators which are already deleted from [_graph]. | |
| void _delete(List<CreatorBase> creators) { | |
| for (final creator in creators) { | |
| _clean.remove(creator)?.call(); | |
| _elements.remove(creator); | |
| _before.remove(creator); | |
| _after.remove(creator); | |
| _toCreate.remove(creator); | |
| } | |
| } | |
| /// Creator will notify Ref that they want to start creation. See [_before]. | |
| /// Returns true if the creation is allowed. | |
| bool _onCreateStart<T>() { | |
| if (_before.containsKey(_owner!)) { | |
| _toCreate.add(_owner!); | |
| return false; | |
| } else { | |
| _before[_owner!] = Set.from(_graph.to(_owner!)); | |
| _after[_owner!] = {}; | |
| _clean.remove(_owner!)?.call(); | |
| return true; | |
| } | |
| } | |
| /// Creator will notify Ref that they finished creation. See [_before]. | |
| void _onCreateFinish<T>() { | |
| // Delete dependencies which are not needed any more. | |
| for (final dep in _before[_owner!] ?? {}) { | |
| if (!(_after[_owner!]?.contains(dep) ?? false)) { | |
| _delete(_graph.deleteEdge(dep, _owner!)); | |
| } | |
| } | |
| // Start queued work if any. | |
| _before.remove(_owner!); | |
| _after.remove(_owner!); | |
| if (_toCreate.contains(_owner!)) { | |
| _toCreate.remove(_owner!); | |
| _element(_owner!).recreate(); | |
| } | |
| } | |
| /// Creator will notify Ref that their state has changed. Ref will simply | |
| /// recreate its watchers' state. Its watcher might further call this function | |
| /// synchronously or asynchronously. The state change is propagated as far as | |
| /// we can. | |
| void _onStateChange<T>(CreatorBase creator, T? before, T after) { | |
| _observer?.onStateChange(creator, before, after); | |
| _notifyWatcher(creator); | |
| } | |
| /// Error was caught in user provided create function. | |
| void _onError<T>(CreatorBase creator, Object? error) { | |
| _observer?.onError(creator, error); | |
| _notifyWatcher(creator); | |
| } | |
| /// Propagate the state change. | |
| void _notifyWatcher<T>(CreatorBase creator) { | |
| for (final watcher in Set.from(_graph.from(creator))) { | |
| if (!_elements.containsKey(watcher)) { | |
| // This means watch is a Creator and its create function is called | |
| // in CreatorElement's constructor, and it is not finished yet. | |
| continue; | |
| } | |
| if (!_element(creator).created && !_element(watcher).created) { | |
| // This means [creator] is actually being newly created because | |
| // [watcher] watches it. There is no need to propagate, because | |
| // [watcher] can get [creator]'s state directly from Ref.watch's return. | |
| continue; | |
| } | |
| _element(watcher).recreate(); | |
| } | |
| } | |
| String graphString() => _graph.toString(); | |
| String graphDebugString() => _graph.toDebugString(); | |
| String elementsString() => | |
| '{${_elements.entries.map((e) => e.key.infoName).join(', ')}}'; | |
| } | |
| /// For testing only. | |
| class RefForTest extends Ref { | |
| RefForTest() : super._(); | |
| Map<CreatorBase, ElementBase> get elements => _elements; | |
| Graph<CreatorBase> get graph => _graph; | |
| Map<CreatorBase, Set<CreatorBase>> get before => _before; | |
| Map<CreatorBase, Set<CreatorBase>> get after => _after; | |
| Set<CreatorBase> get toCreate => _toCreate; | |
| Map<CreatorBase, void Function()> get clean => _clean; | |
| } | |
| /// For testing only. | |
| class RefForLifeCycleTest extends Ref { | |
| RefForLifeCycleTest(CreatorBase owner) : super._(owner: owner); | |
| @override | |
| bool _onCreateStart<T>() => true; | |
| @override | |
| void _onCreateFinish<T>() {} | |
| @override | |
| void _onStateChange<T>(CreatorBase creator, T? before, T after) {} | |
| } | |
| // This file is just to make creator group works. It is not super interesting. | |
| // Put it here instead of inside Creator/Emitter classes for readability. | |
| class _CreatorGroup1<T, A1> { | |
| const _CreatorGroup1(this.create, this.name, this.keepAlive); | |
| final T Function(Ref, A1 arg1) create; | |
| final String? name; | |
| final bool keepAlive; | |
| Creator<T> call(A1 arg1) => Creator<T>((ref) => create(ref, arg1), | |
| name: name, keepAlive: keepAlive, args: [this, arg1]); | |
| } | |
| class _CreatorWithArg1 { | |
| const _CreatorWithArg1(); | |
| _CreatorGroup1<T, A1> call<T, A1>(T Function(Ref, A1 arg1) create, | |
| {String? name, bool keepAlive = false}) { | |
| return _CreatorGroup1<T, A1>(create, name, keepAlive); | |
| } | |
| } | |
| class _CreatorGroup2<T, A1, A2> { | |
| const _CreatorGroup2(this.create, this.name, this.keepAlive); | |
| final T Function(Ref, A1 arg1, A2 arg2) create; | |
| final String? name; | |
| final bool keepAlive; | |
| Creator<T> call(A1 arg1, A2 arg2) => | |
| Creator<T>((ref) => create(ref, arg1, arg2), | |
| name: name, keepAlive: keepAlive, args: [this, arg1, arg2]); | |
| } | |
| class _CreatorWithArg2 { | |
| const _CreatorWithArg2(); | |
| _CreatorGroup2<T, A1, A2> call<T, A1, A2>( | |
| T Function(Ref, A1 arg1, A2 arg2) create, | |
| {String? name, | |
| bool keepAlive = false}) { | |
| return _CreatorGroup2<T, A1, A2>(create, name, keepAlive); | |
| } | |
| } | |
| class _CreatorGroup3<T, A1, A2, A3> { | |
| const _CreatorGroup3(this.create, this.name, this.keepAlive); | |
| final T Function(Ref, A1 arg1, A2 arg2, A3 arg3) create; | |
| final String? name; | |
| final bool keepAlive; | |
| Creator<T> call(A1 arg1, A2 arg2, A3 arg3) => | |
| Creator<T>((ref) => create(ref, arg1, arg2, arg3), | |
| name: name, keepAlive: keepAlive, args: [this, arg1, arg2, arg3]); | |
| } | |
| class _CreatorWithArg3 { | |
| const _CreatorWithArg3(); | |
| _CreatorGroup3<T, A1, A2, A3> call<T, A1, A2, A3>( | |
| T Function(Ref, A1 arg1, A2 arg2, A3 arg3) create, | |
| {String? name, | |
| bool keepAlive = false}) { | |
| return _CreatorGroup3<T, A1, A2, A3>(create, name, keepAlive); | |
| } | |
| } | |
| class _EmitterGroup1<T, A1> { | |
| const _EmitterGroup1(this.create, this.name, this.keepAlive); | |
| final FutureOr<void> Function(Ref ref, A1 arg1, void Function(T) emit) create; | |
| final String? name; | |
| final bool keepAlive; | |
| Emitter<T> call(A1 arg1) => Emitter<T>((ref, emit) => create(ref, arg1, emit), | |
| name: name, keepAlive: keepAlive, args: [this, arg1]); | |
| } | |
| class _EmitterWithArg1 { | |
| const _EmitterWithArg1(); | |
| _EmitterGroup1<T, A1> call<T, A1>( | |
| FutureOr<void> Function(Ref ref, A1 arg1, void Function(T) emit) create, | |
| {String? name, | |
| bool keepAlive = false}) { | |
| return _EmitterGroup1<T, A1>(create, name, keepAlive); | |
| } | |
| } | |
| class _EmitterGroup2<T, A1, A2> { | |
| const _EmitterGroup2(this.create, this.name, this.keepAlive); | |
| final FutureOr<void> Function( | |
| Ref ref, A1 arg1, A2 arg2, void Function(T) emit) create; | |
| final String? name; | |
| final bool keepAlive; | |
| Emitter<T> call(A1 arg1, A2 arg2) => | |
| Emitter<T>((ref, emit) => create(ref, arg1, arg2, emit), | |
| name: name, keepAlive: keepAlive, args: [this, arg1, arg2]); | |
| } | |
| class _EmitterWithArg2 { | |
| const _EmitterWithArg2(); | |
| _EmitterGroup2<T, A1, A2> call<T, A1, A2>( | |
| FutureOr<void> Function(Ref ref, A1 arg1, A2 arg2, void Function(T) emit) | |
| create, | |
| {String? name, | |
| bool keepAlive = false}) { | |
| return _EmitterGroup2<T, A1, A2>(create, name, keepAlive); | |
| } | |
| } | |
| class _EmitterGroup3<T, A1, A2, A3> { | |
| const _EmitterGroup3(this.create, this.name, this.keepAlive); | |
| final FutureOr<void> Function( | |
| Ref ref, A1 arg1, A2 arg2, A3 arg3, void Function(T) emit) create; | |
| final String? name; | |
| final bool keepAlive; | |
| Emitter<T> call(A1 arg1, A2 arg2, A3 arg3) => | |
| Emitter<T>((ref, emit) => create(ref, arg1, arg2, arg3, emit), | |
| name: name, keepAlive: keepAlive, args: [this, arg1, arg2, arg3]); | |
| } | |
| class _EmitterWithArg3 { | |
| const _EmitterWithArg3(); | |
| _EmitterGroup3<T, A1, A2, A3> call<T, A1, A2, A3>( | |
| FutureOr<void> Function( | |
| Ref ref, A1 arg1, A2 arg2, A3 arg3, void Function(T) emit) | |
| create, | |
| {String? name, | |
| bool keepAlive = false}) { | |
| return _EmitterGroup3<T, A1, A2, A3>(create, name, keepAlive); | |
| } | |
| } | |
| /// Observer can listen to creator state changes. | |
| class CreatorObserver { | |
| const CreatorObserver(); | |
| void onStateChange(CreatorBase creator, Object? before, Object? after) {} | |
| void onError(CreatorBase creator, Object? error) {} | |
| } | |
| /// Graph is a simple implementation of a bi-directed graph using adjacency | |
| /// list. It can automatically delete nodes which become zero out-degree. | |
| class Graph<T> { | |
| Graph({this.name, this.keepAlive}); | |
| /// Get name from the node. | |
| final String Function(T)? name; | |
| String _name(T node) => name != null ? name!(node) : node.toString(); | |
| /// Get keep alive property from the node. | |
| final bool Function(T)? keepAlive; | |
| bool _keepAlive(T node) => keepAlive != null ? keepAlive!(node) : false; | |
| /// Graph edges in normal direction. A : {B, C} means A -> B, A -> C. | |
| final Map<T, Set<T>> _out = {}; | |
| /// Graph edges in reversed direction. D : {A, B} means A -> D, B -> D. | |
| final Map<T, Set<T>> _in = {}; | |
| /// Get all nodes from a node. | |
| Set<T> from(T node) => _out[node] ?? {}; | |
| /// Get all nodes to a node. | |
| Set<T> to(T node) => _in[node] ?? {}; | |
| /// Return whether a node is in the graph. | |
| bool contains(T node) => _out.containsKey(node) || _in.containsKey(node); | |
| /// Get all nodes. Used for testing only. | |
| Set<T> get nodes => {..._out.keys, ..._in.keys}; | |
| /// Add an edge. | |
| void addEdge(T from, T to) { | |
| _out[from] ??= {}; | |
| _out[from]!.add(to); | |
| _in[to] ??= {}; | |
| _in[to]!.add(from); | |
| } | |
| /// Remove an edge and non-keep-alive nodes which become zero out-degree. | |
| /// Return deleted nodes. | |
| List<T> deleteEdge(T from, T to) { | |
| _out[from]?.remove(to); | |
| _in[to]?.remove(from); | |
| return _keepAlive(from) || this.from(from).isNotEmpty ? [] : delete(from); | |
| } | |
| /// Delete a node and other non-keep-alive nodes which become zero out-degree, | |
| /// using BFS. Return deleted nodes. | |
| List<T> delete(T node) { | |
| if (!contains(node)) { | |
| return []; | |
| } | |
| var toCheck = [node]; | |
| final deleted = <T>[]; | |
| while (toCheck.isNotEmpty) { | |
| final n = toCheck.first; | |
| toCheck = toCheck.sublist(1); | |
| if (n == node || (!_keepAlive(n) && from(n).isEmpty)) { | |
| toCheck.addAll(_in[n] ?? []); | |
| _delete(n); | |
| deleted.add(n); | |
| } | |
| } | |
| return deleted; | |
| } | |
| /// Perform the actual delete. | |
| void _delete(T node) { | |
| _out[node]?.forEach((n) => _in[n]!.remove(node)); | |
| _out.remove(node); | |
| _in[node]?.forEach((n) => _out[n]!.remove(node)); | |
| _in.remove(node); | |
| } | |
| @override | |
| String toString() { | |
| return _out.entries | |
| .map((e) => '- ${_name(e.key)}: {${e.value.map(_name).join(', ')}}') | |
| .join('\n'); | |
| } | |
| String toDebugString() { | |
| return [ | |
| '- Out:', | |
| ..._out.entries.map( | |
| (e) => ' - ${_name(e.key)}: {${e.value.map(_name).join(', ')}}'), | |
| '- In:', | |
| ..._in.entries.map( | |
| (e) => ' - ${_name(e.key)}: {${e.value.map(_name).join(', ')}}'), | |
| ].join('\n'); | |
| } | |
| } | |
| /// CreatorGraph holds a Ref, which holds the graph internally. Flutter app | |
| /// should be wrapped with this. By default it uses [DefaultCreatorObserver] to | |
| /// log state changes. | |
| class CreatorGraph extends StatefulWidget { | |
| CreatorGraph({ | |
| Key? key, | |
| CreatorObserver observer = const DefaultCreatorObserver(), | |
| required this.child, | |
| }) : ref = Ref(observer: observer), | |
| super(key: key); | |
| final Ref ref; | |
| final Widget child; | |
| @override | |
| State<CreatorGraph> createState() => _CreatorGraph(); | |
| } | |
| class _CreatorGraph extends State<CreatorGraph> { | |
| @override | |
| void dispose() { | |
| widget.ref.disposeAll(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return CreatorGraphData(ref: widget.ref, child: widget.child); | |
| } | |
| } | |
| /// The inherit widget for CreatorGraph. | |
| class CreatorGraphData extends InheritedWidget { | |
| const CreatorGraphData({ | |
| Key? key, | |
| required this.ref, | |
| required Widget child, | |
| }) : super(key: key, child: child); | |
| final Ref ref; | |
| static CreatorGraphData of(BuildContext context) { | |
| final CreatorGraphData? result = | |
| context.dependOnInheritedWidgetOfExactType<CreatorGraphData>(); | |
| return result!; | |
| } | |
| @override | |
| bool updateShouldNotify(CreatorGraphData oldWidget) => ref != oldWidget.ref; | |
| } | |
| extension ContextRef on BuildContext { | |
| Ref get ref => CreatorGraphData.of(this).ref; | |
| } | |
| /// Default observer which just log the new states. | |
| class DefaultCreatorObserver extends CreatorObserver { | |
| const DefaultCreatorObserver(); | |
| @override | |
| void onStateChange(CreatorBase creator, Object? before, Object? after) { | |
| if (ignore(creator)) { | |
| return; | |
| } | |
| debugPrint('[Creator] ${creator.infoName}: $after'); | |
| } | |
| @override | |
| void onError(CreatorBase creator, Object? error) { | |
| if (ignore(creator)) { | |
| return; | |
| } | |
| debugPrint('[Creator][Error] ${creator.infoName}: $error'); | |
| } | |
| /// Ignore a few derived creators to reduce log amount. | |
| bool ignore(CreatorBase creator) { | |
| return (creator.name == 'watcher' || | |
| creator.name == 'listener' || | |
| (creator.name?.endsWith('_asyncData') ?? false) || | |
| (creator.name?.endsWith('_change') ?? false)); | |
| } | |
| } | |
| /// Watch creators to build a widget or to perform other action. | |
| class Watcher extends StatefulWidget { | |
| const Watcher(this.builder, | |
| {Key? key, | |
| this.listener, | |
| this.builderName, | |
| this.listenerName, | |
| this.child}) | |
| : assert(!(builder == null && child == null), | |
| 'builder and child cannot both be null'), | |
| super(key: key); | |
| /// Allows watching creators to populate a widget. | |
| final Widget Function(BuildContext context, Ref ref, Widget? child)? builder; | |
| /// Allows watching creators to perform action. It is independent to builder. | |
| final void Function(Ref ref)? listener; | |
| /// Optional name for the builder creator. | |
| final String? builderName; | |
| /// Optional name for the listener creator. | |
| final String? listenerName; | |
| /// If set, it is passed into builder. Can be used if the subtree should not | |
| /// rebuild when dependency state changes. | |
| final Widget? child; | |
| @override | |
| State<Watcher> createState() => _WatcherState(); | |
| } | |
| class _WatcherState extends State<Watcher> { | |
| Creator<Widget>? builder; | |
| Creator<void>? listener; | |
| late Ref ref; | |
| /// Whether build() is in progress. This allows builder to differentiate | |
| /// create call triggered by Flutter vs triggered by dependency state change. | |
| bool building = false; | |
| /// Set when dependency changes. Cleared by next build call. This avoids | |
| /// calling builder function twice. | |
| bool dependencyChanged = false; | |
| void _setup() { | |
| if (widget.builder != null) { | |
| builder = Creator((ref) { | |
| if (!building) { | |
| // Dependency state changes. Set dependencyChanged so that next time | |
| // build is called, we skip recreating the creator. This way the | |
| // builder function is only called once. | |
| setState(() { | |
| dependencyChanged = true; | |
| }); | |
| } | |
| return widget.builder!(context, ref, widget.child); | |
| }, name: widget.builderName ?? 'watcher'); | |
| // Not watch(builder) here, use recreate(builder) later. | |
| } | |
| if (widget.listener != null) { | |
| listener = | |
| Creator(widget.listener!, name: widget.listenerName ?? 'listener'); | |
| ref.watch(listener!); | |
| } | |
| } | |
| void _dispose() { | |
| if (builder != null) { | |
| ref.dispose(builder!); | |
| } | |
| if (listener != null) { | |
| ref.dispose(listener!); | |
| } | |
| } | |
| @override | |
| void didChangeDependencies() { | |
| super.didChangeDependencies(); | |
| ref = CreatorGraphData.of(context).ref; // Save ref to use in dispose. | |
| _setup(); | |
| } | |
| @override | |
| void didUpdateWidget(covariant Watcher oldWidget) { | |
| super.didUpdateWidget(oldWidget); | |
| _dispose(); | |
| _setup(); | |
| } | |
| @override | |
| void dispose() { | |
| _dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| if (builder == null) { | |
| return widget.child!; | |
| } | |
| building = true; | |
| // Recreate will add builder to the graph. Recreate its state if the build | |
| // call is triggered by Flutter rather than dependency change. | |
| if (!dependencyChanged) { | |
| ref.recreate(builder!); | |
| } | |
| building = false; | |
| dependencyChanged = false; | |
| return ref.read(builder!); | |
| } | |
| @override | |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
| super.debugFillProperties(properties); | |
| // For now, we just dump the whole graph to DevTools. | |
| properties.add(StringProperty('graph', ref.graphString())); | |
| } | |
| } | |
| /// {@template extension} | |
| /// Higher level primitives similar to these methods in [Iterable] / [Stream]. | |
| /// These methods are simple, but hopefully can make code concise and fluid. | |
| /// | |
| /// If you use extensions on the fly, you should understand creator equality | |
| /// and set [CreatorBase.args] as needed. | |
| /// {@endtemplate} | |
| /// {@macro extension} | |
| extension CreatorExtension<T> on Creator<T> { | |
| Creator<F> map<F>(F Function(T) map, | |
| {String? name, bool keepAlive = false, List<Object?>? args}) { | |
| return Creator((ref) => map(ref.watch(this)), | |
| name: name ?? '${infoName}_map', keepAlive: keepAlive, args: args); | |
| } | |
| Emitter<F> mapAsync<F>(Future<F> Function(T) map, | |
| {String? name, bool keepAlive = false, List<Object?>? args}) { | |
| return Emitter((ref, emit) async => emit(await map(ref.watch(this))), | |
| name: name ?? '${infoName}_map', keepAlive: keepAlive, args: args); | |
| } | |
| Emitter<T> where(bool Function(T) test, | |
| {String? name, bool keepAlive = false, List<Object?>? args}) { | |
| return Emitter((ref, emit) { | |
| final value = ref.watch(this); | |
| if (test(value)) { | |
| emit(value); | |
| } | |
| }, name: name ?? '${infoName}_where', keepAlive: keepAlive, args: args); | |
| } | |
| } | |
| /// {@macro extension} | |
| extension EmitterExtension<T> on Emitter<T> { | |
| Emitter<F> map<F>(F Function(T) map, | |
| {String? name, bool keepAlive = false, List<Object?>? args}) { | |
| return Emitter( | |
| (ref, emit) => ref.watch(this).then((value) => emit(map(value))), | |
| name: name ?? '${infoName}_map', | |
| keepAlive: keepAlive, | |
| args: args); | |
| } | |
| Emitter<F> where<F>(bool Function(T) test, | |
| {String? name, bool keepAlive = false, List<Object?>? args}) { | |
| return Emitter((ref, emit) { | |
| return ref.watch(this).then((value) { | |
| if (test(value)) { | |
| emit(value as F); | |
| } | |
| }); | |
| }, name: name ?? '${infoName}_where', keepAlive: keepAlive, args: args); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment