Flutter: Modularized Dependency Injection

Pavel Sulimau
ITNEXT
Published in
4 min readMar 2, 2024

--

Modularization? Why?

Let’s say you’re already at the stage when the maintainability of your Flutter project is an important aspect for you, so you want to ensure you’re adhering to the best guidelines to achieve suitable architecture and code quality and keep it at the desired level.
So in that case separation of concerns, encapsulation, coupling, and cohesion are the things that you would like to govern in your chosen architecture.
It’s preferable to use physical separation over logical, i.e. divide your project into Dart/Flutter packages instead of merely grouping stuff into different directories. If you already have a not-tiny Flutter app with just logical separation, I bet you’ll likely fail to refactor it into physical packages that will mirror the directory structure you already have. This challenge often arises because it’s easy to breach or overlook architectural constraints when there is no physical separation.

Dependency Injection in modules? How?

It stands to reason that the exact architecture depends on your project, team, knowledge, etc. I’ve got no intention to discuss architectures in the scope of this article, but rather want to concentrate on how you can organize Dependency Injection (DI) in a modularized Flutter app.

Refactoring The CounterApp

To convey the concept, let’s take the Counter Flutter App and modify it by introducing some basic packages. The diagram below depicts the packages I’ve selected and how they depend on each other.

Cross-Cutting Concerns

A cross-cutting concerns package typically contains things that affect the entire application and can be used by all the layers. I put DI abstractions into it.

The first one is called DI and this interface is responsible for obtaining objects from a DI container.

The second interface is called DIRegistrar, it provides the API to register dependencies in a DI Container. This interface must be exposed only to its actual implementation and to the ModuleDependencies abstraction and implementations.

The third part of the puzzle is ModuleDependencies. It must be implemented by all the modules that have dependencies.

DI Abstractions Usage

The modules are supposed to encapsulate the implementation details and expose only what is necessary.

For example, the fact that I added flutter_bloc state management in the presentation package is an implementation detail, so it can be easily replaced just inside this package without the need to modify the other packages at all.

In the data package I decided to use shared_preferences to keep the state of the counter between the application launches. Again, this is “known” only to the data package itself.

The app, of course, will have the transitive dependencies on shared_preferences and flutter_bloc in the end, as this is inevitable and intended for the root package that eventually bundles everything together in an artifact, e.g. ipa, apk.

At the initialization phase of the application, we simply go through all the configured modules and tell them to register their dependencies.

You might’ve noticed the GetItDI class in the piece of code above. This is the implementation of DIRegistrar that I put in the app package. This implementation uses the get_it package. Again, should you want to switch to another DI Container, you’ll be able to do that as easily as just changing the implementation of DIRegistrar in the app layer without affecting all the other packages in any way.

Hopefully, the concept is clear to you by now, but you’re more than welcome to explore the repository to inspect the remaining bits!

Conclusion

By dividing your project into physical modules, you immediately benefit from the compiler and static analysis working in your favor. This makes it significantly more difficult to overlook any architectural violations as they arise. The overhead of managing a Monorepo is minor compared to the numerous benefits it offers. Additionally, it’s important to become familiar with the Melos tool, as it greatly simplifies Monorepo management in the Flutter and Dart ecosystem.

Resources

--

--