If you want to use Dependency Injection in your Android application, you’ll need to choose from several popular options: Dagger, Hilt, Koin and even something called Pure Dependency Injection (a.k.a. manual Dependency Injection). Given all these options, making an optimal choice, or even just a choice, becomes a challenging task. To assist you in navigating this landscape, in this article I’ll discuss the most popular approaches to dependency injection in Android and highlight their respective benefits and drawbacks.
Dependency Injection is one of the most powerful and beneficial architectural patterns in object-oriented design. Projects that use proper Dependency Injection are simpler to maintain, simpler to extend and simpler to reason about.
Fundamentally, Dependency Injection is an architectural pattern that separates your codebase into two disjoint sets of components: the “functional” set, which contains the core logic of your app, and the “construction” set, which is responsible for wiring everything inside your application together. I know this definition can be difficult to grasp, so let me explain it with an analogy.
Imagine a concert of a popular musical band. From the audience’s point of view, the band members are the ones who deliver what they came for. That’s indeed the case, but, before and during the performance, there are additional staff members who set up the stage, bring and check the equipment, manage the lighting, etc. These staff members operate behind the scenes and are invisible to the audience, but, nonetheless, they’re essential to making a great show. Can band members take care of everything themselves? Theoretically, if the venue isn’t too big, then yes. But it should be clear that, at some point, band members will need to offload most of the organizational responsibilities to someone else and concentrate on their craft.
Now let’s go back to Dependency Injection. The band is analogous to the “functional set” of components inside your application that contain the “core” logic. The other staff members, who set up and manage everything behind the scenes, are analogous to the “construction set” of components that wire your application together. Just like band members can theoretically handle the organizational stuff themselves, software projects can get away with having no construction set. However, not having this separation of concerns will lead to a messy codebase, and the bigger the codebase will grow, the messier it will get. We want to avoid this outcome, so we use Dependency Injection.
Dependency Injection Frameworks
Dependency injection frameworks are special libraries that assist you in implementing Dependency Injection in your projects. These frameworks utilize Convention over Configuration paradigm. This means that they support a predefined set of conventions, which, when you follow them, spare you quite a bit of coding. In other words: dependency injection frameworks are shortcuts to Dependency Injection.
When you adopt a specific dependency injection framework, you basically build your application according to that framework’s “template”, and can leverage various shortcuts that it provides. Different frameworks offer different templates and different utilities. These are the aspects that you need to understand when choosing a dependency injection framework for your project.
So, let’s discuss the most popular dependency injection frameworks for Android.
Dagger 2 framework, commonly referred to as just “Dagger”, is maintained by Google and, therefore, can be thought of as the “official” dependency injection framework for Android. Unsurprisingly, it’s also the most popular today.
Dagger offers a very impressive set of conventions and features which makes it exceptionally powerful and versatile tool. Unfortunately, it also makes Dagger the most complex dependency injection framework to learn and use. In addition, notoriously bad official documentation makes the situation even worse.
To support its advanced features, Dagger relies on compile time annotation processing and code generation. This means that, when you build your app, Dagger scans your codebase, identifies where you used its conventions, and then generates code that “does the magic” according to your specifications. If you made a mistake, Dagger will detect it and fail the build. Therefore, with Dagger, you get compile-time validation of your DI setup. This is a major benefit, of course, but it comes at a price of an increased build time.
- The “official” framework for Android by Google
- Most popular
- Biggest feature set
- Compile-time validation
- Build time overhead
- Poor official documentation
Hilt is relatively new dependency injection framework developed by Google. Strictly speaking, Hilt is not a full-fledged framework on its own, but a “wrapper” around Dagger, so it uses Dagger under the hood. The reason Google developed Hilt was to simplify the usage of Dagger in Android applications.
You can think about the relationship between Dagger and Hilt in the following manner: Dagger is extremely versatile and, therefore, the “template” it provides is very minimalist. This can be very handy if you need to accommodate some special requirements, but opens many possibilities for developers to make mistakes (and they do). But, since this level of versatility is not needed in most projects, Hilt comes with a more opinionated template that leaves developers less space for creativity and mistakes. That’s the deal.
In my opinion, Hilt’s template is indeed a very reasonable starting point for most Android projects. In addition, it has decent documentation, so, compared to Dagger, developers will probably have easier time using Hilt.
My only issue with Hilt is that it adds even more “magic” on top of Dagger. Therefore, most probably, Hilt adds even more build time overhead. Given the fact that long build times are the primary productivity killer one bigger Android projects, I find Hilt’s value proposition questionable.
- Provides better “template” compared to Dagger
- Less space for mistakes compared to Dagger
- Decent documentation
- Risk of additional build time overhead
- More difficult to accommodate special requirements
Koin is a relatively new dependency injection framework which had been developed to utilize the full power of Kotlin programming language. Unlike Dagger, it doesn’t use compile-time annotation processing and code generation. Therefore, it doesn’t have as much impact on build times. Koin doesn’t use runtime reflection either. Instead, Koin relies on Kotlin’s inline functions and reified type arguments to perform its “magic”.
Koin offers an optional reflection-based features that constitute further quality-of-life improvements for developers. However, runtime reflection can be relatively heavyweight, so it can make your app slower on users’ devices. This overhead is negligible in most cases, but it scales with app’s size and disproportionately affects lower-end devices. Therefore, if you opt for Koin’s optional reflective features, you run the risk of encountering performance problems down the road.
Unlike Dagger, Koin doesn’t provide compile-time validation of your dependency injection setup. Therefore, if you make mistakes, you can experience runtime errors and crashes. Frankly, I think this isn’t a big problem because most of these issues will be found during development or QA. Koin also provides a way to validate your dependency injection setup in a JUnit test, but this approach introduces additional overhead and complexity.
Koin loses to Dagger in terms of features and versatility, but compensates with simplicity and much better documentation.
- Almost no build time overhead
- Good documentation
- Good support
- Targets Kotlin Multiplatform
- No compile-time validation of your Dependency Injection setup
- Limited feature set compared to Dagger and Hilt (e.g. no ability to inject Activity objects into modules)
- Risk of user-facing performance issues if runtime reflection is used (optional)
- Can’t be used in Java-only projects
I’ve just learned that, even though Koin hadn’t used annotations before, it added support for annotations a while ago. These new annotations are very similar to what Dagger and other dependency injection frameworks use. Furthermore, Koin’s reflection-based features were deprecated in favor of Kotlin Symbol Processing (KSP) plugin. This means that we witness a pivot in Koin’s philosophy towards annotation processing and code generation. This will make Koin much more similar to Dagger and Hilt.
Pure Dependency Injection
The last approach to Dependency Injection in Android that I’ll describe is so-called “Pure” Dependency Injection. It’s also known as “manual” or “vanilla” Dependency Injection. This is the least popular approach on this list, which has a pretty bad reputation among developers. The curious thing about this reputation, though, is that when I ask developers who complain about Pure DI how many times they implemented it in real apps, the answer is always “zero”.
The main benefit of Pure Dependency Injection is that it doesn’t require any framework at all. Therefore, all the potential issues affecting the frameworks described above become irrelevant. If you use Pure DI, there is simply no way to ruin your build times or the application’s performance in production. In addition, since no framework is involved, there is no “magic”, no tricky APIs and no new versions. It’s just code.
The main drawback of Pure DI is that you need to implement it by hand, from scratch. Since there is no framework with a “template”, if you don’t know what’s your end goal, you’re dead in the waters. In the best case, you won’t know where to start. In the worst case, you’ll create some barely working monster, riddled with bugs, which will haunt all project’s developers for years to come.
Another drawback is that, well, Pure Dependency Injection is just code, so you’ll need to write all of it yourself.
Pure Dependency Injection’s benefits:
- No performance concerns
- Simple to understand and modify
Pure Dependency Injection’s drawbacks:
- The initial implementation requires special knowledge and skills
- More code to write than when using Dependency Injection frameworks
- Not “sexy” enough for many developers
So, what technique should you use? Here come some bad news… you’ll need to answer this question for yourself, based on your specific situation and preferences. All I can do now, aside from discussing the alternatives, is to share my own setup.
In most cases, I use Dagger. Plain Dagger, without Hilt. As I said, Hilt adds some decent conventions, but I just don’t need them. I’ve been working with Dagger for years, in tens of different projects, so I can set it up in a greenfield project in less than 30 minutes. In addition, I don’t use Jetpack ViewModels (and, luckily, most of my clients don’t use them either), so Hilt’s “magical” handling of ViewModels is of no interest to me. Therefore, Hilt’s benefits don’t justify the risk of additional build time overhead for me.
I don’t use Koin because I’m not sure that it’s a good long-term bet. Dagger is more popular in Android ecosystem, has longer track record and is maintained by Google. Dagger is also much more stable than Koin (which still goes through major API changes). I just can’t see a compelling reason to switch to Koin.
However, in light of Dagger’s effect on build times, for bigger projects I prefer to use Pure Dependency Injection. Big projects that already use Dagger, but face problems with build times, can also be refactored to Pure Dependency Injection.
Why just the big projects? Well, there is nothing wrong with using Pure Dependency Injection even in completely new projects, but I find Dagger’s “magic” very handy and it increases my productivity. That’s especially true at the initial stages of development, when you “learn the business domain” and constantly refactor your application. In this situation, Dagger’s “auto-wiring” capabilities allow me to go faster and explore more code configurations per unit of time. At later stages, when projects grow and become more “rigid”, the value of Dagger’s “magic” decreases, while the build time overhead increases. At some point, a case can be made for refactoring to Pure Dependency Injection.
Dependency Injection is a great architectural pattern and there are several popular approaches to implementing it in Android: Dagger, Hilt, Koin and Pure Dependency Injection. In this article I provided a high-level description of theese approaches and I hope that this will help you in making your own choice.