Line data Source code
1 : part of flutter_data;
2 :
3 : mixin _RemoteAdapterWatch<T extends DataModel<T>> on _RemoteAdapter<T> {
4 0 : @protected
5 : @visibleForTesting
6 : Duration get throttleDuration =>
7 : const Duration(milliseconds: 16); // 1 frame at 60fps
8 :
9 1 : @protected
10 : @visibleForTesting
11 : @nonVirtual
12 : DelayedStateNotifier<List<DataGraphEvent>> get throttledGraph =>
13 4 : graph.throttle(() => throttleDuration);
14 :
15 1 : @protected
16 : @visibleForTesting
17 : DataStateNotifier<List<T>> watchAllNotifier({
18 : bool? remote,
19 : Map<String, dynamic>? params,
20 : Map<String, String>? headers,
21 : bool? syncLocal,
22 : }) {
23 1 : _assertInit();
24 1 : remote ??= _remote;
25 : syncLocal ??= false;
26 :
27 1 : final _notifier = DataStateNotifier<List<T>>(
28 2 : data: DataState(localAdapter
29 1 : .findAll()
30 3 : .map((m) => initializeModel(m, save: true))
31 1 : .filterNulls
32 1 : .toList()),
33 1 : reload: (notifier) async {
34 1 : if (!notifier.mounted) {
35 : return;
36 : }
37 : try {
38 1 : final _future = findAll(
39 : params: params,
40 : headers: headers,
41 : remote: remote,
42 : syncLocal: syncLocal,
43 : );
44 : if (remote!) {
45 1 : notifier.updateWith(isLoading: true);
46 : }
47 1 : await _future;
48 : // trigger doneLoading to ensure state is updated with isLoading=false
49 4 : graph._notify([internalType], DataGraphEventType.doneLoading);
50 1 : } on DataException catch (e) {
51 : // we're only interested in notifying errors
52 : // as models will pop up via the graph notifier
53 : // (we can catch the exception as we are NOT supplying
54 : // an `onError` to `findAll`)
55 1 : if (notifier.mounted) {
56 1 : notifier.updateWith(isLoading: false, exception: e);
57 : } else {
58 : rethrow;
59 : }
60 : }
61 : },
62 : );
63 :
64 : // kick off
65 1 : _notifier.reload();
66 :
67 3 : final _dispose = throttledGraph.addListener((events) {
68 1 : if (!_notifier.mounted) {
69 : return;
70 : }
71 :
72 3 : final models = localAdapter.findAll().toImmutableList();
73 : final modelChanged =
74 3 : !const DeepCollectionEquality().equals(models, _notifier.data.model);
75 : // ensure the done signal belongs to this notifier
76 : final doneLoading = events
77 2 : .where((e) =>
78 2 : e.type == DataGraphEventType.doneLoading &&
79 4 : e.keys.first == internalType)
80 1 : .isNotEmpty;
81 : if (modelChanged || doneLoading) {
82 1 : _notifier.updateWith(model: models, isLoading: false, exception: null);
83 : }
84 : });
85 :
86 1 : _notifier.onDispose = _dispose;
87 : return _notifier;
88 : }
89 :
90 0 : DataState<List<T>> watchAll({
91 : bool? remote,
92 : Map<String, dynamic>? params,
93 : Map<String, String>? headers,
94 : bool? syncLocal,
95 : }) {
96 0 : if (internalWatch == null || _allProvider == null) {
97 0 : throw UnsupportedError(_watchAllError);
98 : }
99 0 : return internalWatch!(_allProvider!(
100 : remote: remote,
101 : params: params,
102 : headers: headers,
103 : syncLocal: syncLocal,
104 : ));
105 : }
106 :
107 0 : String get _watchAllError =>
108 0 : 'Should only be used via `ref.$type.watchAll`. Alternatively use `watch${type.capitalize()}()`.';
109 :
110 1 : @protected
111 : @visibleForTesting
112 : DataStateNotifier<T?> watchOneNotifier(
113 : dynamic model, {
114 : bool? remote,
115 : Map<String, dynamic>? params,
116 : Map<String, String>? headers,
117 : AlsoWatch<T>? alsoWatch,
118 : }) {
119 1 : _assertInit();
120 : if (model == null) {
121 0 : throw AssertionError();
122 : }
123 1 : remote ??= _remote;
124 :
125 1 : final id = _resolveId(model);
126 :
127 : // lazy key access
128 1 : String? key() {
129 3 : return graph.getKeyForId(internalType, id,
130 2 : keyIfAbsent: (model is T ? model._key : null));
131 : }
132 :
133 : var _key = key();
134 :
135 : final _alsoWatchFilters = <String>{};
136 :
137 2 : final localModel = _key != null ? localAdapter.findOne(_key) : null;
138 1 : final _notifier = DataStateNotifier<T?>(
139 1 : data: DataState(
140 1 : localModel == null ? null : initializeModel(localModel, save: true)),
141 1 : reload: (notifier) async {
142 1 : if (!notifier.mounted) {
143 : return;
144 : }
145 : try {
146 : if (id != null) {
147 1 : final _future = findOne(
148 : id,
149 : params: params,
150 : headers: headers,
151 : remote: remote,
152 : );
153 : if (remote!) {
154 1 : notifier.updateWith(isLoading: true);
155 : }
156 1 : await _future;
157 : }
158 :
159 : _key ??= key();
160 : if (_key != null) {
161 : // trigger doneLoading to ensure state is updated with isLoading=false
162 3 : graph._notify([_key!], DataGraphEventType.doneLoading);
163 : }
164 1 : } on DataException catch (e) {
165 : // we're only interested in notifying errors
166 : // as models will pop up via the graph notifier
167 : // (we can catch the exception as we are NOT supplying
168 : // an `onError` to `findOne`)
169 1 : if (notifier.mounted) {
170 1 : notifier.updateWith(isLoading: false, exception: e);
171 : } else {
172 : rethrow;
173 : }
174 : }
175 : },
176 : );
177 :
178 1 : void _initializeRelationshipsToWatch(T? model) {
179 : if (alsoWatch != null &&
180 1 : _alsoWatchFilters.isEmpty &&
181 : model != null &&
182 1 : model.isInitialized) {
183 3 : _alsoWatchFilters.addAll(alsoWatch(model).map((rel) {
184 1 : return rel._name;
185 : }));
186 : }
187 : }
188 :
189 : // kick off
190 :
191 : // try to find relationships to watch
192 2 : _initializeRelationshipsToWatch(_notifier.data.model);
193 :
194 : // trigger local + async loading
195 1 : _notifier.reload();
196 :
197 : // start listening to graph for further changes
198 3 : final _dispose = throttledGraph.addListener((events) {
199 1 : if (!_notifier.mounted) {
200 : return;
201 : }
202 :
203 : // buffers
204 2 : var modelBuffer = _notifier.data.model;
205 : var refresh = false;
206 :
207 2 : for (final event in events) {
208 : _key ??= key();
209 2 : if (_key != null && event.keys.containsFirst(_key!)) {
210 : // add/update
211 2 : if (event.type == DataGraphEventType.addNode ||
212 2 : event.type == DataGraphEventType.updateNode) {
213 2 : final model = localAdapter.findOne(_key!);
214 : if (model != null) {
215 1 : initializeModel(model, save: true);
216 : _initializeRelationshipsToWatch(model);
217 : modelBuffer = model;
218 : }
219 : }
220 :
221 : // remove
222 2 : if (event.type == DataGraphEventType.removeNode &&
223 2 : _notifier.data.model != null) {
224 : modelBuffer = null;
225 : }
226 :
227 : // changes on specific relationships of this model
228 2 : if (_notifier.data.model != null &&
229 2 : event.type.isEdge &&
230 2 : _alsoWatchFilters.contains(event.metadata)) {
231 : // calculate currently related models
232 : refresh = true;
233 : }
234 :
235 : if (modelBuffer != null &&
236 : // ensure the done signal belongs to this type
237 3 : event.keys.first == _key &&
238 2 : event.type == DataGraphEventType.doneLoading) {
239 : refresh = true;
240 : }
241 : }
242 :
243 : // updates on all models of specific relationships of this model
244 2 : if (event.type == DataGraphEventType.updateNode &&
245 6 : _relatedKeys(_notifier.data.model!).any(event.keys.contains)) {
246 : refresh = true;
247 : }
248 : }
249 :
250 : // NOTE: because of this comparison, use field equality
251 : // rather than key equality (which wouldn't update)
252 3 : if (modelBuffer != _notifier.data.model || refresh) {
253 1 : _notifier.updateWith(
254 : model: modelBuffer, isLoading: false, exception: null);
255 : }
256 : });
257 :
258 1 : _notifier.onDispose = _dispose;
259 : return _notifier;
260 : }
261 :
262 0 : DataState<T?> watchOne(
263 : dynamic id, {
264 : bool? remote,
265 : Map<String, dynamic>? params,
266 : Map<String, String>? headers,
267 : AlsoWatch<T>? alsoWatch,
268 : }) {
269 0 : if (internalWatch == null || _oneProvider == null) {
270 0 : throw UnsupportedError(_watchOneError);
271 : }
272 0 : return internalWatch!(_oneProvider!(
273 : id,
274 : remote: remote,
275 : params: params,
276 : headers: headers,
277 : alsoWatch: alsoWatch,
278 : ));
279 : }
280 :
281 0 : String get _watchOneError =>
282 0 : 'Should only be used via `ref.$type.watchOne`. Alternatively use `watch${type.singularize().capitalize()}()`.';
283 :
284 1 : Set<String> _relatedKeys(T model) {
285 1 : return localAdapter
286 1 : .relationshipsFor(model)
287 1 : .values
288 4 : .map((meta) => (meta['instance'] as Relationship?)?.keys)
289 1 : .filterNulls
290 2 : .expand((key) => key)
291 1 : .toSet();
292 : }
293 : }
294 :
295 : typedef AlsoWatch<T> = List<Relationship> Function(T);
|