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