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 : ///If true, only one instance of the service will be created and shared for
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 other
34 : ///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 : ///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 2 : 'Service ${(T).toString()} 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 when
113 : ///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 : ///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 : ///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 container
229 : ///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 with
261 : ///[IocContainerBuilder.addAsync] or [IocContainerBuilder.addSingletonAsync]
262 : ///You can only use this on factories that return a Future<>.
263 : ///Warning: if the definition is singleton/scoped and the Future fails, the factory will never return a
264 : ///valid value, so use [getAsyncSafe] to ensure the container doesn't store
265 : ///failed singletons
266 2 : Future<T> getAsync<T>() async => get<Future<T>>();
267 :
268 : ///See [getAsync].
269 : ///Makes an async call by creating a temporary scoped container,
270 : ///attempting to make the async initialization and merging the result with the
271 : ///current container if there is success.
272 : ///
273 : ///Warning: allows reentrancy and does not do error handling.
274 : ///If you call this more than once in parallel it will create multiple
275 : ///Futures - i.e. make multiple async calls. You need to guard against this
276 : ///and perform retries on failure. Be aware that this may happen even if
277 : ///you only call this method in a single location in your app.
278 : ///You may need a an async lock.
279 1 : Future<T> getAsyncSafe<T>() async {
280 1 : final scope = scoped();
281 :
282 1 : final service = await scope.getAsync<T>();
283 :
284 1 : merge(scope);
285 :
286 : return service;
287 : }
288 :
289 : ///Merge the singletons or scope from a container into this container. This
290 : ///only moves singleton definitions by default, but you can override this
291 : ///with [mergeTest]
292 1 : void merge(
293 : IocContainer container, {
294 : bool overwrite = false,
295 : bool Function(
296 : Type type,
297 : ServiceDefinition<dynamic>? serviceDefinition,
298 : Object? singleton,
299 : )?
300 : mergeTest,
301 : }) {
302 3 : for (final key in container.singletons.keys.where(
303 : mergeTest != null
304 2 : ? (type) => mergeTest(
305 : type,
306 2 : serviceDefinitionsByType[type],
307 2 : container.singletons[type],
308 : )
309 4 : : (type) => serviceDefinitionsByType[type]?.isSingleton ?? false,
310 1 : )) {
311 : if (overwrite) {
312 4 : singletons[key] = container.singletons[key]!;
313 : } else {
314 5 : singletons.putIfAbsent(key, () => container.singletons[key]!);
315 : }
316 : }
317 : }
318 : }
|