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