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