modular_di 0.2.0
modular_di: ^0.2.0 copied to clipboard
A simple way to organize dependency injection using modules.
Modular DI (Dependency Injection)

A simple way to organize dependency injection using modules.
Explore the docs »
Report Bug
·
Request Feature
Hey there! 👋 This is a super simple and flexible way to organize your Flutter app into modules, with a dependency injection features thrown in.
Features #
- 🔌 Works with whatever router you love (Go Router, Auto Route, you name it!)
- 🎯 Keep your app tidy with a simple module system
- 💉 Easy dependency injection powered by auto_injector
- 🔄 Modules can talk to each other through imports
- 🚀 Modules load up and clean up smoothly
- 🎨 Drop in the ModuleWidget wherever you need it
- 🧪 Testing is a breeze with mock replacements
- 📊 Lightning-fast dependency resolution using directed acyclic graphs
Getting Started #
Add to your pubspec.yaml
:
dependencies:
modular_di: <last version>
Usage #
Creating a Module #
Create a module by extending the Module
class:
class UserModule extends Module {
@override
List<Type> imports = []; // Add other modules to import if needed
@override
FutureOr<void> registerBinds(InjectorRegister i) {
// Register your dependencies
i.addSingleton<UserRepository>(() => UserRepositoryImpl());
i.addSingleton<UserService>(() => UserServiceImpl());
}
}
Using ModulesManager #
Initialize and manage your modules using ModulesManager
:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
//Register and initialize modules
await ModulesManager.instance.initModules([
UserModule(),
AuthModule(),
]);
runApp(const MyApp());
}
Registering Modules separately
You can register modules in any order from anywhere, as long as you register them before initializing them.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Register CoreModule
ModulesManager.instance.registerModule(CoreModule());
// Register User and Auth modules
ModulesManager.instance.registerModules([
UserModule(),
AuthModule(),
]);
// Initialize all the registered modules
await ModulesManager.instance.initRegisteredModules();
runApp(const MyApp());
}
Accessing Dependencies #
Use ModuleWidget
to provide module access and Module.get<T>()
to retrieve dependencies:
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ModuleWidget<UserModule>(
child: Builder(
builder: (context) {
// Without listening to changes
final userService = Module.get<UserService>(context);
return UserContent(service: userService);
},
),
);
}
}
Accessing Current Module #
Use ModuleWidget
to provide module access and Module.of()
to retrieve dependencies:
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ModuleWidget<UserModule>(
child: Builder(
builder: (context) {
// Without listening to changes
final userService = Module.get<UserService>(context);
final userModule = Module.of(context);
final String module = userModule.runtimeType.toString();
return UserContent(service: userService);
},
),
);
}
}
Listening to Module Changes
It's recommended to use listen: true
when getting dependencies, especially if you're working with modules that might be reset or if you're using imported modules. This ensures your widget rebuilds when dependencies are updated:
class UserProfileWidget extends StatefulWidget {
@override
State<UserProfileWidget> createState() => _UserProfileWidgetState();
}
class _UserProfileWidgetState extends State<UserProfileWidget> {
late UserService _userService;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// With listening to changes - widget will rebuild when:
// 1. The current module is reset
// 2. Any imported module is disposed/reset
_userService = Module.get<UserService>(context, listen: true);
}
@override
Widget build(BuildContext context) {
return Text(_userService.username);
}
}
For example, if UserModule
imports AuthModule
, and you dispose AuthModule
using:
await ModulesManager.instance.disposeModule<AuthModule>();
Any widget using listen: true
with dependencies from UserModule
will automatically rebuild with the new dependencies.
Module Dependencies #
Modules can depend on other modules using the imports
property:
class ProfileModule extends Module {
@override
List<Type> imports = [UserModule]; // Import dependencies from UserModule
@override
FutureOr<void> registerBinds(InjectorRegister i) {
i.addSingleton<ProfileService>(ProfileServiceImpl.new);
}
}
Dependency Injection Types #
The package supports different types of dependency injection:
- Singleton:
addSingleton<T>()
- Creates a single instance that persists throughout the app - Lazy Singleton:
addLazySingleton<T>()
- Creates a singleton instance only when first requested - Factory:
add<T>()
- Creates a new instance each time it's requested - Instance:
addInstance<T>()
- Registers an existing instance - Replace:
replace<T>()
- Replaces an existing registration (Useful for testing)
Logging #
The package includes a built-in logging system that can be enabled/disabled as needed:
import 'package:modular_di/logger.dart';
// Enable logging (disabled by default)
Logger.enable();
// Disable logging
Logger.disable();
Logs will only be printed in debug mode, making it safe to leave logging code in production.
Want to see it in action? #
Check out our example to see how it all comes together!
Want to help? #
Got ideas? Found a bug? We'd love your help! Feel free to open a PR and join the fun.
Legal stuff #
This project uses the MIT License - check out LICENSE if you're into that kind of thing.