If you have not already seen https://gist.github.com/jogboms/7bc0318e9b33a4b4817306c5333a6cf3 then do have a look before this as this is only an extension to support the Flutter framework.
You can see this in action via this DartPad
If you have not already seen https://gist.github.com/jogboms/7bc0318e9b33a4b4817306c5333a6cf3 then do have a look before this as this is only an extension to support the Flutter framework.
You can see this in action via this DartPad
import 'dart:async'; | |
import 'dart:math'; | |
import 'package:flutter/material.dart'; | |
void main() { | |
runApp( | |
const PodsScope( | |
child: MainApp(), | |
), | |
); | |
} | |
/* Start Example */ | |
final StatePod<int> counterPod = StatePod((_) => 0); | |
final FuturePod<int> randomIntPod = FuturePod( | |
(Ref ref) { | |
int value = Random().nextInt(1000); | |
ref.onDispose(() { | |
debugPrint(('randomInt-disposed', value).toString()); | |
}); | |
return Future<int>.delayed( | |
const Duration(seconds: 2), | |
() => value, | |
); | |
}, | |
); | |
final ValueMapper<int, FuturePod<int>> dependOnRandomIntPod = FuturePod.family( | |
(Ref ref, int multiplier) async { | |
final int value = await ref.watch(randomIntPod.future); | |
return value * multiplier; | |
}, | |
); | |
final StreamPod<int> timerPod = StreamPod( | |
(Ref ref) async* { | |
// This should cause timer to restart everytime count changes | |
final int count = max(1, ref.watch(counterPod)); | |
for (int i = count; i >= 0; i--) { | |
await Future<void>.delayed(const Duration(seconds: 1)); | |
yield i; | |
} | |
}, | |
); | |
final FuturePod<bool> moduloPod = FuturePod( | |
(Ref ref) => ref.watch( | |
timerPod.selectAsync((int value) => value % 5 == 0), | |
), | |
); | |
class MainApp extends StatelessWidget { | |
const MainApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
context.listen(counterPod, (int? previous, int next) { | |
debugPrint(('counter-changed', previous, next).toString()); | |
}); | |
context.listen(timerPod, (AsyncValue<int>? previous, AsyncValue<int> next) { | |
debugPrint(('timer-changed', previous, next).toString()); | |
}); | |
context.listen(moduloPod, (AsyncValue<bool>? previous, AsyncValue<bool> next) { | |
debugPrint(('modulo-changed', previous, next).toString()); | |
}); | |
return MaterialApp( | |
home: Scaffold( | |
appBar: AppBar(title: const Text('Pods')), | |
body: Center( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
// Wrapped in a Builder so 'watch' doesn't propagate upwards | |
Builder( | |
builder: (BuildContext context) => Column( | |
children: <Widget>[ | |
switch (context.watch(randomIntPod)) { | |
PendingAsyncValue() => const Text('Loading...'), | |
SuccessfulAsyncValue(:final int data) => Text('Random => $data'), | |
FailedAsyncValue() => const Text('Failed!'), | |
}, | |
const SizedBox(height: 16), | |
switch (context.watch(dependOnRandomIntPod(10))) { | |
PendingAsyncValue() => const Text('Loading...'), | |
SuccessfulAsyncValue(:final int data) => Text('Depends on Random => $data'), | |
FailedAsyncValue() => const Text('Failed!'), | |
}, | |
const SizedBox(height: 16), | |
TextButton( | |
onPressed: () => context.invalidate(randomIntPod), | |
child: const Text('Invalidate Random'), | |
), | |
], | |
), | |
), | |
const SizedBox(height: 24), | |
const Divider(), | |
const SizedBox(height: 24), | |
// Wrapped in a Builder so 'watch' doesn't propagate upwards | |
Builder( | |
builder: (BuildContext context) => Column( | |
children: <Widget>[ | |
switch (context.watch(timerPod)) { | |
PendingAsyncValue() => const Text('Loading...'), | |
SuccessfulAsyncValue(:final int data) => Text('Countdown => $data'), | |
FailedAsyncValue() => const Text('Failed!'), | |
}, | |
const SizedBox(height: 16), | |
switch (context.watch(moduloPod)) { | |
PendingAsyncValue() => const Text('Loading...'), | |
SuccessfulAsyncValue(:final bool data) => Text('Modulo => $data'), | |
FailedAsyncValue() => const Text('Failed!'), | |
}, | |
], | |
), | |
), | |
const SizedBox(height: 24), | |
const _Counter(), | |
const SizedBox(height: 24), | |
const PodsScope( | |
overrides: <PodOverride>[ | |
// An override for `counterPod` can also be introduced here | |
// counterPod.overrideWith((_) => 10), | |
], | |
child: PodsScope( | |
overrides: <PodOverride>[ | |
// An override for `counterPod` can also be introduced here | |
// counterPod.overrideWith((_) => 100), | |
], | |
child: PodsScope( | |
overrides: <PodOverride>[ | |
// An override for `counterPod` can also be introduced here | |
// counterPod.overrideWith((_) => 1000), | |
], | |
child: _Counter(), | |
), | |
), | |
), | |
], | |
), | |
), | |
), | |
); | |
} | |
} | |
class _Counter extends StatelessWidget { | |
const _Counter(); | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Row( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
TextButton( | |
onPressed: () => context.read(counterPod.notifier).state--, | |
child: const Text('-'), | |
), | |
Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 16.0), | |
child: Row( | |
children: <Widget>[ | |
// Wrapped in a Builder so 'watch' doesn't propagate upwards | |
Builder( | |
builder: (BuildContext context) => Text( | |
'${context.watch(counterPod)}', | |
), | |
), | |
const Text(' % 5 == 0 ? '), | |
// Wrapped in a Builder so 'watch' doesn't propagate upwards. Also it uses a selector | |
Builder( | |
builder: (BuildContext context) => Text( | |
'${context.watch(counterPod.select((int counter) => counter % 5 == 0))}', | |
), | |
), | |
], | |
), | |
), | |
TextButton( | |
onPressed: () => context.read(counterPod.notifier).state++, | |
child: const Text('+'), | |
), | |
], | |
), | |
const SizedBox(height: 4), | |
TextButton( | |
// This should reset its state to default, rebuild all dependent pods + widgets & notify all listeners | |
onPressed: () => context.invalidate(counterPod), | |
child: const Text('Invalidate Counter'), | |
), | |
], | |
); | |
} | |
} | |
/* End Example */ | |
/* Start PodsScope */ | |
class PodsScope extends StatefulWidget { | |
const PodsScope({ | |
super.key, | |
this.overrides = const <PodOverride>[], | |
required this.child, | |
}); | |
final List<PodOverride> overrides; | |
final Widget child; | |
static _PodsScope _of(BuildContext context, {bool listen = false}) { | |
final _PodsScope? scope = listen | |
? context.getInheritedWidgetOfExactType<_PodsScope>() | |
: context.dependOnInheritedWidgetOfExactType<_PodsScope>(); | |
if (scope == null) { | |
throw Exception('Missing PodsScope'); | |
} | |
return scope; | |
} | |
@override | |
State<PodsScope> createState() => _PodsScopeState(); | |
} | |
typedef _PodScopeSubscriptionKey = (Pod, BuildContext); | |
typedef _PodScopeSubscriptions = Map<_PodScopeSubscriptionKey, PodSubscription>; | |
class _PodsScopeState extends State<PodsScope> { | |
final _PodScopeSubscriptions _dependents = <_PodScopeSubscriptionKey, PodSubscription>{}; | |
final _PodScopeSubscriptions _listeners = <_PodScopeSubscriptionKey, PodSubscription>{}; | |
late final Pods _pods = Pods(parent: _parent, overrides: widget.overrides); | |
Pods? get _parent => context.getInheritedWidgetOfExactType<_PodsScope>()?.state._pods; | |
@override | |
void didUpdateWidget(covariant PodsScope oldWidget) { | |
assert(widget.overrides.length == oldWidget.overrides.length, "You shouldn't add or remove overrides"); | |
_pods.updateOverrides(widget.overrides, _parent); | |
super.didUpdateWidget(oldWidget); | |
} | |
@override | |
void dispose() { | |
_dependents | |
..forEach((_, PodSubscription sub) => sub.close()) | |
..clear(); | |
_listeners | |
..forEach((_, PodSubscription sub) => sub.close()) | |
..clear(); | |
_pods.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) => _PodsScope(state: this, child: widget.child); | |
} | |
class _PodsScope extends InheritedWidget { | |
const _PodsScope({required this.state, required super.child}); | |
final _PodsScopeState state; | |
@override | |
bool updateShouldNotify(covariant _PodsScope oldWidget) => false; | |
} | |
/* End PodsScope */ | |
/* Start PodsProvider extension */ | |
extension PodsProvider on BuildContext { | |
_PodsScopeState get _state => PodsScope._of(this).state; | |
U read<U>(Pod<U> pod) => _state._pods.read(pod); | |
U watch<U>(NotifiablePod<U> pod) { | |
final _PodScopeSubscriptionKey key = (pod, this); | |
final _PodsScopeState state = PodsScope._of(this, listen: true).state; | |
if (state._dependents[key] case final PodSubscription subscription) { | |
return subscription.read() as U; | |
} | |
return state._dependents | |
.putIfAbsent( | |
key, | |
() => state._pods.listen<U>(pod, (_, __) { | |
if (this case final Element element when element.debugIsActive) { | |
element.markNeedsBuild(); | |
} else { | |
scheduleMicrotask(() => state._dependents | |
..[key]!.close() | |
..remove(key)); | |
} | |
}), | |
) | |
.read() as U; | |
} | |
PodSubscription<U> listen<U>( | |
ListenablePod<U> pod, | |
PodListener<U> listener, { | |
bool fireImmediately = false, | |
}) { | |
final _PodScopeSubscriptionKey key = (pod, this); | |
final _PodScopeSubscriptions listeners = _PodScopeSubscriptions.of(_state._listeners); | |
try { | |
listeners.forEach((_PodScopeSubscriptionKey subKey, PodSubscription subscription) { | |
if (subKey == key) { | |
subscription.close(); | |
_state._listeners.remove(key); | |
} | |
}); | |
return _state._listeners.putIfAbsent( | |
key, | |
() => _state._pods.listen<U>(pod, listener), | |
) as PodSubscription<U>; | |
} finally { | |
listeners.clear(); | |
} | |
} | |
void invalidate<U>(Pod<U> pod) => _state._pods.invalidate(pod); | |
} | |
/* End PodsProvider extension */ | |
/* Start Pods */ | |
typedef VoidCallback = void Function(); | |
typedef ValueCallback<T> = T Function(); | |
typedef ValueMapper<U, T> = T Function(U); | |
@optionalTypeArgs | |
typedef PodOverride<T> = ({Pod<T> origin, Pod<T> override}); | |
@optionalTypeArgs | |
typedef PodSubscription<U> = ({ValueCallback<U> read, VoidCallback close}); | |
typedef PodListener<U> = void Function(U? previous, U next); | |
typedef PodFactory<T> = ValueMapper<Ref, T>; | |
typedef PodUpdater<T> = ValueMapper<T?, T>; | |
typedef FamilyPodFactory<T, U> = T Function(Ref ref, U args); | |
abstract interface class Ref { | |
U read<U>(Pod<U> pod); | |
U watch<U>(WatchablePod<U> pod); | |
PodSubscription<U> listen<U>( | |
ListenablePod<U> pod, | |
PodListener<U> listener, { | |
bool fireImmediately = false, | |
}); | |
void invalidate<U>(Pod<U> pod); | |
void onDispose(VoidCallback callback); | |
} | |
/* End Pods */ | |
/* Start Pod Element */ | |
@optionalTypeArgs | |
abstract base class PodElement<T> { | |
PodElement([this._state]); | |
String? _debugLabel; | |
late Ref _ref; | |
final Set<PodListener<T>> _listeners = <PodListener<T>>{}; | |
final Set<PodElement> _references = <PodElement>{}; | |
final Set<PodElement> _dependents = <PodElement>{}; | |
final Set<VoidCallback> _disposeListener = <VoidCallback>{}; | |
@protected | |
T get state => _state!; | |
T? _state; | |
@protected | |
set state(T newState) { | |
final T? oldState = _state; | |
_state = newState; | |
_notifyListeners(oldState); | |
} | |
@protected | |
bool get mounted => _state != null; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || other is PodElement<T> && runtimeType == other.runtimeType; | |
@override | |
int get hashCode => identityHashCode(this); | |
@mustCallSuper | |
void mount(covariant Ref ref) { | |
assert(state != null, 'Set the state after mount'); | |
_ref = ref; | |
} | |
@mustCallSuper | |
void dispose() { | |
_notifyDisposeCallbacks(); | |
_state = null; | |
_debugLabel = null; | |
_listeners.clear(); | |
_dependents.clear(); | |
_references.clear(); | |
} | |
@override | |
String toString() => (StringBuffer() | |
..write(_debugLabel != null ? '${_debugLabel}Element' : '$runtimeType') | |
..write('($hashCode)')) | |
.toString(); | |
// todo: maybe do not mount immediately | |
// todo: maybe introduce a scheduler | |
void _invalidate() { | |
_notifyDisposeCallbacks(); | |
Future<void>(() { | |
if (_ref case final Ref ref) { | |
final T? oldState = _state; | |
mount(ref); | |
_notifyListeners(oldState); | |
} | |
}); | |
} | |
void _attachTo(PodElement element) => element._references.add(this); | |
void _dependsOn(PodElement element) => element._dependents.add(this); | |
VoidCallback _addListener(PodListener<T> listener) { | |
_listeners.add(listener); | |
return () => _listeners.remove(listener); | |
} | |
void _addDisposeListener(VoidCallback callback) => _disposeListener.add(callback); | |
@protected | |
void _notifyListeners(T? oldState) { | |
if (oldState == state) { | |
return; | |
} | |
for (int i = 0; i < _listeners.length; i++) { | |
_listeners.elementAt(i)(oldState, state); | |
} | |
for (int i = _dependents.length - 1; i >= 0; i--) { | |
final PodElement dependent = _dependents.elementAt(i); | |
dependent.mount(dependent._ref); | |
} | |
} | |
void _notifyDisposeCallbacks() { | |
for (final VoidCallback callback in _disposeListener) { | |
callback(); | |
} | |
_disposeListener.clear(); | |
} | |
} | |
/* End Pod Element */ | |
/* Start Pod */ | |
@optionalTypeArgs | |
abstract base class Pod<T> { | |
const Pod({this.debugLabel}); | |
final String? debugLabel; | |
static ValueMapper<U, V> _family<U, V>(ValueMapper<U, V> factory) => (U args) => factory(args); | |
@factory | |
PodElement<T> createElement(); | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || other is Pod<T> && runtimeType == other.runtimeType && debugLabel == other.debugLabel; | |
@override | |
int get hashCode => debugLabel?.hashCode ?? identityHashCode(this); | |
@override | |
String toString() => '${debugLabel ?? runtimeType}($hashCode)'; | |
} | |
@optionalTypeArgs | |
base mixin ListenablePod<T> on Pod<T> {} | |
@optionalTypeArgs | |
base mixin WatchablePod<T> on Pod<T> {} | |
@optionalTypeArgs | |
base mixin NotifiablePod<T> implements WatchablePod<T>, ListenablePod<T> {} | |
@optionalTypeArgs | |
base mixin AsyncPod<T, U> implements NotifiablePod<AsyncValue<T>> { | |
PodFactory<U> get _factory; | |
WatchablePod<FutureOr<T>> get future; | |
} | |
/* End Pod */ | |
/* Start Pods Container */ | |
class Pods implements Ref { | |
Pods({ | |
Pods? parent, | |
PodElement? owner, | |
List<PodOverride> overrides = const <PodOverride>[], | |
}) : this._( | |
owner: owner ?? (_AnonymousPodElement().._debugLabel = 'RootElement'), | |
pods: <Pod, PodElement>{ | |
...?parent?._pods, | |
}, | |
overrides: <Pod, Pod>{ | |
...?parent?._overrides, | |
for (final PodOverride entry in overrides) entry.origin: entry.override, | |
}, | |
); | |
const Pods._({ | |
required PodElement owner, | |
required Map<Pod, PodElement> pods, | |
required Map<Pod, Pod> overrides, | |
}) : _owner = owner, | |
_pods = pods, | |
_overrides = overrides; | |
final PodElement _owner; | |
final Map<Pod, PodElement> _pods; | |
final Map<Pod, Pod> _overrides; | |
@override | |
U read<U>(Pod<U> pod) => _resolvePodElement(pod).state; | |
@Deprecated('.watch is not supported in CLI mode, instead use .listen') | |
@override | |
U watch<U>(WatchablePod<U> pod) { | |
final PodElement<U> element = _resolvePodElement(pod); | |
_owner._dependsOn(element); | |
return element.state; | |
} | |
@override | |
PodSubscription<U> listen<U>( | |
ListenablePod<U> pod, | |
PodListener<U> listener, { | |
bool fireImmediately = false, | |
}) { | |
final PodElement<U> element = _resolvePodElement(pod, shouldMount: false); | |
final VoidCallback dispose = element._addListener(listener); | |
if (!element.mounted) { | |
_mountPodElement(element); | |
} | |
if (fireImmediately) { | |
listener(null, element.state); | |
} | |
return ( | |
read: () => element.state, | |
close: dispose, | |
); | |
} | |
@override | |
void invalidate<U>(Pod<U> pod) { | |
return switch ((_pods[pod], _overrides[pod])) { | |
(_, Pod<U> podOverride) => invalidate(podOverride), | |
(PodElement<U> element, _) => element._invalidate(), | |
_ => null, | |
}; | |
} | |
@Deprecated('not supposed to use this directly') | |
@override | |
void onDispose(VoidCallback callback) => _owner._addDisposeListener(callback); | |
void updateOverrides(List<PodOverride> overrides, Pods? parent) { | |
Map<Pod, Pod>? parentOverrides = parent?._overrides; | |
final Set<PodOverride> combinedOverrides = <PodOverride>{ | |
if (parentOverrides != null) | |
for (final MapEntry<Pod, Pod> entry in parentOverrides.entries) (origin: entry.key, override: entry.value), | |
...overrides, | |
}; | |
for (final PodOverride entry in combinedOverrides) { | |
final Pod? previousOverride = _overrides[entry.origin]; | |
_pods | |
..[previousOverride]?.dispose() | |
..remove(previousOverride); | |
_overrides[entry.origin] = entry.override; | |
} | |
} | |
void dispose() { | |
Future<void>(() { | |
_pods | |
..forEach((_, PodElement pod) => pod.dispose()) | |
..clear(); | |
_overrides.clear(); | |
_owner.dispose(); | |
}); | |
} | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
other is Pods && | |
runtimeType == other.runtimeType && | |
_owner == other._owner && | |
_pods == other._pods && | |
_overrides == other._overrides; | |
@override | |
int get hashCode => _owner.hashCode ^ _pods.hashCode ^ _overrides.hashCode; | |
@override | |
String toString() => 'Ref($hashCode)'; | |
@visibleForTesting | |
PodElement<U> element<U>(Pod<U> pod) => _resolvePodElement(pod); | |
@visibleForTesting | |
Future<void> pump() => Future<void>.microtask(() {}); | |
PodElement<U> _resolvePodElement<U>(Pod<U> pod, {bool shouldMount = true}) { | |
switch ((_pods[pod], _overrides[pod])) { | |
case (_, Pod<U> podOverride): | |
return _resolvePodElement(podOverride, shouldMount: shouldMount); | |
case (PodElement<U> element, _): | |
return element.._attachTo(_owner); | |
case _: | |
final PodElement<U> element = pod.createElement() | |
.._debugLabel = pod.debugLabel | |
.._attachTo(_owner); | |
_pods[pod] = element; | |
if (shouldMount) { | |
_mountPodElement(element); | |
} | |
return element; | |
} | |
} | |
void _mountPodElement(PodElement element) => element.mount( | |
Pods._( | |
owner: element, | |
pods: _pods, | |
overrides: _overrides, | |
), | |
); | |
} | |
/* End Pods Container */ | |
/* Start Pod Types */ | |
extension PodOverrideWith<T> on Pod<T> { | |
PodOverride<T> overrideWith(PodFactory<T> factory) => ( | |
origin: this, | |
override: _ProxyPod(factory, debugLabel: 'Override<$this>'), | |
); | |
} | |
extension PodSelector<T> on NotifiablePod<T> { | |
NotifiablePod<U> select<U>(ValueMapper<T, U> selector) => _SelectorPod( | |
(Ref ref) => selector(ref.watch(this)), | |
debugLabel: 'Selector<$this>', | |
); | |
} | |
extension AsyncPodSelector<T, V> on AsyncPod<T, V> { | |
NotifiablePod<Future<U>> selectAsync<U>(ValueMapper<T, U> selector) => _AsyncSelectorPod<T, U, V>( | |
_factory, | |
selector, | |
debugLabel: 'AsyncSelector<$this>', | |
); | |
} | |
@optionalTypeArgs | |
final class ValuePod<T> extends Pod<T> { | |
const ValuePod(this._factory, {super.debugLabel}); | |
static ValueMapper<U, ValuePod<T>> family<T, U>( | |
FamilyPodFactory<T, U> factory, { | |
String? debugLabel, | |
}) { | |
return Pod._family( | |
(U args) => ValuePod<T>( | |
(Ref ref) => factory(ref, args), | |
debugLabel: '${debugLabel ?? 'ValueFamily'}<$args>', | |
), | |
); | |
} | |
final PodFactory<T> _factory; | |
@override | |
PodElement<T> createElement() => _ValuePodElement<T>(this); | |
} | |
@optionalTypeArgs | |
final class StatePod<T> extends ValuePod<T> with NotifiablePod<T> { | |
StatePod(super._factory, {super.debugLabel}); | |
static ValueMapper<U, StatePod<T>> family<T, U>( | |
FamilyPodFactory<T, U> factory, { | |
String? debugLabel, | |
}) { | |
return Pod._family( | |
(U args) => StatePod<T>( | |
(Ref ref) => factory(ref, args), | |
debugLabel: '${debugLabel ?? 'StateFamily'}<$args>', | |
), | |
); | |
} | |
late final Pod<Notifier<T>> notifier = _ProxyPod( | |
(Ref ref) { | |
Pods pods = (ref as Pods); | |
return Notifier( | |
() => pods._resolvePodElement(this).state, | |
(T state) => pods._resolvePodElement(this, shouldMount: false).state = state, | |
); | |
}, | |
debugLabel: 'Notifier<$this>', | |
); | |
} | |
@optionalTypeArgs | |
final class FuturePod<T> extends _AsyncValuePod<T, FutureOr<T>> { | |
FuturePod(super._factory, {super.debugLabel}); | |
static ValueMapper<U, FuturePod<T>> family<T, U>( | |
FamilyPodFactory<FutureOr<T>, U> factory, { | |
String? debugLabel, | |
}) { | |
return Pod._family( | |
(U args) => FuturePod<T>( | |
(Ref ref) => factory(ref, args), | |
debugLabel: '${debugLabel ?? 'FutureFamily'}<$args>', | |
), | |
); | |
} | |
@override | |
PodElement<AsyncValue<T>> createElement() => _FuturePodElement(this); | |
} | |
@optionalTypeArgs | |
final class StreamPod<T> extends _AsyncValuePod<T, Stream<T>> { | |
StreamPod(super._factory, {super.debugLabel}); | |
static ValueMapper<U, StreamPod<T>> family<T, U>( | |
FamilyPodFactory<Stream<T>, U> factory, { | |
String? debugLabel, | |
}) { | |
return Pod._family( | |
(U args) => StreamPod<T>( | |
(Ref ref) => factory(ref, args), | |
debugLabel: '${debugLabel ?? 'StreamFamily'}<$args>', | |
), | |
); | |
} | |
@override | |
PodElement<AsyncValue<T>> createElement() => _StreamPodElement(this); | |
} | |
/* End Pod Types */ | |
/* Start Internal Pod Types */ | |
@optionalTypeArgs | |
final class _ProxyPod<T> extends Pod<T> with WatchablePod<T> { | |
const _ProxyPod(this._builder, {required super.debugLabel}); | |
final PodFactory<T> _builder; | |
@override | |
PodElement<T> createElement() => _ProxyPodElement(_builder); | |
} | |
@optionalTypeArgs | |
final class _SelectorPod<T> extends _ProxyPod<T> with NotifiablePod<T> { | |
const _SelectorPod(super._builder, {super.debugLabel}); | |
} | |
@optionalTypeArgs | |
final class _AsyncSelectorPod<T, U, V> extends Pod<Future<U>> with NotifiablePod<Future<U>> { | |
const _AsyncSelectorPod(this._factory, this._selector, {super.debugLabel}); | |
final PodFactory<V> _factory; | |
final ValueMapper<T, U> _selector; | |
@override | |
PodElement<Future<U>> createElement() => _AsyncSelectorPodElement<T, U, V>(_factory, _selector); | |
} | |
@optionalTypeArgs | |
abstract base class _AsyncValuePod<T, U> extends Pod<AsyncValue<T>> implements AsyncPod<T, U> { | |
_AsyncValuePod(this._factory, {super.debugLabel}); | |
@override | |
final PodFactory<U> _factory; | |
Completer<T>? _completer; | |
@override | |
late final WatchablePod<Future<T>> future = _AsyncSelectorPod<T, T, Future<T>>( | |
(Ref ref) { | |
if (_completer?.isCompleted == true) { | |
_completer = null; | |
} | |
_completer ??= Completer<T>.sync(); | |
final T? value = ref.watch(this).previousValue; | |
if (value != null) { | |
_completer?.complete(value); | |
} | |
(ref as Pods)._owner._dependents.whereType<_AsyncValuePodElement>().forEach( | |
(_) => _._refreshState(), | |
); | |
return _completer!.future; | |
}, | |
(T value) => value, | |
debugLabel: 'Future<$this>', | |
); | |
} | |
/* End Internal Pod Types */ | |
/* Start Pod Elements */ | |
final class _AnonymousPodElement extends PodElement<Object> { | |
_AnonymousPodElement() : super(Object()); | |
} | |
@optionalTypeArgs | |
final class _ValuePodElement<T> extends PodElement<T> { | |
_ValuePodElement(this._pod); | |
final ValuePod<T> _pod; | |
@override | |
void mount(Ref ref) { | |
_state = _pod._factory(ref); | |
super.mount(ref); | |
} | |
} | |
@optionalTypeArgs | |
base class _AsyncValuePodElement<T, U> extends PodElement<AsyncValue<T>> { | |
_AsyncValuePodElement(_AsyncValuePod<T, U> pod) : _factory = pod._factory; | |
final PodFactory<U> _factory; | |
@override | |
void mount(Ref ref) { | |
_refreshState(); | |
super.mount(ref); | |
} | |
void _refreshState() => state = PendingAsyncValue(_state?.previousValue); | |
} | |
@optionalTypeArgs | |
final class _FuturePodElement<T> extends _AsyncValuePodElement<T, FutureOr<T>> { | |
_FuturePodElement(super._pod); | |
@override | |
void mount(Ref ref) { | |
Future<T>.value(_factory(ref)).then((T value) { | |
state = SuccessfulAsyncValue(value); | |
}).catchError((Object error, StackTrace stackTrace) { | |
state = FailedAsyncValue(error, stackTrace); | |
}); | |
super.mount(ref); | |
} | |
} | |
@optionalTypeArgs | |
final class _StreamPodElement<T> extends _AsyncValuePodElement<T, Stream<T>> { | |
_StreamPodElement(super._pod); | |
StreamSubscription<T>? _subscription; | |
@override | |
void mount(Ref ref) { | |
_subscription?.cancel(); | |
_subscription = _factory(ref).listen((T value) { | |
state = SuccessfulAsyncValue(value); | |
}, onError: (Object error, StackTrace stackTrace) { | |
state = FailedAsyncValue(error, stackTrace); | |
}); | |
super.mount(ref); | |
} | |
@override | |
void dispose() { | |
_subscription?.cancel(); | |
super.dispose(); | |
} | |
} | |
@optionalTypeArgs | |
final class _ProxyPodElement<T> extends PodElement<T> { | |
_ProxyPodElement(this._builder); | |
final PodFactory<T> _builder; | |
@override | |
void mount(Pods ref) { | |
state = _builder(ref); | |
super.mount(ref); | |
} | |
} | |
@optionalTypeArgs | |
final class _AsyncSelectorPodElement<T, U, V> extends PodElement<Future<U>> { | |
_AsyncSelectorPodElement(this._factory, this._selector); | |
final PodFactory<V> _factory; | |
final ValueMapper<T, U> _selector; | |
final StreamController<T> _controller = StreamController<T>.broadcast(); | |
final Completer<U> _completer = Completer<U>(); | |
StreamSubscription<U>? _subscription; | |
StreamSubscription<T>? _innerSubscription; | |
@override | |
void mount(Ref ref) { | |
_subscription ??= _controller.stream.map(_selector).distinct().listen((U value) { | |
state = Future<U>.value(value); | |
if (!_completer.isCompleted) { | |
_completer.complete(value); | |
} | |
}); | |
switch (_factory(ref)) { | |
case T value: | |
_controller.add(value); | |
case Future<T> future: | |
future.then(_controller.add); | |
case Stream<T> stream: | |
_innerSubscription?.cancel(); | |
_innerSubscription = stream.listen(_controller.add); | |
} | |
_state = _completer.future; | |
super.mount(ref); | |
} | |
@override | |
void dispose() { | |
_state?.ignore(); | |
_innerSubscription?.cancel(); | |
_subscription?.cancel(); | |
_controller.close(); | |
super.dispose(); | |
} | |
} | |
/* End Pod Elements */ | |
/* Start Data Classes */ | |
@optionalTypeArgs | |
class Notifier<T> { | |
Notifier(this._stateFn, this._listener); | |
final ValueCallback<T> _stateFn; | |
final ValueMapper<T, void> _listener; | |
@protected | |
T get state => _stateFn(); | |
set state(T value) => _listener(value); | |
T update(ValueMapper<T, T> cb) => state = cb(_stateFn()); | |
@override | |
String toString() => 'Notifier<${_stateFn()}>'; | |
} | |
@optionalTypeArgs | |
sealed class AsyncValue<T> {} | |
@optionalTypeArgs | |
class PendingAsyncValue<T> implements AsyncValue<T> { | |
const PendingAsyncValue(this.data); | |
final T? data; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || other is PendingAsyncValue<T> && runtimeType == other.runtimeType && data == other.data; | |
@override | |
int get hashCode => data.hashCode; | |
@override | |
String toString() => 'PendingAsyncValue${data != null ? '<$data>' : ''}'; | |
} | |
@optionalTypeArgs | |
class SuccessfulAsyncValue<T> implements AsyncValue<T> { | |
const SuccessfulAsyncValue(this.data); | |
final T data; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
other is SuccessfulAsyncValue<T> && runtimeType == other.runtimeType && data == other.data; | |
@override | |
int get hashCode => data.hashCode; | |
@override | |
String toString() => 'SuccessfulAsyncValue<$data>'; | |
} | |
@optionalTypeArgs | |
class FailedAsyncValue<T> implements AsyncValue<T> { | |
const FailedAsyncValue(this.error, this.stackTrace); | |
final Object error; | |
final StackTrace stackTrace; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
other is FailedAsyncValue<T> && runtimeType == other.runtimeType && error == other.error; | |
@override | |
int get hashCode => error.hashCode; | |
@override | |
String toString() => 'FailedAsyncValue<$error>'; | |
} | |
extension<T> on AsyncValue<T> { | |
T? get previousValue => switch (this) { | |
PendingAsyncValue<T> state => state.data, | |
SuccessfulAsyncValue<T> state => state.data, | |
FailedAsyncValue<T> _ => null, | |
}; | |
} | |
/* End Data Classes */ | |