GetIt Modular 🧩
A simple and powerful modular architecture for Flutter that provides scoped dependency injection on top of get_it. It does one thing and does it well, without mixing in state management or navigation.
Note: If you use the AutoRoute package for navigation, check out our companion package
get_it_modular_with_auto_route. It provides seamless integration between GetIt Modular and AutoRoute, making route scoping even easier.
Philosophy
The Flutter ecosystem has amazing tools for dependency injection (get_it), state management (bloc, provider, etc.), and navigation (auto_route, go_router).
However, many "modular" solutions try to bundle these separate concerns together. This can lead to a lack of flexibility and a tightly coupled architecture.
get_it_modular is different. It believes these concerns should remain separate. Its only job is to manage the lifecycle of your dependencies within a defined scope, leaving you free to choose your favorite packages for everything else.
Core Concepts
ModuleContract: This is the blueprint for your feature. You extend this abstract class to define all the dependencies (BLoCs, Repositories, UseCases) that your feature needs. It automatically manages its own state and the lifecycle of its child dependencies.ModuleScope: This is aStatefulWidgetthat brings your module to life. You wrap your feature's routes withModuleScopeto automatically load dependencies when the user enters the feature and unload them when they leave.
Installation
Add get_it_modular to your pubspec.yaml file:
dependencies:
get_it_modular: ^1.0.0
Getting Started
Here’s how to set up a feature module in just 3 steps.
Step 1: Define Your Module
Create a class that extends ModuleContract. Inside registerDependencies, use the built-in helpers to register your BLoCs, Repositories, etc.
// lib/features/events/event_module.dart
import 'package:get_it_modular/get_it_modular.dart';
import 'package:my_app/features/events/bloc/event_bloc.dart';
import 'package.my_app/features/events/data/event_repository.dart';
class EventModule extends ModuleContract {
@override
void registerDependencies() {
// These dependencies will only exist while the EventModule is active.
registerLazySingleton<EventRepository>(() => EventRepositoryImpl());
registerFactory<EventBloc>(() => EventBloc(repository: GetIt.I.get()));
}
}
Step 2: Register Your Module as a Singleton
In your main app setup (e.g., main.dart), register your module itself as a lazy singleton. This allows ModuleScope to find it later.
// lib/main.dart
import 'package:get_it/get_it.dart';
import 'package:my_app/features/events/event_module.dart';
void setupDependencies() {
// Register your module itself as a singleton blueprint.
GetIt.I.registerLazySingleton(() => EventModule());
// ... register other global dependencies
}
Step 3: Scope Your Routes
Wrap your feature's routes with ModuleScope. The best way to do this is with a wrapper route in your navigation package. The example below uses auto_route.
// In your router (e.g., app_router.dart)
import 'package:auto_route/auto_route.dart';
import 'package:get_it_modular/get_it_modular.dart';
import 'package:my_app/features/events/event_module.dart';
// Create a simple wrapper widget for your module
@RoutePage()
class EventScopeWrapper extends StatelessWidget {
const EventScopeWrapper({super.key});
@override
Widget build(BuildContext context) {
// Note: The generic type <EventModule> is required!
return ModuleScope<EventModule>(child: const AutoRouter());
}
}
// In your router list
@AutoRouterConfig()
class AppRouter extends _$AppRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(
path: '/events',
page: EventScopeWrapper.page, // Use the wrapper
children: [
// These routes are now scoped! Their dependencies will be
// loaded and unloaded automatically.
AutoRoute(path: '', page: EventListRoute.page),
AutoRoute(path: ':id', page: EventDetailsRoute.page),
]
)
];
}
An Optional Pattern for App Initialization 🚀
For larger apps, organizing the initialization of all your modules can get complicated. We provide MainModuleContract and the WithModules mixin as an optional, opinionated pattern to help structure your app's startup logic.
1. Create a MainModule
First, create a core module for your app's global dependencies that extends MainModuleContract.
// lib/main_module.dart
import 'package:get_it_modular/get_it_modular.dart';
class MainModule extends MainModuleContract {
@override
void registerDependencies() {
// Register global services like an API Client, Auth Repository, etc.
registerLazySingleton<AuthRepository>(() => AuthRepositoryImpl());
}
}
2. Use the WithModules Mixin
In a central place, like your root Application widget or an initializer class, use the WithModules mixin to bring everything together.
// lib/main.dart or lib/app_initializer.dart
import 'package:get_it_modular/get_it_modular.dart';
class AppInitializer with WithModules {
@override
final MainModuleContract mainModule = MainModule();
Future<void> init() async {
// 1. Initialize global dependencies from the main module
await initializeMainModule();
// 2. Register all your feature modules
registerSubModule<EventModule>(EventModule());
registerSubModule<ProfileModule>(ProfileModule());
// 3. Now you can use the modules to configure other services,
// like collecting all routes for your router.
final allModules = [mainModule, ...childModules];
// appRouter.init(modules: allModules);
}
}
This pattern gives you a single, clear place to see and manage all modules in your application.
Developer Safety 🛡️
Forgetting to specify the generic type on ModuleScope (e.g., ModuleScope(child: ...) instead of ModuleScope<EventModule>(...)) can cause a silent but dangerous bug.
To prevent this, ModuleScope includes a built-in assert that will fail instantly in debug mode if the type is omitted, telling you exactly how to fix it. This makes the package safer for teams and prevents hard-to-find errors.
This package was born from a long, productive architectural discussion on a sunny afternoon in Guarapari, Brazil. ☀️ Happy coding!