Easy 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:
flutter_easy_di: <last version>
Usage
Creating a Module
Create a module by extending the EasyModule
class:
class UserModule extends EasyModule {
@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());
}
}
Initializing Modules
Initialize and manage your modules using EasyDI.initModules()
:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
//Register and initialize modules
await EasyDI.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
EasyDI.registerModules([CoreModule()]);
// Register User and Auth modules
EasyDI.registerModules([
UserModule(),
AuthModule(),
]);
// Initialize all the registered modules
await EasyDI.initRegisteredModules();
runApp(const MyApp());
}
Accessing Dependencies
Use EasyModuleWidget
to provide module access and EasyDI.get<T>()
to retrieve dependencies:
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return EasyModuleWidget<UserModule>(
// Use builder to create a new context with the module available. This is not needed if your EasyModuleWidget is not in the same widget as the EasyDI.get<T>()
child: Builder(
builder: (context) {
// Without listening to changes
final userService = EasyDI.get<UserService>(context);
return UserContent(service: userService);
},
),
);
}
}
Accessing Current Module
Use EasyModuleWidget
to provide module access and EasyModule.of()
to get the current module instance:
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 = EasyDI.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 EasyDI.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 EasyModule {
@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:flutter_easy_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.