Saturday, July 24, 2021

 

How Dagger, Hilt and Koin differ under the hood?

This post was created alongside the video that I posted on my YouTube channel 🎥

Dagger and Koin are without a doubt the two most popular dependency injection frameworks on Android. Both those libraries serve the same purpose and seem to be very similar but they both work quite differently under the hood.

And what about Hilt? Hilt is a library that uses Dagger internally and just simplifies its usage, so everything I say here about Dagger is also applicable to Hilt.

In this article, I won’t tell you which one of those libraries to choose. Instead, I want to show you how they are different under the hood and what might be the consequences of those differences for your app.

Dagger

If we want Dagger to provide an instance of some class, all we need to do is to add @Inject annotation to the constructor.

Adding this annotation causes that Dagger will generate a Factory for this class at build time. In this case, since the class name is CompositeAdapter, it’ll generate a class named CompositeAdapter_Factory.

This class contains all the information that is needed to create the instance of the CompositeAdapter class.

code generated by Dagger (fragment)

As you can see the factory implements get() method that returns a new instance of theCompositeAdapter class. This is actually a method specified in the Provider<T> interface that this class implements. Other classes can use Provider<T> interface to obtain an instance of a class.

What if we use Hilt instead of Dagger?

In this example, it wouldn’t make any difference. Hilt is a library that uses Dagger internally and the class I’ve shown you is generated by Dagger. If you use Hilt it does generate a couple of extra classes for us that simplify usage of Dagger and reduce the number of boilerplate code that we need to write. But the core part stays the same.

Koin

Koin has a completely different approach to managing dependencies than Dagger and of course also than Hilt. To register a dependency in Koin we don’t use any annotations since Koin does not generate any code. Instead, we have to provide modules with the factories that will be used to create instances of each class that will be needed in our project.

The reference to those factories is added by Koin to InstancesRegistry class which contains the references to all the factories that we wrote.

The key in this map is the full name of a class or name that we provided if we used a named parameter. The value is a factory that we wrote that will be used to create an instance of a class.

To get a dependency all we need to do is to call the get() (for example in a factory) or by calling by inject() delegated property in activities or fragments, which calls get() lazily under the hood. The get() method will look for a factory registered for a class of a given type and inject it there.

What are the consequences?

There are some consequences of the fact that Dagger generates code to provide dependencies and Koin doesn’t.

1. Error handling

Because Dagger is a compile-time dependency injection framework if we forgot to provide some dependency we will know about our mistake almost instantly because our project will fail to build.

For example, if we forgot to add @Inject annotation to the constructor to the CompositeAdapter and try to inject it in a fragment, the build will fail with an appropriate error which shows us exactly what went wrong.

Dagger build output where there is @ Inject annotation missing

In Koin the situation is different. Because it does not generate any code if we forgot to add a factory for CompositeAdapter class, an app will build, but it will crash with RuntimeException once we request an instance of this class. It might happen at app start so we might notice it right away, but it can also happen later, on some further screen or when the user performs some specific action.

Koin throws an exception when a factory for CompositeAdapter is missing

2. Impact on build time

There is some advantage of the fact that Koin does not generate any code: it has a much smaller impact on our built time. Dagger needs to use an annotation processor to scan our code and generate appropriate classes. It may take some time and it may slow down our build.

3. Impact on runtime performance

On the other hand, because Koin resolves dependencies at runtime it has slightly worse runtime performance.

By how much? To estimate the performance difference be we can check out this repository where Rafa Vázquez measured and compared the performance of those two libraries on different devices. The test data was prepared in a way that simulates multiple levels of transitive dependencies so it’s not just a dummy app with 4 classes.

source: https://github.com/Sloy/android-dependency-injection-performance

As you can see Dagger has almost no impact at startup performance. On the other hand in Koin we can see that it takes a significant time to set up. Injecting dependencies is also a bit faster in Dagger than in Koin.

Summary

As I said at the beginning of this article, my goal here is not to tell you which one of those libraries to use. I used both Koin and Dagger in two different, quite big projects. To be honest, I think the decision which one to choose, Dagger or Koin, is way less important than to just anything that allows you to write code that is clean, simple and easy to unit test. And I think all those libraries: Koin, Dagger and Hilt do fulfil this purpose.

All those libraries have their strengths and I hope knowing how they work under the hood will help you to make a decision for yourself which one will be best for your app.

No comments:

Post a Comment