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