modularity_core

pub package pub points

Core implementation of the Modularity framework, providing Dependency Injection (DI) container and Module Lifecycle management.

Features

  • SimpleBinder: A lightweight, platform-agnostic DI container
  • ModuleController: Manages module lifecycle (init, dispose, hot reload)
  • Scoped Injection: Hierarchical injectors with parent/child scopes
  • Import/Export System: Clean separation between public and private dependencies
  • Circular Dependency Detection: Prevents infinite loops in module graphs

Installation

dependencies:
  modularity_core: ^0.0.1

Usage

Defining a Module

import 'package:modularity_core/modularity_core.dart';

class AuthModule extends Module {
  @override
  void binds(Binder binder) {
    // Private dependencies (internal to module)
    binder.registerLazySingleton<AuthRepository>(() => AuthRepositoryImpl());
    binder.registerLazySingleton<AuthService>(
      () => AuthService(binder.get<AuthRepository>()),
    );
  }
  
  @override
  void exports(Binder binder) {
    // Public dependencies (available to importers)
    binder.registerLazySingleton<AuthService>(() => binder.get<AuthService>());
  }
}

Using ModuleController

void main() async {
  final registry = <ModuleRegistryKey, ModuleController>{};
  
  final controller = ModuleController(AuthModule());
  
  await controller.initialize(registry);
  
  final authService = controller.binder.get<AuthService>();
  
  // Cleanup
  await controller.dispose();
}

Module Imports

class ProfileModule extends Module {
  @override
  List<Module> get imports => [AuthModule()];
  
  @override
  List<Type> get expects => [AuthService]; // Validates imports
  
  @override
  void binds(Binder binder) {
    final authService = binder.get<AuthService>(); // From AuthModule
    binder.registerLazySingleton<ProfileService>(
      () => ProfileService(authService),
    );
  }
}

Lifecycle Hooks

class MyModule extends Module {
  @override
  void binds(Binder binder) { /* ... */ }
  
  @override
  Future<void> onInit() async {
    // Called after binds() and exports()
    print('Module initialized');
  }
  
  @override
  Future<void> onDispose() async {
    // Called on dispose
    print('Module disposed');
  }
}

Hot Reload Support

// Rebind all dependencies without recreating the controller
controller.hotReload();

Behind the scenes ModuleController switches the underlying binder into RegistrationStrategy.preserveExisting, so singleton instances survive the rebind while factories are refreshed with the latest code.

Scoped overrides

Use ModuleOverrideScope when you need to override dependencies for imported modules only during tests or hot reload:

final overrides = ModuleOverrideScope(children: {
  AuthModule: ModuleOverrideScope(
    selfOverrides: (binder) {
      binder.registerLazySingleton<AuthApi>(() => FakeAuthApi());
    },
  ),
});

await testModule(
  DashboardModule(),
  (module, binder) {
    // ...
  },
  overrideScope: overrides,
);

Overrides run after binds and before exports, and they are re-applied automatically during hot reload.

Interceptors

class LoggingInterceptor implements ModuleInterceptor {
  @override
  void onInit(Module module) => print('Init: ${module.runtimeType}');
  
  @override
  void onLoaded(Module module) => print('Loaded: ${module.runtimeType}');
  
  @override
  void onError(Module module, Object error) => print('Error: $error');
  
  @override
  void onDispose(Module module) => print('Disposed: ${module.runtimeType}');
}

final controller = ModuleController(
  MyModule(),
  interceptors: [LoggingInterceptor()],
);

SimpleBinder API

Method Description
registerFactory<T>() New instance each call
registerLazySingleton<T>() Single instance, created on first access
registerSingleton<T>() Single instance, created immediately
get<T>() Get dependency (throws if not found)
tryGet<T>() Get dependency (returns null if not found)
parent<T>() Get from parent scope only

License

MIT License - see LICENSE for details.

Libraries

modularity_core