Line data Source code
1 : part of flutter_data;
2 :
3 : /// Data classes extending this class are marked to be managed
4 : /// through Flutter Data.
5 : ///
6 : /// It enforces the implementation of an [id] getter.
7 : /// It contains private state and methods to track the model's identity.
8 : abstract class DataModel<T extends DataModel<T>> {
9 : Object? get id;
10 :
11 11 : DataModel() {
12 33 : final repository = internalRepositories[_internalType];
13 : if (repository != null) {
14 33 : repository.remoteAdapter.localAdapter.initModel(
15 : this,
16 22 : onModelInitialized: repository.remoteAdapter.onModelInitialized,
17 : );
18 : }
19 : }
20 :
21 : String? _key;
22 22 : String get _internalType => DataHelpers.getType<T>();
23 11 : T get _this => this as T;
24 :
25 : /// Exposes this type's [RemoteAdapter]
26 11 : RemoteAdapter<T> get _remoteAdapter =>
27 44 : internalRepositories[_internalType]!.remoteAdapter as RemoteAdapter<T>;
28 :
29 : // data model helpers
30 :
31 : /// Returns a model's `_key` private attribute.
32 : ///
33 : /// Useful for testing, debugging or usage in [RemoteAdapter] subclasses.
34 14 : static String keyFor(DataModel model) => model._key!;
35 :
36 : /// Returns a model's non-null relationships.
37 1 : static Map<String, Relationship> relationshipsFor<T extends DataModel<T>>(
38 : T model) {
39 1 : return {
40 : for (final meta
41 4 : in model._remoteAdapter.localAdapter.relationshipMetas.values)
42 6 : if (meta.instance(model) != null) meta.name: meta.instance(model)!,
43 : };
44 : }
45 :
46 : /// Returns a model [RemoteAdapter]
47 4 : static RemoteAdapter adapterFor(DataModel model) => model._remoteAdapter;
48 :
49 : /// Apply [model]'s key to [applyTo].
50 4 : static DataModel withKeyOf(DataModel model,
51 : {required DataModel applyTo, bool force = false}) {
52 : final _this = applyTo;
53 12 : if (model._key != _this._key) {
54 : DataModel oldModel;
55 : DataModel newModel;
56 :
57 : // if the passed-in model has no ID
58 : // then treat the original as prevalent
59 12 : if (force == false && model.id == null && _this.id != null) {
60 : oldModel = model;
61 : newModel = _this;
62 : } else {
63 : // in all other cases, treat the passed-in
64 : // model as prevalent
65 : oldModel = _this;
66 : newModel = model;
67 : }
68 :
69 4 : final oldKey = oldModel._key;
70 12 : if (_this._key != newModel._key) {
71 4 : _this._key = newModel._key;
72 : }
73 12 : if (_this._key != oldModel._key) {
74 6 : oldModel._key = _this._key;
75 9 : _this._remoteAdapter.graph.removeKey(oldKey!);
76 : }
77 :
78 4 : if (oldModel.id != null) {
79 2 : _this._remoteAdapter.graph
80 3 : .removeId(_this._internalType, oldModel.id!, notify: false);
81 5 : _this._remoteAdapter.graph.getKeyForId(_this._internalType, oldModel.id,
82 1 : keyIfAbsent: _this._key);
83 : }
84 : }
85 : return _this;
86 : }
87 : }
88 :
89 : /// Extension that adds syntax-sugar to data classes,
90 : /// linking them to common [Repository] methods such as
91 : /// [save] and [delete].
92 : extension DataModelExtension<T extends DataModel<T>> on DataModel<T> {
93 : /// Copy identity (internal key) from an old model to a new one
94 : /// to signal they are the same.
95 : ///
96 : /// **Only makes sense to use if model is immutable and has no ID!**
97 : ///
98 : /// ```
99 : /// final walter = Person(name: 'Walter');
100 : /// person.copyWith(age: 56).withKeyOf(walter);
101 : /// ```
102 : ///
103 : /// [force] will set [model]'s key even if its `id` is null.
104 4 : T withKeyOf(T model, {bool force = false}) {
105 4 : return DataModel.withKeyOf(model, applyTo: this, force: force) as T;
106 : }
107 :
108 : /// Saves this model through a call equivalent to [Repository.save].
109 : ///
110 : /// Usage: `await post.save()`, `author.save(remote: false, params: {'a': 'x'})`.
111 6 : Future<T> save({
112 : bool? remote,
113 : Map<String, dynamic>? params,
114 : Map<String, String>? headers,
115 : OnSuccessOne<T>? onSuccess,
116 : OnErrorOne<T>? onError,
117 : }) async {
118 18 : return await _remoteAdapter.save(
119 6 : _this,
120 : remote: remote,
121 : params: params,
122 : headers: headers,
123 : onSuccess: onSuccess,
124 : onError: onError,
125 : );
126 : }
127 :
128 : /// Deletes this model through a call equivalent to [Repository.delete].
129 3 : Future<T?> delete({
130 : bool? remote,
131 : Map<String, dynamic>? params,
132 : Map<String, String>? headers,
133 : OnSuccessOne<T>? onSuccess,
134 : OnErrorOne<T>? onError,
135 : }) async {
136 9 : return await _remoteAdapter.delete(
137 : this,
138 : remote: remote,
139 : params: params,
140 : headers: headers,
141 : onSuccess: onSuccess,
142 : onError: onError,
143 : );
144 : }
145 :
146 : /// Reload this model through a call equivalent to [Repository.findOne].
147 : /// with the current object/[id]
148 1 : Future<T?> reload({
149 : bool? remote,
150 : Map<String, dynamic>? params,
151 : Map<String, String>? headers,
152 : bool? background,
153 : DataRequestLabel? label,
154 : }) async {
155 3 : return await _remoteAdapter.findOne(
156 : this,
157 : remote: remote,
158 : params: params,
159 : headers: headers,
160 : background: background,
161 : label: label,
162 : );
163 : }
164 :
165 : // locals
166 :
167 : /// Saves this model to local storage.
168 40 : T saveLocal() => _remoteAdapter.saveLocal(_this);
169 :
170 : /// Deletes this model from local storage.
171 4 : void deleteLocal() => _remoteAdapter.deleteLocal(_this);
172 :
173 : /// Reload model from local storage.
174 5 : T? reloadLocal() => _remoteAdapter.localAdapter.findOne(_key);
175 : }
|