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