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 */ | |