Why Dependency Injection Matters in Dart and Flutter
Modern software development is increasingly modular. Applications grow ever more complex, and organizing code to allow for reusability, scalability, and testability has become a necessity rather than a luxury. Dependency Injection (DI) is a well-established pattern that helps manage dependencies between components, making code easier to maintain and extend.
Flutter developers may know packages like get_it or injectable, but these solutions often come with additional dependencies or are tightly coupled to the Flutter framework. For pure Dart applications—CLI tools, server-side backends, scripts—there hasn't been a lightweight, standalone solution. That’s why I created Pure DI, a minimal DI library built entirely in Dart, with no external dependencies whatsoever.
What Makes Pure DI Unique?
While designing Pure DI, my core goals were simplicity, zero dependency footprint, and maximum flexibility. Pure DI is not a framework; it’s a service locator and DI utility you can drop into any Dart project. Here are some highlights:
- Zero External Dependencies: No reliance on third-party libraries. You get complete control and minimal package size.
- Service Locator Pattern: Offers a streamlined API for registering and retrieving dependencies—both singletons and factories.
- Lazy Singletons: Create instances only when needed, saving memory and startup time.
- Scoped Services: Manage sets of dependencies in grouped contexts (e.g., isolate resources for a request or background job).
- Automatic Disposal: Clean up resources effortlessly with the Disposable interface.
- Full Type Safety: Errors are caught at compile time, not runtime, reducing bugs and refactoring risks.
- Cross-Platform Support: Use Pure DI in server, CLI, Flutter, and web applications.
Getting Started with Pure DI
Setting up Pure DI is easy.
Installation
Add this line to your pubspec.yaml file:
dependencies:
pure_di: ^0.0.3
Run dart pub get to fetch the package.
Registering Services
Here's a basic example:
import 'package:pure_di/pure_di.dart';
class DatabaseService {
void query(String sql) => print('Running: $sql');
}
class UserRepository {
final DatabaseService db;
UserRepository(this.db);
void getUser(String id) => db.query('SELECT * FROM users WHERE id = $id');
}
void main() {
// Register database as a singleton
locator.registerSingleton<DatabaseService>(DatabaseService());
// Register repository as a factory that depends on DatabaseService
locator.register<UserRepository>(() => UserRepository(locator.get<DatabaseService>()));
// Use your service
final repo = locator.get<UserRepository>();
repo.getUser('42');
// When finished, dispose all resources
locator.dispose();
}
Advanced Features
Lazy Singletons
Need a resource that is expensive to create? Pure DI supports lazy singletons out of the box.
locator.registerLazySingleton<AnalyticsService>(() {
print('Initializing Analytics');
return AnalyticsService();
});
Your service will only be instantiated when
locator.get<AnalyticsService>()
is called the first time.
Scoped Instances
Scopes are ideal for isolating resources, such as managing per-request state in a backend server.
final requestScope = locator.createScope('request-123');
requestScope.registerSingleton<SessionService>(SessionService());
// In this scope, you can register and resolve dependencies independently
final session = requestScope.get<SessionService>();
Dispose scopes when done:
dart
locator.disposeScope('request-123');
Automatic Disposal
Implement Pure DI's disposable interface to be cleaned up when unregistered or disposed:
class FileService implements Disposable {
final File file;
FileService(this.file);
@override
void dispose() => file.close();
}
When you call locator.dispose() or locator.unregister(), the dispose method is automatically called.
Real-World Use Cases
- CLI & Scripting: Pure DI is perfect for CLI tools and scripts where you want separation of concerns and simple DI without Flutter or web dependencies.
- Server-Side Dart: Great for creating web servers, handling API requests with scoped dependencies, database connections, and efficient resource management.
- Testable Code: Easily swap real services for mocks in unit and integration tests.
Best Practices
- Register at Startup: Register all your dependencies at the application entry point for clarity and maintainability.
- Favour Factories for Stateful Objects: Objects like HTTP clients or database connections often need fresh instances—use factories instead of singletons.
- Leverage Scopes: Use scopes for lifecycles tied to specific tasks, requests, or background jobs.
- Disposable Resources: Implement the Disposable interface on classes that manage streams, files, or other external resources.
Example Project Walkthrough
Suppose you’re building a Dart HTTP server. Here’s how Pure DI makes dependency management easy:
import 'package:pure_di/pure_di.dart';
void handleRequest(HttpRequest request) {
final reqScope = locator.createScope('req-${request.hashCode}');
reqScope.registerSingleton<RequestContext>(RequestContext(request));
reqScope.register<UserController>(
() => UserController(reqScope.get<RequestContext>())
);
try {
final controller = reqScope.get<UserController>();
controller.process();
} finally {
locator.disposeScope('req-${request.hashCode}');
}
}
Each request gets isolated, clean resources, which are cleaned up automatically.
How to Test with Pure DI
Testing is seamless. Reset your service locator on every test run for a clean environment:
import 'package:test/test.dart';
import 'package:pure_di/pure_di.dart';
void main() {
setUp(() => ServiceLocator.reset());
test('UserRepository fetches user', () {
locator.registerSingleton<DatabaseService>(MockDatabase());
locator.register<UserRepository>(() => UserRepository(locator.get<DatabaseService>()));
final repo = locator.get<UserRepository>();
expect(repo.getUser('1'), equals('user-details'));
});
}
Roadmap
Pure DI’s design is intentionally minimal—but future features may include:
- Named instance registration (instanceName)
- Asynchronous factory support (registerAsync())
- Lifecycle hooks for advanced initialization/disposal (onInit, onDispose)
- Optional diagnostics and runtime logging for development
Follow updates and contribute on GitHub.
Conclusion
If you’re looking for a clean, lightweight dependency injection solution for Dart, Pure DI delivers simplicity and power without the baggage of external dependencies. Whether you’re writing scripts, services, or scalable backend applications, give Pure DI a try. It’s open source, free, and ready for your next project.
Find Pure DI on pub.dev: https://pub.dev/packages/pure_di
Source & Issues: https://github.com/suhail7cb/pure_di
Made with ❤️ for the Dart community.
Top comments (0)