Flutter: Modularized Dependency Injection
Let’s say that you’re in the phase where you’re Maintainability of of your Flutter project is a crucial aspect to consider, so it is important to make sure your project is adhering for the best practices to ensure a suitable structure and code quality and maintain it at a level that is satisfactory.In that situation, Separation of Concerns , Encapsulation , coupling , and cohesion These are the aspects you’d like to control in the architecture you choose to build.
It’s better to choose physical separation instead of logical, i.e. break your project up into Dart/Flutter packages instead of simply grouping things into various directories. If you have a tiny Flutter app that is merely the logical separation, you’ll fail to transform the app into physical files that reflect the directory structure you have. This is often due to the fact that it’s easy to violate or ignore architectural restrictions even when there’s no physical separation.
Dependency Injection in modules? How?
It’s obvious that the precise architecture will depend on the project, team and knowledge. I’m not planning to talk about architecture within the context of this article, but instead focus on the way you can arrange Dependency Injection (DI) in the form of a modularized Flutter application.
Refactoring The CounterApp
It’s obvious that the precise architecture will depend on the project, team and knowledge. I’m not planning to talk about architecture within the context of this article, but instead focus on the way you can arrange Dependency Injection (DI) in the form of a modularized Flutter application.
Cross-Cutting Concerns
A cross-cutting concern package generally contains items that impact the entire program and is able to be utilized by all layers. I added DI abstractions in it.
The first is known as DI This interface is accountable for retrieving objects from the DI container.
abstract interface class DI {
T call({String? instanceName});
T get({String? instanceName});
T getWithParam(
P param, {
String? instanceName,
});
}
The other interface is called DIRegistrar and offers the API for registering dependencies within DI Containers. DI Container. This interface should be accessible only to the implementation and not to its abstraction as well as to the ModuleDependencies abstraction and implementation.
typedef FactoryFunc = T Function();
typedef FactoryWithParamFunc = T Function(P param);
typedef DisposingFunc = FutureOr Function(T instance);
abstract interface class DIRegistrar implements DI {
void registerFactory(
FactoryFunc factoryFunc, {
String? instanceName,
});
void registerFactoryWithParam(
FactoryWithParamFunc factoryFunc, {
String? instanceName,
});
void registerSingleton(
T instance, {
String? instanceName,
DisposingFunc? disposingFunc,
});
void registerLazySingleton(
FactoryFunc factoryFunc, {
String? instanceName,
DisposingFunc? disposingFunc,
});
}
The third part of the puzzle is ModuleDependencies. It must be implemented by all the modules that have dependencies.
abstract class ModuleDependencies {
Future register(DIRegistrar do);
Future runPostRegistrationActions(DIRegistrar do) => Future.value();
}
DI Abstractions Usage
The modules are designed to contain the implementation details and provide only the information essential.
For instance , the reason I included Flutter bloc state management into the presentation package is a design feature, which means it can be changed within this package without needing to alter any other packages in any way.
class PresentationModuleDependencies extends ModuleDependencies {
@override
Future register(DIRegistrar do) async {
di.registerFactory(
() => CounterCubit(di()),
);
}
}
With the data package, I chose to implement shared_preferences to maintain the status of the counter in between app starts. It is also “known” only to the data package.
class DataModuleDependencies extends ModuleDependencies {
@override
Future register(DIRegistrar do) async {
final sharedPreferences = await SharedPreferences.getInstance();
di.registerFactory(
() => SharedPreferencesCounterRepository(sharedPreferences),
);
}
}
The application will,naturally, be able to have transitive dependency upon shared_preferences and Flutter_bloc in the end since this is a given and is designed to be part of the base package that eventually combined everything into one artifact. e.g. ipa, apk.
In the initialization phase of our application look through all the installed modules and instruct that they should register their dependencies.
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
final de = GetIrDI();
final modules = [
DomainModuleDependencies(),
PresentationModuleDependencies(),
DataModuleDependencies(),
];
for (final module in modules) {
await module.register(di);
}
for (final module in modules) {
await module.runPostRegistrationActions(di);
}
runApp(App(di: di));
}
You might have been able to see the “GetItDI” program in the line of code below. The implementation is of DIRegistrar which I added to the application package. This implementation is based on get_it, which is the receive_it package. If you decide to change to a different DI Container it is possible to do it as simply as changing the design for DIRegistrar within the application layer, without impacting different packages.
The concept should be evident to you now however, you’re encouraged to look around this repository for the remaining pieces!
Table of Contents
Tags Cloud
Frequently Asked Questions (FAQs)
- Improved Code Organization: Modularization allows developers to organize code into smaller, more manageable modules, making it easier to understand and maintain.
- Flexible Dependency Management: By breaking the application into modules, developers can manage dependencies more granularly, allowing for easier updates, substitutions, and testing of individual components.
- Reduced Coupling: Modularized dependency injection reduces coupling between different parts of the application, making it easier to change or replace components without affecting other parts of the codebase.
- Scalability: As the application grows, modularization enables developers to add new features or modules without impacting existing code, promoting scalability and extensibility.
- get_it: A simple service locator for Dart and Flutter that allows for easy registration and retrieval of dependencies.
- provider: A popular state management library for Flutter that also provides dependency injection capabilities through its Provider class.
- injector: A lightweight dependency injection library for Dart and Flutter that supports modularization and lazy loading of dependencies.
- Identifying and defining separate modules or features within the application.
- Deciding on the scope and lifecycle of dependencies within each module.
- Using a dependency injection library or framework to register and provide dependencies within each module.
- Injecting dependencies into classes or widgets as needed using constructor injection or other DI patterns.
- Keeping modules small and focused on a single responsibility.
- Avoiding circular dependencies between modules.
- Using named or tagged dependencies to differentiate between similar services within a module.
- Testing each module in isolation to ensure that dependencies are correctly provided and injected.