Skip to content

Instantly share code, notes, and snippets.

@qronos-ai
Created August 15, 2022 13:41
Show Gist options
  • Select an option

  • Save qronos-ai/3a06ad834034ff74cebf38fd4866fa7d to your computer and use it in GitHub Desktop.

Select an option

Save qronos-ai/3a06ad834034ff74cebf38fd4866fa7d to your computer and use it in GitHub Desktop.
[Creator Example] Counter

[Creator Example] Counter

Created with <3 with dartpad.dev.

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