DEV Community

Cover image for Introducing Pure DI: Lightweight, Dependency Injection for Dart Without Compromise
Suhail
Suhail

Posted on

Introducing Pure DI: Lightweight, Dependency Injection for Dart Without Compromise

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
Enter fullscreen mode Exit fullscreen mode

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();
}


Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

When you call locator.dispose() or locator.unregister(), the dispose method is automatically called.

Real-World Use Cases

  1. 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.
  2. Server-Side Dart: Great for creating web servers, handling API requests with scoped dependencies, database connections, and efficient resource management.
  3. 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}');
  }
}
Enter fullscreen mode Exit fullscreen mode

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'));
  });
}
Enter fullscreen mode Exit fullscreen mode

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)