Bye bye Dagger 👋
When writing an Android application there comes a time, where you need to pick libraries. You always have in mind that these libraries should be scaleable, will help you achieve your goal and make your developer experience easier. That’s why we pick libraries that all big companies have in their toolset and have proved to work perfectly.
Such decisions I had to take when I scaffolded the Workable Android app. One of the libraries I chose was Dagger. Dagger is a dependency injection framework that generates the dependencies at compile time. Well.. Guess what.
Dagger didn’t work well for us.
Dagger is stable and powerful and I can’t argue on that. One problem is that it is too complex, both the examples and the documentation and requires a lot of boilerplate code. I can’t remember how many times I looked over the Thermosiphon example in order to understand what exactly is happening. But complexity wasn’t the reason that Dagger didn’t work well for us despite it being a significant reason.
The biggest and most annoying problem, was the accompanied code generation by (k)apt. In our app, we also use Databinding heavily which is also based on code generation (note that for later). There was a moment when I needed to change the configuration of Dagger. After finishing with the changes and hitting compile, there were over 100 errors flooding the console. There wasn’t a clear indication what went wrong.
After a lot of continuous changes, retries and refactors over the already refactored configuration, I opened this issue on Dagger because I couldn’t reproduce it on small projects. After a couple of days of debugging with a colleague, we found that the annotation processors of Dagger and Databinding were messing around and when one failed, it caused the other processor to fail as well. As a result, I had several errors that I didn’t know how they were produced. Several days ahead struggling with the issue, we discovered that
javac outputs only the first 100 errors, by default. That meant that our actual error was hidden somewhere above those 100 errors. After increasing the max errors
javac produces, I found the “hidden” error and didn’t touch the Dagger setup again for 2 years, fearing similar situations.
Another issue our team faced with Dagger and probably the biggest one, was compile times. When your codebase is growing, compile times are increasing along with it. Our build takes on average 5–7mins and the incremental one is ~2.00–2.30mins. This is because kapt was breaking incremental compilation resulting in a full rebuild. Even tests are taking a long time to compile. Things can go sideways if you also add another library with code generation. Our team decided that this isn’t a “healthy” development environment to be working on and had to do something to make our build more reliable.
What did we do?
We ditched Dagger.
We decided to replace it with Koin and couldn’t be more happy with that. Koin provides almost the same feature set as Dagger without a proxy, code generation, or reflection. It has scopes, multibindings and it is much easier to understand. Also, the documentation and examples are way more simple and straightforward. There is a tiny overhead at runtime, because it creates the dependency graph, but the cost is negligible.
Its easy and fluent API, saved us a lot of time with the migration. We started the migration by rebuilding the dependency graph exactly the way it is build with Dagger but in a Koinish way. In the meantime, we retained the dependency of Dagger in our project. Each time, we created a new module, we ran in a Unit-test the dryRun function that Koin provides. This function goes through all your modules and checks if the dependency graph is set up correctly. In addition, this helpful function, guarantees us that we will not break at runtime when we are pulling dependencies.
Once we finished the dependency graph, it was time to replace Dagger’s inject call sites with Koin ones. This was a piece of cake, since Koin offers custom delegated properties to inject dependencies. The final result looked like this:
What we did on tests, was to replace modules or some parts of them with fake ones. For example, if we want to mock the above Storage, we just replaced the module with the mocked one.
One of the powerful features of Koin is the ability to declare scopes. At Workable, we have a feature that allows a user to be logged-in in multiple accounts. This functionality is backed up by a specific module. When the user switches account, we release that module and let the user-scoped module/graph pull a new instance.
But what happened with Java files? Koin also works with Java. You can see more in the official documentation. You can also find several techniques on this Github issue.
Once finished replacing all the call sites, we deleted every Dagger class annotated with Module, Component etc, resulting in 93 additions and 1803 deletions. Quite a lot huh?
After doing that, we also saw several seconds down on compile time. This is because we removed some heavy work from kapt. Here is a sample benchmark :
| Modules \ Compile time | Dagger | Koin
| :app | 41.610s | 28.374s |
| :module1 | 26.563s | 19.745s |
| :module2 | 14.345s | 11.325s |
| :module3 | 7.241s | 3.950s |
| :module4 | 22.828s | 5.866s |
| :module5 | 4.976s | 2.505s |
| :module6 | 5.631s | 4.684s |
| :module7 | 1.988s | 3.178s |
| SUM | 125.15s | 79.59s |
What I suggest you to do, is look what works best for your team. There are several dependency injection and service locators libraries out there. We decided to use Koin. But that is our choice. Maybe your choice is to create your own, which could also be a perfect solution, because it would be simpler to understand how it actually works, since it is written by you. What won us in Koin, was its simple DSL, non verbose configuration and the ability to declare scopes and multibindings.
I am not a big fan of code generation. While sometimes it makes life easier, others it drains a lot of energy from you. I don’t like the trend of a code generation tool for every single tiny problem/thing on Android but that’s my Preference(Context.MODE_PRIVATE) 😛.
At least, one could argue about the differences between dependency injection and service locator pattern. I dοn’t want to dwell on this. You can find long and contradictory articles on the definition of these things with ease. My point is to, see what suits best for your team. Just because big companies use a library, it doesn’t mean it will work for you. Evaluate it and then go for it.