Line data Source code
1 : library data_state;
2 :
3 : import 'package:equatable/equatable.dart';
4 : import 'package:state_notifier/state_notifier.dart';
5 :
6 : class DataState<T> with EquatableMixin {
7 : final T model;
8 : final bool isLoading;
9 : final DataException? exception;
10 : final StackTrace? stackTrace;
11 :
12 1 : const DataState(
13 : this.model, {
14 : this.isLoading = false,
15 : this.exception,
16 : this.stackTrace,
17 : });
18 :
19 2 : bool get hasException => exception != null;
20 :
21 2 : bool get hasModel => model != null;
22 :
23 1 : @override
24 4 : List<Object?> get props => [model, isLoading, exception];
25 :
26 0 : @override
27 : bool get stringify => true;
28 : }
29 :
30 : class DataException with EquatableMixin implements Exception {
31 : final Object error;
32 : final StackTrace? stackTrace;
33 : final int? statusCode;
34 :
35 1 : const DataException(this.error, {this.stackTrace, this.statusCode});
36 :
37 1 : @override
38 4 : List<Object?> get props => [error, stackTrace, statusCode];
39 :
40 1 : @override
41 : String toString() {
42 6 : return 'DataException: $error ${statusCode != null ? " [HTTP $statusCode]" : ""}\n${stackTrace ?? ''}';
43 : }
44 : }
45 :
46 : class DataStateNotifier<T> extends StateNotifier<DataState<T>> {
47 1 : DataStateNotifier({
48 : required DataState<T> data,
49 : Future<void> Function(DataStateNotifier<T>)? reload,
50 : }) : _reloadFn = reload,
51 1 : super(data);
52 :
53 : final Future<void> Function(DataStateNotifier<T>)? _reloadFn;
54 : void Function()? onDispose;
55 :
56 2 : DataState<T> get data => super.state;
57 :
58 1 : void updateWith({
59 : Object? model = stamp,
60 : bool? isLoading,
61 : Object? exception = stamp,
62 : Object? stackTrace = stamp,
63 : }) {
64 2 : super.state = DataState<T>(
65 3 : model == stamp ? state.model : model as T,
66 2 : isLoading: isLoading ?? state.isLoading,
67 : exception:
68 3 : exception == stamp ? state.exception : exception as DataException?,
69 : stackTrace:
70 3 : stackTrace == stamp ? state.stackTrace : stackTrace as StackTrace?,
71 : );
72 : }
73 :
74 1 : Future<void> reload() async {
75 1 : return _reloadFn?.call(this);
76 : }
77 :
78 1 : @override
79 : RemoveListener addListener(
80 : Listener<DataState<T>> listener, {
81 : bool fireImmediately = true,
82 : }) {
83 : final dispose =
84 1 : super.addListener(listener, fireImmediately: fireImmediately);
85 1 : return () {
86 : dispose();
87 1 : onDispose?.call();
88 : };
89 : }
90 :
91 1 : @override
92 : void dispose() {
93 1 : if (mounted) {
94 1 : super.dispose();
95 : }
96 : }
97 : }
98 :
99 : class _Stamp {
100 1 : const _Stamp();
101 : }
102 :
103 : const stamp = _Stamp();
104 :
105 : class _FunctionalDataStateNotifier<T, W> extends DataStateNotifier<W> {
106 : final DataStateNotifier<W> _source;
107 : late RemoveListener _sourceDisposeFn;
108 :
109 3 : _FunctionalDataStateNotifier(this._source) : super(data: _source.data);
110 :
111 1 : DataStateNotifier<W> where(bool Function(T) test) {
112 4 : _sourceDisposeFn = _source.addListener((state) {
113 1 : if (state.hasModel) {
114 : W _model;
115 :
116 1 : if (_typesEqual<W, List<T>>()) {
117 3 : _model = (state.model as List<T>).where(test).toList() as W;
118 1 : } else if (_typesEqual<W, T?>()) {
119 2 : _model = test(state.model as T) ? state.model : null as W;
120 : } else {
121 0 : throw UnsupportedError('W must either be T? or List<T>');
122 : }
123 :
124 2 : super.state = DataState(_model,
125 1 : isLoading: state.isLoading,
126 1 : exception: state.exception,
127 1 : stackTrace: state.stackTrace);
128 : }
129 : });
130 : return this;
131 : }
132 :
133 1 : DataStateNotifier<W> map(T Function(T) convert) {
134 4 : _sourceDisposeFn = _source.addListener((state) {
135 1 : if (state.hasModel) {
136 : W _model;
137 :
138 1 : if (_typesEqual<W, List<T>>()) {
139 3 : _model = (state.model as List<T>).map(convert).toList() as W;
140 1 : } else if (_typesEqual<W, T>()) {
141 1 : _model = convert(state.model as T) as W;
142 : } else {
143 0 : throw UnsupportedError('W must either be T or List<T>');
144 : }
145 :
146 2 : super.state = DataState(_model,
147 1 : isLoading: state.isLoading,
148 1 : exception: state.exception,
149 1 : stackTrace: state.stackTrace);
150 : }
151 : });
152 : return this;
153 : }
154 :
155 2 : bool _typesEqual<T1, T2>() => T1 == T2;
156 :
157 1 : @override
158 : RemoveListener addListener(
159 : Listener<DataState<W>> listener, {
160 : bool fireImmediately = true,
161 : }) {
162 : final dispose =
163 1 : super.addListener(listener, fireImmediately: fireImmediately);
164 1 : return () {
165 : dispose.call();
166 1 : _sourceDisposeFn.call();
167 : };
168 : }
169 :
170 0 : @override
171 : void dispose() {
172 0 : if (mounted) {
173 0 : super.dispose();
174 : }
175 0 : _source.dispose();
176 : }
177 : }
178 :
179 : /// Functional utilities for [DataStateNotifier]
180 : extension DataStateNotifierListX<T> on DataStateNotifier<List<T>> {
181 : /// Filters all models of the list (if present) through [test]
182 1 : DataStateNotifier<List<T>> where(bool Function(T) test) {
183 2 : return _FunctionalDataStateNotifier<T, List<T>>(this).where(test);
184 : }
185 :
186 : /// Maps all models of the list (if present) through [convert]
187 1 : DataStateNotifier<List<T>> map(T Function(T) convert) {
188 2 : return _FunctionalDataStateNotifier<T, List<T>>(this).map(convert);
189 : }
190 : }
191 :
192 : /// Functional utilities for [DataStateNotifier]
193 : extension DataStateNotifierX<T> on DataStateNotifier<T> {
194 : /// Filters all models of the list (if present) through [test]
195 1 : DataStateNotifier<T> where(bool Function(T) test) {
196 2 : return _FunctionalDataStateNotifier<T, T>(this).where(test);
197 : }
198 :
199 : /// Maps all models of the list (if present) through [convert]
200 1 : DataStateNotifier<T> map(T Function(T) convert) {
201 2 : return _FunctionalDataStateNotifier<T, T>(this).map(convert);
202 : }
203 : }
|