Syringe ๐
Syringe is a lightweight, robust, and type-safe dependency injection (DI) library for Dart applications. ๐
Features โจ
- Flexible Dependency Types: Support for synchronous (
SyncDependency), asynchronous (AsyncDependency), and factory (FactoryDependency) dependencies. ๐ ๏ธ - Service Locator & Locator API: Clean resolution of dependencies using typed locators. ๐
- Hierarchical Scopes: Support for parent and child injectors with scoping and inheritance. ๐ณ
- Module Visibility: Control dependency visibility using
exported(public across scopes) andprivate(local to the injector) bindings. ๐ - Dynamic Factory Creation: Create new instances on-demand using
FactoryDependency. โก - Cyclic Dependency Detection: Automatically detect and prevent circular dependencies during resolution. ๐
- Progress Tracking: Monitor injection phases and progress via
onProgresscallbacks. ๐ - Resource Cleanup: Easily dispose resolved instances and manage lifecycles with
dispose(). ๐งน - Testability: Decouple components and make unit testing effortless. ๐งช
Installation ๐ฆ
Add Syringe to your pubspec.yaml:
dependencies:
syringe: ^0.9.8
Then run dart pub get. ๐ฅ
Usage ๐ก
1. Basic & Asynchronous Dependencies
import 'dart:async';
import 'package:syringe/syringe.dart';
// Define an asynchronous dependency
final class BarDependency extends AsyncDependency<Bar> {
@override
Future<Bar> onInject(Locator locator) async {
await Future.delayed(Duration(milliseconds: 500));
return Bar(title: 'Hello from Syringe');
}
}
// Define a synchronous dependency depending on Bar
final class FooDependency extends SyncDependency<Foo> {
@override
List<Type> get dependencies => [BarDependency];
@override
Foo onInject(Locator locator) {
// Resolve dependency using type-safe locator
final bar = locator.value<BarDependency, Bar>();
return Foo(bar: bar);
}
}
void main() async {
final injector = Injector<Foo>(
bindings: [
Binding.exported(BarDependency()),
Binding.exported(FooDependency()),
],
onProgress: (info) {
print('[${info.percent}%] ${info.type} - ${info.phase.name}');
},
onInject: (locator) {
return locator.value<FooDependency, Foo>();
},
);
final foo = await injector.inject();
print('Result: ${foo.bar.title}');
// Clean up resources
await injector.dispose();
}
class Bar {
final String title;
Bar({required this.title});
}
class Foo {
final Bar bar;
Foo({required this.bar});
}
2. Hierarchical Scopes (Parent & Child Injectors)
Syringe allows you to structure dependencies hierarchically, where child injectors can access exported bindings from their parent injector.
void main() async {
// 1. Parent Scope
final parentInjector = Injector<void>(
bindings: [
Binding.exported(BarDependency(), name: 'ParentBar'),
],
onInject: (_) {},
);
await parentInjector.inject();
// 2. Child Scope depending on Parent
final childInjector = Injector<Foo>(
parent: parentInjector,
bindings: [
Binding.exported(FooDependency()),
],
onInject: (locator) {
return locator.value<FooDependency, Foo>();
},
);
final foo = await childInjector.inject();
print('Foo bar title from parent: ${foo.bar.title}');
// Dispose scopes in order
await childInjector.dispose();
await parentInjector.dispose();
}
3. Factory Dependencies (FactoryDependency)
If you need a new transient instance every time a dependency is requested (such as ViewModels or Presenters that depend on shared services), use FactoryDependency:
// A shared singleton repository
final class UserRepositoryDep extends SyncDependency<UserRepository> {
@override
UserRepository onInject(Locator locator) => UserRepository();
}
// A factory that resolves the repository and creates a new ViewModel on demand
final class UserViewModelFactory extends FactoryDependency<UserViewModel> {
@override
List<Type> get dependencies => [UserRepositoryDep];
@override
UserViewModel onInject(Locator locator) {
// Inject the shared repository from the locator
final repository = locator.value<UserRepositoryDep, UserRepository>();
return UserViewModel(repository: repository);
}
}
void main() async {
final injector = Injector<List<UserViewModel>>(
bindings: [
Binding.exported(UserRepositoryDep()),
Binding.exported(UserViewModelFactory()),
],
onInject: (locator) {
// Each call to factory() creates a new UserViewModel instance sharing the same repository
final vm1 = locator.factory<UserViewModelFactory, UserViewModel>();
final vm2 = locator.factory<UserViewModelFactory, UserViewModel>();
// vm1 and vm2 are distinct instances, but share the same UserRepository
print(identical(vm1, vm2)); // false
print(identical(vm1.repository, vm2.repository)); // true
return [vm1, vm2];
},
);
await injector.inject();
}
class UserRepository {}
class UserViewModel {
final UserRepository repository;
UserViewModel({required this.repository});
}
Visibility Rules ๐
Binding.exported(...): Makes the dependency public. It can be accessed by child injectors when passed as a parent.Binding.private(...): Restricts the dependency to the local injector scope. Private dependencies cannot be accessed externally or by child scopes.
Error Handling โ ๏ธ
Syringe provides descriptive error classes for robust debugging:
SyringeCycleDependencyError: Thrown when a circular dependency is detected.SyringeModuleDuplicateError: Thrown when duplicate dependency registrations occur.SyringeUnknownDependencyError: Thrown when an unregistered dependency is requested.
Contributing ๐ค
Contributions, issues, and feature requests are welcome! Feel free to check the repository.
License ๐
This project is licensed under the terms of the MIT License.