Line data Source code
1 : part of flutter_data;
2 :
3 : /// An adapter base class for all remote operations for type [T].
4 : ///
5 : /// Includes:
6 : ///
7 : /// - Remote methods such as [_RemoteAdapter.findAll] or [_RemoteAdapter.save]
8 : /// - Configuration methods and getters like [_RemoteAdapter.baseUrl] or [_RemoteAdapter.urlForFindAll]
9 : /// - Serialization methods like [_RemoteAdapterSerialization.serialize]
10 : /// - Watch methods such as [_RemoteAdapterWatch.watchOneNotifier]
11 : /// - Access to the [_RemoteAdapter.graph] for subclasses or mixins
12 : ///
13 : /// This class is meant to be extended via mixing in new adapters.
14 : /// This can be done with the [DataRepository] annotation on a [DataModel] class:
15 : ///
16 : /// ```
17 : /// @JsonSerializable()
18 : /// @DataRepository([MyAppAdapter])
19 : /// class Todo with DataModel<Todo> {
20 : /// @override
21 : /// final int? id;
22 : /// final String title;
23 : /// final bool completed;
24 : ///
25 : /// Todo({this.id, required this.title, this.completed = false});
26 : /// }
27 : /// ```
28 : class RemoteAdapter<T extends DataModel<T>> = _RemoteAdapter<T>
29 : with
30 : _RemoteAdapterSerialization<T>,
31 : _RemoteAdapterOffline<T>,
32 : _RemoteAdapterWatch<T>;
33 :
34 : abstract class _RemoteAdapter<T extends DataModel<T>> with _Lifecycle {
35 1 : @protected
36 : _RemoteAdapter(this.localAdapter, [this._oneProvider, this._allProvider]);
37 :
38 : @protected
39 : @visibleForTesting
40 : @nonVirtual
41 : final LocalAdapter<T> localAdapter;
42 :
43 : /// A [GraphNotifier] instance also available to adapters
44 1 : @protected
45 : @nonVirtual
46 2 : GraphNotifier get graph => localAdapter.graph;
47 :
48 : // None of these fields below can be late finals as they might be re-initialized
49 : Map<String, RemoteAdapter>? _adapters;
50 : bool? _remote;
51 : bool? _verbose;
52 : Reader? _read;
53 :
54 : /// All adapters for the relationship subgraph of [T] and their relationships.
55 : ///
56 : /// This [Map] is typically required when initializing new models, and passed as-is.
57 1 : @protected
58 : @nonVirtual
59 1 : Map<String, RemoteAdapter> get adapters => _adapters!;
60 :
61 : /// Give adapter subclasses access to the dependency injection system
62 1 : @protected
63 : @nonVirtual
64 1 : Reader get read => _read!;
65 :
66 : /// INTERNAL: DO NOT USE
67 1 : @visibleForTesting
68 : @protected
69 : @nonVirtual
70 1 : String get internalType => DataHelpers.getType<T>();
71 :
72 : /// The pluralized and downcased [DataHelpers.getType<T>] version of type [T]
73 : /// by default.
74 : ///
75 : /// Example: [T] as `Post` has a [type] of `posts`.
76 1 : @visibleForTesting
77 : @protected
78 1 : String get type => internalType;
79 :
80 : /// ONLY FOR FLUTTER DATA INTERNAL USE
81 : Watcher? internalWatch;
82 :
83 : final OneProvider<T>? _oneProvider;
84 : final AllProvider<T>? _allProvider;
85 :
86 : /// Returns the base URL for this type [T].
87 : ///
88 : /// Typically used in a generic adapter (i.e. one shared by all types)
89 : /// so it should be e.g. `http://jsonplaceholder.typicode.com/`
90 : ///
91 : /// For specific paths to this type [T], see [urlForFindAll], [urlForFindOne], etc
92 1 : @protected
93 1 : String get baseUrl => throw UnsupportedError('Please override baseUrl');
94 :
95 : /// Returns URL for [findAll]. Defaults to [type].
96 1 : @protected
97 2 : String urlForFindAll(Map<String, dynamic> params) => '$type';
98 :
99 : /// Returns HTTP method for [findAll]. Defaults to `GET`.
100 1 : @protected
101 : DataRequestMethod methodForFindAll(Map<String, dynamic> params) =>
102 : DataRequestMethod.GET;
103 :
104 : /// Returns URL for [findOne]. Defaults to [type]/[id].
105 1 : @protected
106 2 : String urlForFindOne(id, Map<String, dynamic> params) => '$type/$id';
107 :
108 : /// Returns HTTP method for [findOne]. Defaults to `GET`.
109 1 : @protected
110 : DataRequestMethod methodForFindOne(id, Map<String, dynamic> params) =>
111 : DataRequestMethod.GET;
112 :
113 : /// Returns URL for [save]. Defaults to [type]/[id] (if [id] is present).
114 1 : @protected
115 : String urlForSave(id, Map<String, dynamic> params) =>
116 3 : id != null ? '$type/$id' : type;
117 :
118 : /// Returns HTTP method for [save]. Defaults to `PATCH` if [id] is present,
119 : /// or `POST` otherwise.
120 1 : @protected
121 : DataRequestMethod methodForSave(id, Map<String, dynamic> params) =>
122 : id != null ? DataRequestMethod.PATCH : DataRequestMethod.POST;
123 :
124 : /// Returns URL for [delete]. Defaults to [type]/[id].
125 1 : @protected
126 2 : String urlForDelete(id, Map<String, dynamic> params) => '$type/$id';
127 :
128 : /// Returns HTTP method for [delete]. Defaults to `DELETE`.
129 1 : @protected
130 : DataRequestMethod methodForDelete(id, Map<String, dynamic> params) =>
131 : DataRequestMethod.DELETE;
132 :
133 : /// A [Map] representing default HTTP query parameters. Defaults to empty.
134 : ///
135 : /// It can return a [Future], so that adapters overriding this method
136 : /// have a chance to call async methods.
137 : ///
138 : /// Example:
139 : /// ```
140 : /// @override
141 : /// FutureOr<Map<String, dynamic>> get defaultParams async {
142 : /// final token = await _localStorage.get('token');
143 : /// return await super.defaultParams..addAll({'token': token});
144 : /// }
145 : /// ```
146 1 : @protected
147 1 : FutureOr<Map<String, dynamic>> get defaultParams => {};
148 :
149 : /// A [Map] representing default HTTP headers.
150 : ///
151 : /// Initial default is: `{'Content-Type': 'application/json'}`.
152 : ///
153 : /// It can return a [Future], so that adapters overriding this method
154 : /// have a chance to call async methods.
155 : ///
156 : /// Example:
157 : /// ```
158 : /// @override
159 : /// FutureOr<Map<String, String>> get defaultHeaders async {
160 : /// final token = await _localStorage.get('token');
161 : /// return await super.defaultHeaders..addAll({'Authorization': token});
162 : /// }
163 : /// ```
164 1 : @protected
165 : FutureOr<Map<String, String>> get defaultHeaders =>
166 1 : {'Content-Type': 'application/json'};
167 :
168 : // lifecycle methods
169 :
170 : @mustCallSuper
171 1 : Future<void> onInitialized() async {}
172 :
173 : @mustCallSuper
174 : @nonVirtual
175 1 : Future<RemoteAdapter<T>> initialize(
176 : {bool? remote,
177 : bool? verbose,
178 : required Map<String, RemoteAdapter> adapters,
179 : required Reader read}) async {
180 1 : if (isInitialized) return this as RemoteAdapter<T>;
181 :
182 : // initialize attributes
183 1 : _adapters = adapters;
184 1 : _remote = remote ?? true;
185 1 : _verbose = verbose ?? true;
186 1 : _read = read;
187 :
188 3 : await localAdapter.initialize();
189 :
190 : // hook for clients
191 2 : await onInitialized();
192 :
193 : return this as RemoteAdapter<T>;
194 : }
195 :
196 1 : @override
197 2 : bool get isInitialized => localAdapter.isInitialized;
198 :
199 1 : @override
200 : void dispose() {
201 2 : localAdapter.dispose();
202 : }
203 :
204 1 : void _assertInit() {
205 : assert(isInitialized, true);
206 : }
207 :
208 : // serialization interface
209 :
210 : /// Returns a [DeserializedData] object when deserializing a given [data].
211 : ///
212 : /// [key] can be used to supply a specific `key` when deserializing ONE model.
213 : @protected
214 : @visibleForTesting
215 : DeserializedData<T, DataModel> deserialize(Object data, {String key});
216 :
217 : /// Returns a serialized version of a model of [T],
218 : /// as a [Map<String, dynamic>] ready to be JSON-encoded.
219 : @protected
220 : @visibleForTesting
221 : Map<String, dynamic> serialize(T model);
222 :
223 : // caching
224 :
225 : /// Returns whether calling [findAll] should trigger a remote call.
226 : ///
227 : /// Meant to be overriden. Defaults to [remote].
228 1 : @protected
229 : bool shouldLoadRemoteAll(
230 : bool remote,
231 : Map<String, dynamic> params,
232 : Map<String, String> headers,
233 : ) =>
234 : remote;
235 :
236 : /// Returns whether calling [findOne] should initiate an HTTP call.
237 : ///
238 : /// Meant to be overriden. Defaults to [remote].
239 1 : @protected
240 : bool shouldLoadRemoteOne(
241 : dynamic id,
242 : bool remote,
243 : Map<String, dynamic> params,
244 : Map<String, String> headers,
245 : ) =>
246 : remote;
247 :
248 : // remote implementation
249 :
250 : @protected
251 : @visibleForTesting
252 1 : Future<List<T>> findAll({
253 : bool? remote,
254 : Map<String, dynamic>? params,
255 : Map<String, String>? headers,
256 : bool? syncLocal,
257 : OnData<List<T>>? onSuccess,
258 : OnDataError<List<T>>? onError,
259 : }) async {
260 1 : _assertInit();
261 1 : remote ??= _remote;
262 : syncLocal ??= false;
263 3 : params = await defaultParams & params;
264 3 : headers = await defaultHeaders & headers;
265 :
266 1 : if (!shouldLoadRemoteAll(remote!, params, headers)) {
267 3 : final models = localAdapter.findAll().toImmutableList();
268 1 : models.map((m) => m._initialize(adapters, save: true));
269 : return models;
270 : }
271 :
272 2 : final result = await sendRequest(
273 5 : baseUrl.asUri / urlForFindAll(params) & params,
274 1 : method: methodForFindAll(params),
275 : headers: headers,
276 : requestType: DataRequestType.findAll,
277 1 : key: internalType,
278 1 : onSuccess: (data) async {
279 : if (syncLocal!) {
280 3 : await localAdapter.clear();
281 : }
282 : final models = data != null
283 3 : ? deserialize(data as Object).models.toImmutableList()
284 1 : : <T>[];
285 : return onSuccess?.call(models) ?? models;
286 : },
287 : onError: onError,
288 : );
289 1 : return result ?? <T>[];
290 : }
291 :
292 : @protected
293 : @visibleForTesting
294 1 : Future<T?> findOne(
295 : final dynamic model, {
296 : bool? remote,
297 : Map<String, dynamic>? params,
298 : Map<String, String>? headers,
299 : OnData<T?>? onSuccess,
300 : OnDataError<T?>? onError,
301 : }) async {
302 1 : _assertInit();
303 : if (model == null) {
304 1 : throw AssertionError('Model must be not null');
305 : }
306 1 : remote ??= _remote;
307 :
308 3 : params = await defaultParams & params;
309 3 : headers = await defaultHeaders & headers;
310 :
311 1 : final id = _resolveId(model);
312 :
313 1 : if (!shouldLoadRemoteOne(id, remote!, params, headers)) {
314 3 : final key = graph.getKeyForId(internalType, id,
315 2 : keyIfAbsent: model is T ? model._key : null);
316 : if (key == null) {
317 : return null;
318 : }
319 2 : final newModel = localAdapter.findOne(key);
320 2 : newModel?._initialize(adapters, save: true);
321 : return newModel;
322 : }
323 :
324 2 : return await sendRequest(
325 5 : baseUrl.asUri / urlForFindOne(id, params) & params,
326 1 : method: methodForFindOne(id, params),
327 : headers: headers,
328 : requestType: DataRequestType.findOne,
329 2 : key: StringUtils.typify(internalType, id),
330 1 : onSuccess: (data) {
331 : final model = data != null
332 2 : ? deserialize(data as Map<String, dynamic>).model
333 : : null;
334 : return onSuccess?.call(model) ?? model;
335 : },
336 : onError: onError,
337 : );
338 : }
339 :
340 : @protected
341 : @visibleForTesting
342 1 : Future<T> save(
343 : T model, {
344 : bool? remote,
345 : Map<String, dynamic>? params,
346 : Map<String, String>? headers,
347 : OnData<T>? onSuccess,
348 : OnDataError<T>? onError,
349 : }) async {
350 1 : _assertInit();
351 1 : remote ??= _remote;
352 :
353 3 : params = await defaultParams & params;
354 3 : headers = await defaultHeaders & headers;
355 :
356 : // we ignore the `init` argument here as
357 : // saving locally requires initializing
358 2 : model._initialize(adapters, save: true);
359 :
360 1 : if (remote == false) {
361 : return model;
362 : }
363 :
364 2 : final body = json.encode(serialize(model));
365 :
366 2 : final result = await sendRequest(
367 6 : baseUrl.asUri / urlForSave(model.id, params) & params,
368 2 : method: methodForSave(model.id, params),
369 : headers: headers,
370 : body: body,
371 : requestType: DataRequestType.save,
372 1 : key: model._key,
373 1 : onSuccess: (data) {
374 : T _model;
375 : if (data == null) {
376 : // return "old" model if response was empty
377 2 : _model = model._initialize(adapters, save: true);
378 : } else {
379 : // deserialize already inits models
380 : // if model had a key already, reuse it
381 : final _newModel =
382 2 : deserialize(data as Map<String, dynamic>, key: model._key!)
383 1 : .model!;
384 :
385 : // in the unlikely case where supplied key couldn't be used
386 : // ensure "old" copy of model carries the updated key
387 4 : if (model._key != null && model._key != _newModel._key) {
388 3 : graph.removeKey(model._key!);
389 2 : model._key = _newModel._key;
390 : }
391 : _model = _newModel;
392 : }
393 : return onSuccess?.call(_model) ?? _model;
394 : },
395 : onError: onError,
396 : );
397 : return result ?? model;
398 : }
399 :
400 : @protected
401 : @visibleForTesting
402 1 : Future<void> delete(
403 : dynamic model, {
404 : bool? remote,
405 : Map<String, dynamic>? params,
406 : Map<String, String>? headers,
407 : OnData<void>? onSuccess,
408 : OnDataError<void>? onError,
409 : }) async {
410 1 : _assertInit();
411 1 : remote ??= _remote;
412 :
413 3 : params = await defaultParams & params;
414 3 : headers = await defaultHeaders & headers;
415 :
416 1 : final id = _resolveId(model);
417 1 : final key = _keyForModel(model);
418 :
419 : if (key != null) {
420 3 : await localAdapter.delete(key);
421 : }
422 :
423 : if (remote!) {
424 2 : return await sendRequest(
425 5 : baseUrl.asUri / urlForDelete(id, params) & params,
426 1 : method: methodForDelete(id, params),
427 : headers: headers,
428 : requestType: DataRequestType.delete,
429 2 : key: StringUtils.typify(internalType, id),
430 : onSuccess: onSuccess,
431 : onError: onError,
432 : );
433 : }
434 : }
435 :
436 1 : @protected
437 : @visibleForTesting
438 2 : Future<void> clear() => localAdapter.clear();
439 :
440 : // http
441 :
442 : /// An [http.Client] used to make an HTTP request.
443 : ///
444 : /// This getter returns a new client every time
445 : /// as by default they are used once and then closed.
446 0 : @protected
447 : @visibleForTesting
448 0 : http.Client get httpClient => http.Client();
449 :
450 : /// The function used to perform an HTTP request and return an [R].
451 : ///
452 : /// **IMPORTANT**:
453 : /// - [uri] takes the FULL `Uri` including query parameters
454 : /// - [headers] does NOT include ANY defaults such as [defaultHeaders]
455 : /// (unless you omit the argument, in which case defaults will be included)
456 : ///
457 : /// Example:
458 : ///
459 : /// ```
460 : /// await sendRequest(
461 : /// baseUrl.asUri + 'token' & await defaultParams & {'a': 1},
462 : /// headers: await defaultHeaders & {'a': 'b'},
463 : /// onSuccess: (data) => data['token'] as String,
464 : /// );
465 : /// ```
466 : ///
467 : ///ignore: comment_references
468 : /// To build the URI you can use [String.asUri], [Uri.+] and [Uri.&].
469 : ///
470 : /// To merge headers and params with their defaults you can use the helper
471 : /// [Map<String, dynamic>.&].
472 : ///
473 : /// In addition, [onSuccess] is supplied to post-process the
474 : /// data in JSON format. Deserialization and initialization
475 : /// typically occur in this function.
476 : ///
477 : /// [onError] can also be supplied to override [_RemoteAdapter.onError].
478 : @protected
479 : @visibleForTesting
480 1 : FutureOr<R?> sendRequest<R>(
481 : final Uri uri, {
482 : DataRequestMethod method = DataRequestMethod.GET,
483 : Map<String, String>? headers,
484 : String? body,
485 : String? key,
486 : OnRawData<R>? onSuccess,
487 : OnDataError<R>? onError,
488 : DataRequestType requestType = DataRequestType.adhoc,
489 : bool omitDefaultParams = false,
490 : }) async {
491 : // callbacks
492 0 : onError ??= this.onError as OnDataError<R>;
493 :
494 2 : headers ??= await defaultHeaders;
495 : final _params =
496 3 : omitDefaultParams ? <String, dynamic>{} : await defaultParams;
497 :
498 : http.Response? response;
499 : Object? data;
500 : Object? error;
501 : StackTrace? stackTrace;
502 :
503 : try {
504 3 : final request = http.Request(method.toShortString(), uri & _params);
505 2 : request.headers.addAll(headers);
506 : if (body != null) {
507 1 : request.body = body;
508 : }
509 3 : final stream = await httpClient.send(request);
510 2 : response = await http.Response.fromStream(stream);
511 : } catch (err, stack) {
512 : error = err;
513 : stackTrace = stack;
514 : } finally {
515 2 : httpClient.close();
516 : }
517 :
518 : // response handling
519 :
520 : try {
521 2 : if (response?.body.isNotEmpty ?? false) {
522 2 : data = json.decode(response!.body);
523 : }
524 1 : } on FormatException catch (e) {
525 : error = e;
526 : }
527 :
528 1 : final code = response?.statusCode;
529 :
530 1 : if (_verbose!) {
531 1 : print(
532 3 : '[flutter_data] [$internalType] ${method.toShortString()} $uri [HTTP ${code ?? ''}]${body != null ? '\n -> body:\n $body' : ''}');
533 : }
534 :
535 2 : if (error == null && code != null && code >= 200 && code < 300) {
536 1 : return await onSuccess?.call(data);
537 : } else {
538 1 : final e = DataException(error ?? data!,
539 : stackTrace: stackTrace, statusCode: code);
540 :
541 1 : if (_verbose!) {
542 3 : print('[flutter_data] [$internalType] Error: $e');
543 : }
544 1 : return await onError(e);
545 : }
546 : }
547 :
548 : /// Implements global request error handling.
549 : ///
550 : /// Defaults to throw [e] unless it is an HTTP 404
551 : /// or an `OfflineException`.
552 : ///
553 : /// NOTE: `onError` arguments throughout the API are used
554 : /// to override this default behavior.
555 1 : @protected
556 : @visibleForTesting
557 : FutureOr<R?> onError<R>(DataException e) {
558 3 : if (e.statusCode == 404 || e is OfflineException) {
559 : return null;
560 : }
561 : throw e;
562 : }
563 :
564 : /// Initializes [model] making it ready to use with [DataModel] extensions.
565 : ///
566 : /// Optionally provide [key]. Use [save] to persist in local storage.
567 1 : @protected
568 : @visibleForTesting
569 : @nonVirtual
570 : T initializeModel(T model, {String? key, bool save = false}) {
571 2 : return model._initialize(adapters, key: key, save: save);
572 : }
573 :
574 1 : String? _resolveId(dynamic model) {
575 2 : final id = model is T ? model.id : model;
576 1 : return id?.toString();
577 : }
578 :
579 1 : String? _keyForModel(dynamic model) {
580 1 : final id = _resolveId(model);
581 3 : return graph.getKeyForId(internalType, id,
582 2 : keyIfAbsent: model is T ? model._key : null);
583 : }
584 : }
585 :
586 : /// A utility class used to return deserialized main [models] AND [included] models.
587 : class DeserializedData<T, I> {
588 1 : const DeserializedData(this.models, {this.included = const []});
589 : final List<T> models;
590 : final List<I> included;
591 3 : T? get model => models.singleOrNull;
592 : }
593 :
594 : // ignore: constant_identifier_names
595 2 : enum DataRequestMethod { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE }
596 :
597 : extension _ToStringX on DataRequestMethod {
598 4 : String toShortString() => toString().split('.').last;
599 : }
600 :
601 : typedef OnData<R> = FutureOr<R> Function(R);
602 : typedef OnRawData<R> = FutureOr<R?> Function(dynamic);
603 : typedef OnDataError<R> = FutureOr<R?> Function(DataException);
604 :
605 : // ignore: constant_identifier_names
606 2 : enum DataRequestType {
607 : findAll,
608 : findOne,
609 : save,
610 : delete,
611 : adhoc,
612 : }
613 :
614 : extension _DataRequestTypeX on DataRequestType {
615 4 : String toShortString() => toString().split('.').last;
616 : }
617 :
618 1 : DataRequestType _getDataRequestType(String type) =>
619 4 : DataRequestType.values.singleWhere((_) => _.toShortString() == type);
|