Line data Source code
1 : ///❌ An exception that occurs when the service is not found
2 : class ServiceNotFoundException<T> implements Exception {
3 : ///❌ Creates a new instance of [ServiceNotFoundException]
4 1 : const ServiceNotFoundException(this.message);
5 :
6 : ///The exception message
7 : final String message;
8 1 : @override
9 2 : String toString() => 'ServiceNotFoundException: $message';
10 : }
11 :
12 : ///📙 Defines a factory for the service and whether or not it is a singleton.
13 : class ServiceDefinition<T> {
14 : ///📙 Defines a factory for the service and whether or not it is a singleton.
15 2 : const ServiceDefinition(
16 : this.factory, {
17 : this.isSingleton = false,
18 : this.dispose,
19 : this.disposeAsync,
20 : }) : assert(
21 2 : !isSingleton || dispose == null,
22 : 'Singleton factories cannot have a dispose method',
23 : ),
24 : assert(
25 2 : dispose == null || disposeAsync == null,
26 : "Service definitions can't have both dispose and disposeAsync",
27 : );
28 :
29 : ///1️⃣ If true, only one instance of the service will be created and shared
30 : ///for the lifespan of the container.
31 : final bool isSingleton;
32 :
33 : ///🏭 The factory that creates the instance of the service and can access
34 : ///other services in this container
35 : final T Function(
36 : IocContainer container,
37 : ) factory;
38 :
39 : ///🗑️ The dispose method that is called when you dispose the scope
40 : final void Function(T service)? dispose;
41 :
42 : ///🗑️ The async dispose method that is called when you dispose the scope
43 : final Future<void> Function(T service)? disposeAsync;
44 :
45 3 : void _dispose(T instance) => dispose?.call(instance);
46 :
47 3 : Future<void> _disposeAsync(T instance) async => disposeAsync?.call(instance);
48 : }
49 :
50 : ///📦 A built Ioc Container. To create a new [IocContainer], use
51 : ///[IocContainerBuilder]. To get a service from the container, call
52 : ///[get], [getAsync], or [getAsyncSafe]
53 : ///Call [scoped] to get a scoped container
54 : class IocContainer {
55 : ///📦 Creates an IocContainer. You can build your own container by injecting
56 : ///service definitions and singletons here, but you should probably use
57 : ///[IocContainerBuilder] instead.
58 2 : const IocContainer(
59 : this.serviceDefinitionsByType,
60 : this.singletons, {
61 : this.isScoped = false,
62 : });
63 :
64 : ///📙 The service definitions by type
65 : final Map<Type, ServiceDefinition<dynamic>> serviceDefinitionsByType;
66 :
67 : ///1️⃣ Map of singletons or scoped services by type. This map is mutable
68 : ///so the container can store scope or singletons
69 : final Map<Type, Object> singletons;
70 :
71 : ///⌖ If true, this container is a scoped container. Scoped containers never
72 : ///create more than one instance of a service
73 : final bool isScoped;
74 :
75 : ///👐 Get an instance of the service by type
76 2 : T get<T extends Object>() {
77 4 : final serviceDefinition = serviceDefinitionsByType[T];
78 :
79 : if (serviceDefinition == null) {
80 1 : throw ServiceNotFoundException<T>(
81 1 : 'Service $T not found',
82 : );
83 : }
84 :
85 4 : if (serviceDefinition.isSingleton || isScoped) {
86 4 : final singletonValue = singletons[T];
87 :
88 : if (singletonValue != null) {
89 : return singletonValue as T;
90 : }
91 : }
92 :
93 4 : final service = serviceDefinition.factory(this) as T;
94 :
95 4 : if (serviceDefinition.isSingleton || isScoped) {
96 4 : singletons[T] = service;
97 : }
98 :
99 : return service;
100 : }
101 :
102 : ///👐 This is a shortcut for [get]
103 2 : T call<T extends Object>() => get<T>();
104 : }
105 :
106 : ///👷 A builder for creating an [IocContainer].
107 : class IocContainerBuilder {
108 : ///👷 Creates a container builder
109 2 : IocContainerBuilder({this.allowOverrides = false});
110 : final Map<Type, ServiceDefinition<dynamic>> _serviceDefinitionsByType = {};
111 :
112 : /// 🔃 Throw an error if a service is added more than once. Set this to true
113 : /// when you want to add mocks to set of services for a test.
114 : final bool allowOverrides;
115 :
116 : ///📙 Add a factory to the container.
117 2 : void addServiceDefinition<T>(
118 : ///Add a factory and whether or not this service is a singleton
119 : ServiceDefinition<T> serviceDefinition,
120 : ) {
121 4 : if (_serviceDefinitionsByType.containsKey(T)) {
122 1 : if (allowOverrides) {
123 2 : _serviceDefinitionsByType.remove(T);
124 : } else {
125 1 : throw Exception('Service already exists');
126 : }
127 : }
128 :
129 6 : _serviceDefinitionsByType.putIfAbsent(T, () => serviceDefinition);
130 : }
131 :
132 : ///📦 Create an [IocContainer] from the [IocContainerBuilder].
133 4 : IocContainer toContainer() => IocContainer(
134 2 : Map<Type, ServiceDefinition<dynamic>>.unmodifiable(
135 2 : _serviceDefinitionsByType,
136 : ),
137 2 : <Type, Object>{},
138 : );
139 :
140 : ///Add a singleton service to the container.
141 2 : void addSingletonService<T>(T service) => addServiceDefinition(
142 1 : ServiceDefinition<T>(
143 1 : (container) => service,
144 : isSingleton: true,
145 : ),
146 : );
147 :
148 : ///1️⃣ Add a singleton factory to the container. The container
149 : ///will only call this once throughout the lifespan of the container
150 2 : void addSingleton<T>(
151 : T Function(
152 : IocContainer container,
153 : )
154 : factory,
155 : ) =>
156 2 : addServiceDefinition<T>(
157 2 : ServiceDefinition<T>(
158 4 : (container) => factory(container),
159 : isSingleton: true,
160 : ),
161 : );
162 :
163 : ///🏭 Add a factory to the container.
164 2 : void add<T>(
165 : T Function(
166 : IocContainer container,
167 : )
168 : factory, {
169 : void Function(T service)? dispose,
170 : }) =>
171 2 : addServiceDefinition<T>(
172 2 : ServiceDefinition<T>(
173 4 : (container) => factory(container),
174 : dispose: dispose,
175 : ),
176 : );
177 :
178 : ///⌛ Adds an async [ServiceDefinition]
179 1 : void addAsync<T>(
180 : Future<T> Function(
181 : IocContainer container,
182 : )
183 : factory, {
184 : Future<void> Function(T service)? disposeAsync,
185 : }) =>
186 1 : addServiceDefinition<Future<T>>(
187 1 : ServiceDefinition<Future<T>>(
188 2 : (container) async => factory(container),
189 2 : disposeAsync: (service) async => disposeAsync?.call(await service),
190 : ),
191 : );
192 :
193 : ///1️⃣ ⌛ Add an async singleton factory to the container. The container
194 : ///will only call the factory once throughout the lifespan of the container
195 1 : void addSingletonAsync<T>(
196 : Future<T> Function(
197 : IocContainer container,
198 : )
199 : factory,
200 : ) =>
201 1 : addServiceDefinition<Future<T>>(
202 1 : ServiceDefinition<Future<T>>(
203 : isSingleton: true,
204 2 : (container) async => factory(container),
205 : ),
206 : );
207 : }
208 :
209 : ///Extensions for IocContainer
210 : extension IocContainerExtensions on IocContainer {
211 : ///🗑️ Dispose all singletons or scope. Warning: don't use this on your root
212 : ///container. You should only use this on scoped containers.
213 1 : Future<void> dispose() async {
214 1 : assert(isScoped, 'Only dispose scoped containers');
215 3 : for (final type in singletons.keys) {
216 : //Note: we don't need to check if the service is a singleton because
217 : //singleton service definitions never have dispose
218 2 : final serviceDefinition = serviceDefinitionsByType[type]!;
219 :
220 : //We can't do a null check here because if a Dart issue
221 4 : serviceDefinition._dispose.call(singletons[type]);
222 :
223 3 : await serviceDefinition._disposeAsync(singletons[type]);
224 : }
225 2 : singletons.clear();
226 : }
227 :
228 : ///🏁 Initalizes and stores each singleton in case you want a zealous
229 : ///container instead of a lazy one
230 1 : void initializeSingletons() {
231 3 : serviceDefinitionsByType.forEach((type, serviceDefinition) {
232 1 : if (serviceDefinition.isSingleton) {
233 2 : singletons.putIfAbsent(
234 : type,
235 3 : () => serviceDefinition.factory(
236 : this,
237 : ) as Object,
238 : );
239 : }
240 : });
241 : }
242 :
243 : ///⌖ Gets a service, but each service in the object mesh will have only one
244 : ///instance. If you want to get multiple scoped objects, call [scoped] to
245 : ///get a reusable [IocContainer] and then call [get] or [getAsync] on that.
246 3 : T getScoped<T extends Object>() => scoped().get<T>();
247 :
248 : ///⌖ Creates a new Ioc Container for a particular scope. Does not use existing
249 : ///singletons/scope by default. Warning: if you use the existing singletons,
250 : ///calling [dispose] will dispose those singletons
251 2 : IocContainer scoped({
252 : bool useExistingSingletons = false,
253 : }) =>
254 2 : IocContainer(
255 2 : serviceDefinitionsByType,
256 4 : useExistingSingletons ? Map<Type, Object>.from(singletons) : {},
257 : isScoped: true,
258 : );
259 :
260 : ///⌛ Gets a service that requires async initialization. Add these services
261 : ///with [IocContainerBuilder.addAsync] or
262 : ///[IocContainerBuilder.addSingletonAsync] You can only use this on factories
263 : ///that return a Future<>.
264 : ///Warning: if the definition is singleton/scoped and the Future fails, the factory will never return a
265 : ///valid value, so use [getAsyncSafe] to ensure the container doesn't store
266 : ///failed singletons
267 2 : Future<T> getAsync<T>() async => get<Future<T>>();
268 :
269 : ///See [getAsync].
270 : ///Makes an async call by creating a temporary scoped container,
271 : ///attempting to make the async initialization and merging the result with the
272 : ///current container if there is success.
273 : ///
274 : ///⚠️ Warning: allows reentrancy and does not do error handling.
275 : ///If you call this more than once in parallel it will create multiple
276 : ///Futures - i.e. make multiple async calls. You need to guard against this
277 : ///and perform retries on failure. Be aware that this may happen even if
278 : ///you only call this method in a single location in your app.
279 : ///You may need a an async lock.
280 1 : Future<T> getAsyncSafe<T>() async {
281 1 : final scope = scoped();
282 :
283 1 : final service = await scope.getAsync<T>();
284 :
285 1 : merge(scope);
286 :
287 : return service;
288 : }
289 :
290 : ///⛙ Merge the singletons or scope from a container into this container. This
291 : ///only moves singleton definitions by default, but you can override this
292 : ///with [mergeTest]
293 1 : void merge(
294 : IocContainer container, {
295 : bool overwrite = false,
296 : bool Function(
297 : Type type,
298 : ServiceDefinition<dynamic>? serviceDefinition,
299 : Object? singleton,
300 : )?
301 : mergeTest,
302 : }) {
303 3 : for (final key in container.singletons.keys.where(
304 : mergeTest != null
305 2 : ? (type) => mergeTest(
306 : type,
307 2 : serviceDefinitionsByType[type],
308 2 : container.singletons[type],
309 : )
310 4 : : (type) => serviceDefinitionsByType[type]?.isSingleton ?? false,
311 1 : )) {
312 : if (overwrite) {
313 4 : singletons[key] = container.singletons[key]!;
314 : } else {
315 5 : singletons.putIfAbsent(key, () => container.singletons[key]!);
316 : }
317 : }
318 : }
319 :
320 : ///✔️ Checks if an instance of the service already exists in the container.
321 : ///This will be true if the service is a singleton and has already been
322 : ///resolved once, or the container is scoped and the service has been
323 : ///resolved in the current scope.
324 3 : bool hasInstance<T extends Object>() => singletons.containsKey(T);
325 :
326 : ///➰ Recursively climbs through a hierarchy of containers until it finds an
327 : ///instance of the service, or calls [get] the last container returned by
328 : ///[nextParent] if the service does not already exist
329 1 : T fallback<T extends Object>(
330 : IocContainer? Function() nextParent,
331 : ) {
332 1 : if (hasInstance<T>()) {
333 1 : return get<T>();
334 : }
335 :
336 1 : final parent = nextParent();
337 1 : if (parent == null) return get<T>();
338 1 : return parent.hasInstance<T>()
339 1 : ? parent.get<T>()
340 1 : : parent.fallback(nextParent);
341 : }
342 : }
343 :
344 : extension IocContainersExtensions on List<IocContainer> {
345 : ///Iterates through the list of containers from the last element to the first
346 : /// and returns the first one that already has an instance of the service,
347 : /// or calls get<>() on the first container in the list
348 1 : T fallback<T extends Object>() {
349 2 : assert(length > 0, 'The list must have at least one element');
350 :
351 2 : if (length == 1) {
352 2 : return this[0].get<T>();
353 : }
354 :
355 4 : for (var i = length - 1; i >= 0; i--) {
356 2 : if (this[i].hasInstance<T>()) {
357 2 : return this[i].get<T>();
358 : }
359 : }
360 :
361 : // coverage:ignore-start
362 : //This shouldn't happen but the compiler doesn't know that
363 : return this[0].get<T>();
364 : // coverage:ignore-end
365 : }
366 : }
|