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