About DI Frameworks

Mihaly Nagy
6 min readJun 2, 2021

--

For an Android developer there’s a number of dependency injection frameworks available, however, the most popular will always be the one marketed by Google (currently Dagger).

The importance of using a DI framework becomes apparent when we’re thinking about reusable components, testing components in isolation or decoupling dependencies.

Dependency Injection as a pattern is relatively straight forward, and it can be done manually: https://en.wikipedia.org/wiki/Dependency_injection

And because we rely on dependency injection so much, frameworks to make our code more readable were introduced.

This article will mention some of the options we have to introduce a DI framework in an Android project (keeping in mind we’re working with a Kotlin codebase), as well as my personal preference on the subject.

There’s no shortage of articles about how to use dependency injection frameworks, comparisons and benchmarks. Probably the most useful article is the official Android tutorial here:

https://developer.android.com/training/dependency-injection

Here are some other articles:

The most popular DI frameworks at this time are:

Although these frameworks have a lot of interesting and complex features, I somehow, always end up reusing the same ~18 lines of code in my personal projects.

Let’s look at what my personal requirement in terms of dependency injection are.

If say a class called LoginViewModel needs to use a UserSession class, I don’t want to care how it’s created I just want to use it so the code would look something like this:

I also want a way to define how this class will be created (i.e. create a binding for it). The way I imagine this would happen is:

If I want to reuse the same instance over the application lifecycle I would do something like:

Note that I have omitted <UserSession> because Kotlin type inference helps us and we only need the diamond(<>) when the lambda returns a subclass.

So how would someone implement all of this?

The private val userSession by inject<UserSession>() construct in Kotlin is called a delegated property (https://kotlinlang.org/docs/delegated-properties.html)

To implement a delegated property we have some options but we’re interested in a specific use case when it’s a read only property. And the code to achieve this is:

We have multiple ways of achieving property delegation, the code above makes sure of a couple of things:

  • when we access the field getValue is called
  • using private lazy field we make sure we reuse the injected instance within the same client instance

We also need the type information of the property to know what to inject so the method signature changes a bit:

We’re using a reified type, because that give us runtime access to the call site type information, which also means we have to use an inline function.

If we think about how to store the binding, the best way I’ve come across is to save the lambda functions into a map with the class as the key:

Making sure to only use Kotlin library (in case this will be used as a multi platform solution).

The final inject() function would look like this:

We get the lambda associated with T injectionFactories.getValue(T::class) and cast it to T.

At this point you could just add the bindings in the map and the framework is done. But one of the points was to make that as easy and explicit as possible. So we need to introduce the factory and single methods.

This is relatively straight forward, we just save the received lambda in the map. We need the reified inline function to have access to T::class (otherwise type erasure would prevent it), and the block has to be declared noinline to make sure we pass the function reference into the map instead of inlining the code.

To solve the single case, we’re using a Kotlin trick:

We have the same signature as the factory function. But the way to achieve reusing the same instance lies in the fact that we manually invoke the block (lambda) to create the instance and add our own lambda { it } to return the created instance every time. I just love how a simple line solves so many problems in Kotlin.

There’s just one more thing I need from this framework, and it’s usable. Named injections:

To implement this, while also keeping the simplicity when it’s not needed, we just use a Pair<KClass, String?> and pass the named parameter but also supply a default null value. So the whole framework would look like this:

A DI framework in just 18 lines…

This piece of code was created and updated a couple of times throughout the years. And whenever I would use this, I had in mind to get up and running quickly and then swapping it out with one of the injection frameworks from the beginning of the article, but I never really had a reason to do it.

Let’s take a look at some of the features in other dependency injection frameworks and discuss their usefulness:

Modules

While it’s important to separate your code into modules that make sense on how the project is organised in the real world, dependency injection doesn’t have to follow the exact same structure. Moreover, I like to keep the bindings somewhere near the entry point of the application (or tests) and the injection framework itself somewhere in a base module that everything depends on. This means that dependency injection doesn’t need the concept of a module.

Scopes

This concept is useful in very complex situations, where you have to control the lifecycle of the objects you created using dependency injection. The 2 types of dependencies I really care about are reusable objects that have to share the instance (e.g. state) throughout the application lifecycle and the ones that can be garbage collected (stateless and created every time). Which is precisely the reason I don’t use scopes. In the rarest of occasions I always imagine implementing something inside the lambda that I pass to the injection framework (to handle that one in a million special case). Although I didn’t have to do that yet, and I’ve been using more or less the same thing for years now.

Annotations

If you ever wondered where all the annotations come from there’s a java specification request for dependency injection that most frameworks are based on (at least until we switched to Kotlin). It’s the JSR330 you can read about here: https://jcp.org/en/jsr/detail?id=330

Some go a bit further than the specification and include more annotations, in support of the extra features like scopes and modules and lifecycle.

Using Kotlin you don’t need annotations to inject dependencies (property delegates work really well for that). Plus, using annotations just makes the code look really cluttered.

These days I look at dependency injection frameworks as a way to replace factories, separate and bind interfaces from their actual implementation (specially when they are in different modules). When the framework that is supposed to help you with that, comes with a much greater learning curve and/or code you have to write just to make it work, well… now you know why I use those 18 lines of code everywhere.

I realise some of the frameworks have a lot more features I didn’t cover in this article. For most of them there’s an easy workaround or they are just really not that useful.

If you are using a dependency injection framework in your app. Take a step back and have a closer look. What are the features of that framework you really care about? Does it help you or does it get in the way? Are you using the framework because it’s popular, or because it fits your needs?

Feel free to copy the code into your projects, and if you find it useful, let me know by leaving a star here: https://gist.github.com/code-twister/b25e46d1538108c82d7cb82d7307a44f

--

--

Mihaly Nagy
Mihaly Nagy

Responses (1)