dependy 1.2.0 dependy: ^1.2.0 copied to clipboard
Dependy is a lightweight and flexible dependency injection (DI) library for Dart. It simplifies the management of services and their dependencies.
Dependy - A Simple Dependency Injection Library for Dart #
Contents #
- About
- Installation
- Core Concepts
- Disposing Providers and Modules
- Purpose of Keys
- Scopes
- Exceptions
- Examples
- MIT License
About #
Dependy is a lightweight and flexible dependency injection (DI) library for Dart. It simplifies managing services and their dependencies, making our code more modular, maintainable, and testable. Dependy supports hierarchical modules, dependency tracking, and circular dependency detection. It also supports eager or lazy/asynchronous initialization, allowing services to be loaded efficiently when needed.
Key Features: #
- Provider-based DI: Define how each service (provider) is created and manage dependencies between them.
- Support for modules: Structure your providers into modules to improve separation of concerns.
- Circular dependency detection: Avoid infinite loops by tracking dependencies.
- Async initialization: Initialize services asynchronously.
- Eager initialization: Initialize and prepare all services eagerly.
Installation #
Add dependy
to your pubspec.yaml
:
dependencies:
dependy: ^1.2.0
Then, run:
dart pub get
Import dependy
in your Dart files:
import 'package:dependy/dependy.dart';
Core Concepts #
DependyProvider #
A DependyProvider
is responsible for providing instances of a specific service. It manages how a service or object
is created, its dependencies, and optionally how it is disposed when no longer needed.
Each provider:
- Requires a factory function that defines how the instance of an object is created.
- Optionally takes a set of dependencies (other services that it relies on).
- Handles cleanup when the provider is disposed using a dispose method.
Example of a provider for a LoggerService
:
DependyProvider<LoggerService>((_) => LoggerService());
You can use providers to manage services, singletons, and objects that require dependencies. For example, if a service
A
requires service B
, you can specify this dependency through dependsOn
in DependyProvider
.
DependyModule #
A DependyModule
is a collection of DependyProvider
instances. It serves as a container for managing the
lifecycle and resolution of dependencies. A module can also contain other modules, allowing you to organize your
services hierarchically.
Modules help you:
- Group related services (e.g., authentication services, database services).
- Avoid tight coupling between components by abstracting how services are resolved.
- Handle complex applications with multiple modules, each responsible for different functionality.
Example of a simple module:
final loggingService = DependyModule(
providers: {
DependyProvider<LoggerService>(
(_) => LoggerService(),
),
},
);
Use Cases #
- Single Service: A module can manage a single service that can be used throughout the application (e.g.,
LoggerService
). - Multiple Services: A module can manage several unrelated services, allowing you to inject them where necessary (
e.g.,
LoggerService
andApiService
). - Services with Dependencies: Providers can depend on each other (e.g.,
ApiService
depends onLoggerService
, andLoggerService
depends onConfigService
). - Hierarchical Modules: Large applications can be structured into multiple modules that handle different domains ( e.g., database services, API services, etc.).
Disposing Providers and Modules #
You can dispose of all the providers in a DependyModule
by calling the dispose
method. This method will invoke the dispose
function on each DependyProvider
within the module.
Disposing Submodules
If you want to also dispose of all submodules along with their providers, you can use the dispose
method with the named argument disposeSubmodules
set to true
.
Example
Here’s how to use the dispose
method in a module:
final loggingModule = DependyModule(
providers: {
DependyProvider<LoggerService>(
(_) => LoggerService(),
),
},
);
// When you are done with the module, dispose of its providers
// provide disposeSubmodules: true o also dispose of submodules (default: false)
loggingModule.dispose(disposeSubmodules: true);
Purpose of Keys #
Keys provide a way to identify specific providers and modules during development. When an exception occurs or when you log information, the key associated with a provider or module can help you trace back to the source of the issue. This makes it easier to pinpoint where things went wrong.
Example Usage #
When creating a provider, you can specify a key like this:
DependyProvider<MyService>(
(_) => MyService(),
key: 'my_service_key',
);
Scopes #
A scope defines the lifetime of a service or object. Scopes determine how long a service stays in memory and when it should be disposed of. Services can either be short-lived or long-lived, depending on the scope they are placed in. Here's a breakdown:
-
Singleton Scope:
- Services in this scope live as long as the application is running.
- They are created once and shared across the entire app.
- Perfect for services that need to persist throughout the app's lifecycle, such as logging, authentication, or configuration services.
-
Scoped Scope:
- Services in this scope are temporary and exist only for a short time.
- These services are disposed of when they are no longer needed.
- Useful for services that are tied to a specific part of your app, like a screen, a form, or a session.
Example
// region Logger
abstract class LoggerService {
void log(String message);
}
class ConsoleLoggerService extends LoggerService {
@override
void log(String message) {
print('[Logger]: $message');
}
}
// endregion
// region Database
abstract class DatabaseService {
void connect();
void close();
}
class SqlDatabaseService extends DatabaseService {
final LoggerService _logger;
SqlDatabaseService(this._logger);
@override
void connect() {
_logger.log('Connected to Sql Database');
}
@override
void close() {
_logger.log('Closed Sql Database');
}
}
class SqliteDatabaseService extends DatabaseService {
final LoggerService _logger;
SqliteDatabaseService(this._logger);
@override
void connect() {
_logger.log('Connected to Sqlite Database');
}
@override
void close() {
_logger.log('Closed Sqlite Database');
}
}
// endregion
// region Api
abstract class ApiService {
void fetchData();
void dispose();
}
class ApiServiceImpl extends ApiService {
final DatabaseService _db;
final LoggerService _logger;
ApiServiceImpl(this._db, this._logger);
@override
void fetchData() {
_logger.log('Fetching data from API...');
_db.connect();
}
@override
void dispose() {
_logger.log('Disposing $this');
}
}
class MockApiService extends ApiService {
final DatabaseService _db;
final LoggerService _logger;
MockApiService(this._db, this._logger);
@override
void fetchData() {
_logger.log('Mocking API data...');
_db.connect();
}
@override
void dispose() {
_logger.log('Disposing $this');
}
}
// endregion
// Singleton module: provides services that live across the application
final singletonModule = DependyModule(
key: 'singleton_module',
providers: {
DependyProvider<LoggerService>(
(resolve) => ConsoleLoggerService(),
key: 'singleton_logger_service',
),
DependyProvider<DatabaseService>(
(dependy) async {
final logger = await dependy<LoggerService>();
return SqlDatabaseService(logger);
},
key: 'singleton_database_service',
dependsOn: {
LoggerService,
},
),
DependyProvider<ApiService>(
(dependy) async {
final databaseService = await dependy<DatabaseService>();
final logger = await dependy<LoggerService>();
return ApiServiceImpl(databaseService, logger);
},
key: 'singleton_api_service',
dependsOn: {
DatabaseService,
LoggerService,
},
),
},
);
// Scoped module: provides services that live temporarily and are disposed when done
final scopedModule = DependyModule(
key: 'scoped_module',
providers: {
// Here we declare a different implementation of DatabaseService [SqliteDatabaseService]
// for scoped usage. Scoped services are designed to be used in a temporary context.
DependyProvider<DatabaseService>(
(dependy) async {
final logger = await dependy<LoggerService>();
return SqliteDatabaseService(logger);
},
key: 'scoped_database_service',
dependsOn: {
LoggerService,
},
dispose: (database) {
// Close the database connections when the [DependyProvider] is disposed.
database?.close();
},
),
// A different implementation of ApiService (MockApiService) is provided.
DependyProvider<ApiService>(
(dependy) async {
// will resolve to [SqliteDatabaseService]
final database = await dependy<DatabaseService>();
// will resolve from [singletonModule]
final logger = await dependy<LoggerService>();
return MockApiService(database, logger);
},
dependsOn: {
DatabaseService,
LoggerService,
},
dispose: (api) {
// Dispose the api service when the [DependyProvider] is disposed.
api?.dispose();
},
),
},
modules: {
singletonModule,
// Makes available all of its providers to the [scopedModule]
},
);
void main() async {
print('=== Scoped Module Usage ===');
final scopedApiService = await scopedModule<ApiService>();
scopedApiService.fetchData();
scopedModule.dispose(); // Disposes all services in the [scopedModule]
// Result:
// [Logger]: Mocking API data...
// [Logger]: Connected to Sqlite Database
// [Logger]: Closed Sqlite Database
// [Logger]: Disposing Instance of 'MockApiService'
// Demonstrating the singleton behavior (persistent services)
print('\n=== Singleton Module Usage After Scoped Module Disposed ===');
final singletonApiService = await singletonModule<ApiService>();
singletonApiService.fetchData();
// Result:
// [Logger]: Fetching data from API...
// [Logger]: Connected to Sql Database
}
Exceptions #
Dependy can throw several exceptions during its operation. Understanding when these exceptions occur will help you handle errors gracefully in your application.
DependyCircularDependencyException
#
This exception is thrown when there is a circular dependency in the provider graph. A circular dependency occurs when two or more providers depend on each other directly or indirectly, leading to an infinite loop during resolution.
Example: If Service-A depends on Service-B, Service-B depends on Service-C, and Service-C depends on Service-A. This indirect chain of dependencies leads to an infinite loop during resolution.
DependyModuleDisposedException
#
This exception is thrown when you attempt to resolve a provider from a DependyModule
that has already been disposed. Once a module is disposed, it can no longer provide instances of its services.
Example: If you call module<SomeService>()
after calling module.dispose()
, a DependyModuleDisposedException
will be raised.
DependyProviderNotFoundException
#
This exception occurs when you try to resolve a provider for a specific type, but no provider is registered for that type within the module or its submodules.
Example: If you attempt to get an instance of Provider C but it has not been defined in the current module or any of its submodules, a DependyProviderNotFoundException
will be thrown.
DependyProviderMissingDependsOnException
#
This exception is thrown when a provider tries to resolve a dependency that has not been declared in its dependsOn
set. This verifies that all dependencies are explicitly defined, promoting better design and avoiding runtime errors.
Example: If Provider D requires Provider E, but Provider E is not listed in the dependsOn
set of Provider D, a DependyProviderMissingDependsOnException
will occur.
DependyProviderDisposedException
#
This exception is thrown when you try to use a provider that has already been disposed. Once a provider is disposed, it cannot be used anymore.
Example: If you call <module><T>()
on a disposed provider, a DependyProviderDisposedException
will be raised.
DependyDuplicateProviderException
#
This exception is thrown when there is more than one provider of the same type registered within a single module.
Example:
final databaseModule = DependyModule(
providers: {
DependyProvider<DatabaseService>(
(_) => SqlDatabaseService(),
),
DependyProvider<DatabaseService>(
(_) => SqliteDatabaseService(),
dependsOn: {},
),
},
);
In this example, both SqlDatabaseService
and SqliteDatabaseService
are registered as providers for the DatabaseService
type within the same module.
Examples #
Simple Counter Service #
In this example, we use a single service called CounterService
, which provides a simple counter that increments on
each call.
class CounterService {
int _count = 0;
int increment() => ++_count;
}
final dependy = DependyModule(
providers: {
DependyProvider<CounterService>(
(_) => CounterService(),
),
},
);
void main() async {
final counterService = await dependy<CounterService>();
print('Initial Count: ${counterService.increment()}');
print('After Increment: ${counterService.increment()}');
}
Two Independent Services #
In this example, we have two independent services: LoggerService
for logging and MathService
for performing a
mathematical operation.
class LoggerService {
void log(String message) {
print('Log: $message');
}
}
class MathService {
int square(int number) => number * number;
}
final dependy = DependyModule(
providers: {
DependyProvider<LoggerService>(
(_) => LoggerService(),
),
DependyProvider<MathService>(
(_) => MathService(),
),
},
);
void main() async {
final loggerService = await dependy<LoggerService>();
final mathService = await dependy<MathService>();
loggerService.log('Calculating square of 4: ${mathService.square(4)}');
}
Services with Dependencies #
Here, the CalculatorService
depends on LoggerService
, and LoggerService
depends on ConfigService
. Each
dependency is resolved in the correct order.
import 'package:dependy/dependy.dart';
class ConfigService {
final String appName = 'DependyExample';
}
class LoggerService {
final ConfigService _config;
LoggerService(this._config);
void log(String message) {
print('${_config.appName}: $message');
}
}
class CalculatorService {
final LoggerService _logger;
CalculatorService(this._logger);
int multiply(int a, int b) {
_logger.log('Multiplying $a and $b');
return a * b;
}
}
final dependy = DependyModule(
providers: {
DependyProvider<ConfigService>(
(_) => ConfigService(),
),
DependyProvider<LoggerService>(
(dependy) async {
final configService = await dependy<ConfigService>();
return LoggerService(configService);
},
dependsOn: {
ConfigService,
},
),
DependyProvider<CalculatorService>(
(dependy) async {
final loggerService = await dependy<LoggerService>();
return CalculatorService(loggerService);
},
dependsOn: {
LoggerService,
},
),
},
);
void main() async {
final calculatorService = await dependy<CalculatorService>();
print('Result: ${calculatorService.multiply(3, 5)}');
}
Multiple Modules with Dependencies #
In this example, we organize services into two modules: one for database and API services, and another for authentication and payment services. Each module manages its own set of dependencies.
class DatabaseService {
void connect() => print('Connected to Database');
}
class ApiService {
final DatabaseService _db;
ApiService(this._db);
void fetchData() {
print('Fetching data from API...');
_db.connect();
}
}
class AuthService {
void authenticate() => print('User authenticated');
}
class PaymentService {
final AuthService _auth;
PaymentService(this._auth);
void processPayment() {
_auth.authenticate();
print('Payment processed');
}
}
final module1 = DependyModule(
providers: {
DependyProvider<DatabaseService>(
(_) => DatabaseService(),
),
DependyProvider<ApiService>(
(dependy) async {
final databaseService = await dependy<DatabaseService>();
return ApiService(databaseService);
},
dependsOn: {
DatabaseService,
},
),
},
);
final module2 = DependyModule(
providers: {
DependyProvider<AuthService>(
(_) => AuthService(),
),
DependyProvider<PaymentService>(
(dependy) async {
final authService = await dependy<AuthService>();
return PaymentService(authService);
},
dependsOn: {
AuthService,
},
),
},
);
final module3 = DependyModule(
providers: {
DependyProvider<AuthService>(
(_) => AuthService(),
),
DependyProvider<PaymentService>(
(dependy) async {
final authService = await dependy<AuthService>();
return PaymentService(authService);
},
dependsOn: {
AuthService,
},
),
},
);
final mainModule = DependyModule(
providers: {},
modules: {
module1,
module2,
},
);
void main() async {
final apiService = await mainModule<ApiService>();
final paymentService = await mainModule<PaymentService>();
apiService.fetchData();
paymentService.processPayment();
}
Eager initialization #
class ConfigService {
ConfigService(this.apiUrl);
final String apiUrl;
}
class LoggerService {
void log(String message) {
print("LOG: $message");
}
}
class DatabaseService {
DatabaseService(this.config, this.logger);
final ConfigService config;
final LoggerService logger;
void connect() {
logger.log("Connecting to database at ${config.apiUrl}");
}
}
class ApiService {
ApiService(this.config, this.logger);
final ConfigService config;
final LoggerService logger;
void fetchData() {
logger.log("Fetching data from API at ${config.apiUrl}");
}
}
void main() async {
final eagerModule = await DependyModule(
providers: {
DependyProvider<ConfigService>(
(resolve) async => ConfigService("https://api.example.com"),
),
DependyProvider<LoggerService>(
(resolve) async => LoggerService(),
),
DependyProvider<DatabaseService>(
(resolve) async => DatabaseService(
await resolve<ConfigService>(),
await resolve<LoggerService>(),
),
dependsOn: {ConfigService, LoggerService},
),
DependyProvider<ApiService>(
(resolve) async => ApiService(
await resolve<ConfigService>(),
await resolve<LoggerService>(),
),
dependsOn: {ConfigService, LoggerService},
),
},
).asEager(); // Convert to an EagerDependyModule using asEager extension
// Now, retrieve the services synchronously
final dbService = eagerModule<DatabaseService>();
dbService.connect();
final apiService = eagerModule<ApiService>();
apiService.fetchData();
}
MIT License #
MIT License
Copyright (c) [2024] [xeinebiu/dependy]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.