Easy DI (Dependency Injection)


Logo

A simple way to organize dependency injection using modules.
Explore the docs ยป

Report Bug ยท Request Feature


Version Pub Points

Pub Publisher



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.

This project uses the MIT License - check out LICENSE if you're into that kind of thing.