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 : // "late" finals
11 : String? _key;
12 : Map<String, RemoteAdapter>? _adapters;
13 :
14 : // computed
15 14 : String get _internalType => DataHelpers.getType<T>();
16 7 : RemoteAdapter<T> get remoteAdapter =>
17 21 : _adapters?[_internalType]! as RemoteAdapter<T>;
18 :
19 21 : bool get isInitialized => _key != null && _adapters != null;
20 :
21 : // initializers
22 :
23 7 : T _initialize(final Map<String, RemoteAdapter> adapters,
24 : {String? key, bool save = false}) {
25 7 : if (isInitialized) return this as T;
26 :
27 7 : _adapters = adapters;
28 :
29 49 : _key = remoteAdapter.graph.getKeyForId(remoteAdapter.internalType, id,
30 7 : keyIfAbsent: key ?? DataHelpers.generateKey<T>());
31 :
32 : if (save) {
33 28 : remoteAdapter.localAdapter.save(_key!, this as T);
34 : }
35 :
36 : // initialize relationships
37 : for (final metadata
38 34 : in remoteAdapter.localAdapter.relationshipsFor(this as T).entries) {
39 12 : final relationship = metadata.value['instance'] as Relationship?;
40 :
41 6 : relationship?.initialize(
42 : adapters: adapters,
43 : owner: this,
44 12 : name: metadata.value['name'] as String,
45 12 : inverseName: metadata.value['inverse'] as String?,
46 : );
47 : }
48 :
49 : return this as T;
50 : }
51 : }
52 :
53 : /// Extension that adds syntax-sugar to data classes,
54 : /// linking them to common [Repository] methods such as
55 : /// [save] and [delete].
56 : extension DataModelExtension<T extends DataModel<T>> on DataModel<T> {
57 : /// Initializes a model copying the identity of supplied [model].
58 : ///
59 : /// Usage:
60 : /// ```
61 : /// final post = await repository.findOne('1'); // returns initialized post
62 : /// final newPost = Post(title: 'test'); // uninitialized post
63 : /// newPost.was(post); // new is now initialized with same key as post
64 : /// ```
65 1 : T was(T model) {
66 : assert(model.isInitialized,
67 : 'Please initialize model before passing it to `was`');
68 3 : return _initialize(model._adapters!, key: model._key, save: true);
69 : }
70 :
71 : /// Saves this model through a call equivalent to [Repository.save].
72 : ///
73 : /// Usage: `await post.save()`, `author.save(remote: false, params: {'a': 'x'})`.
74 : ///
75 : /// **Requires this model to be initialized.**
76 2 : Future<T> save({
77 : bool? remote,
78 : Map<String, dynamic>? params,
79 : Map<String, String>? headers,
80 : OnData<T>? onSuccess,
81 : OnDataError<T>? onError,
82 : }) async {
83 1 : _assertInit('save');
84 3 : return await remoteAdapter.save(
85 : this as T,
86 : remote: remote,
87 : params: params,
88 : headers: headers,
89 : onSuccess: onSuccess,
90 : onError: onError,
91 : );
92 : }
93 :
94 : /// Deletes this model through a call equivalent to [Repository.delete].
95 : ///
96 : /// Usage: `await post.delete()`
97 : ///
98 : /// **Requires this model to be initialized.**
99 2 : Future<void> delete({
100 : bool? remote,
101 : Map<String, dynamic>? params,
102 : Map<String, String>? headers,
103 : OnData<void>? onSuccess,
104 : OnDataError<void>? onError,
105 : }) async {
106 1 : _assertInit('delete');
107 3 : await remoteAdapter.delete(
108 : this,
109 : remote: remote,
110 : params: params,
111 : headers: headers,
112 : onSuccess: onSuccess,
113 : onError: onError,
114 : );
115 : }
116 :
117 : /// Re-fetch this model through a call equivalent to [Repository.findOne].
118 : /// with the current object/[id]
119 : ///
120 : /// **Requires this model to be initialized.**
121 2 : Future<T?> reload({
122 : bool? remote,
123 : Map<String, dynamic>? params,
124 : Map<String, String>? headers,
125 : }) async {
126 1 : _assertInit('reload');
127 3 : return await remoteAdapter.findOne(
128 : this,
129 : remote: remote,
130 : params: params,
131 : headers: headers,
132 : );
133 : }
134 :
135 : /// Watch this model through a call equivalent to [Repository.watchOne].
136 : /// with the current object/[id].
137 : ///
138 : /// **Requires this model to be initialized.**
139 2 : DataStateNotifier<T?> watch({
140 : bool? remote,
141 : Map<String, dynamic>? params,
142 : Map<String, String>? headers,
143 : AlsoWatch<T>? alsoWatch,
144 : }) {
145 1 : _assertInit('watch');
146 2 : return remoteAdapter.watchOne(
147 : this,
148 : remote: remote,
149 : params: params,
150 : headers: headers,
151 : alsoWatch: alsoWatch,
152 : );
153 : }
154 :
155 1 : void _assertInit(String method) {
156 1 : if (isInitialized) {
157 : return;
158 : }
159 1 : throw AssertionError('''\n
160 : This model MUST be initialized in order to call `$method`.
161 :
162 : DON'T DO THIS:
163 :
164 2 : final ${_internalType.singularize()} = $T(...);
165 2 : ${_internalType.singularize()}.$method(...);
166 :
167 : DO THIS:
168 :
169 2 : final ${_internalType.singularize()} = $T(...).init(context.read);
170 2 : ${_internalType.singularize()}.$method(...);
171 :
172 : Call `init(context.read)` on the model first.
173 :
174 : This ONLY happens when a model is manually instantiated
175 : and had no contact with Flutter Data.
176 :
177 : Initializing models is not necessary in any other case.
178 :
179 : When assigning new models to a relationship, only initialize
180 : the actual model:
181 :
182 : Family(surname: 'Carlson', dogs: {Dog(name: 'Jerry'), Dog(name: 'Zoe')}.asHasMany)
183 : .init(context.read);
184 1 : ''');
185 : }
186 : }
187 :
188 : /// Returns a model's `_key` private attribute.
189 : ///
190 : /// Useful for testing, debugging or usage in [RemoteAdapter] subclasses.
191 2 : String? keyFor<T extends DataModel<T>>(T model) => model._key;
192 :
193 1 : @visibleForTesting
194 : @protected
195 : RemoteAdapter? adapterFor<T extends DataModel<T>>(T model) =>
196 1 : model.remoteAdapter;
|