Our journey from Dagger to Koin
When you think about Dependency Injection (DI) on Android, the first library you probably think of is Dagger. But, with Kotlin becoming a first-class citizen as an Android language, there were a couple of new developments in the DI area.
Like probably 90% of the Android apps out there, we at COYO also relied on Dagger as our dependency injection framework. We had a couple of global modules and many Activity
- and Fragment
-scoped modules as well. The Engage app consists of roughly 25 screens.
But at some point, especially the scoped modules, caused us headaches. Adding a new screen (thus new Activity
and sometimes a Fragment
, too) forced the author to write a lot of boilerplate code. It was also very hard to get it right in the first attempt. Most Dagger users probably know what I’m talking about. Who of you never forgot to add your new module in ActivityBindingModule
? Sometimes this became very frustrating.
Our COYO Engage app already has a decent age, since it started at the beginning of 2017. Back then, it was written in Java and migrated to Kotlin during the last years. Today, the amount of Kotlin code is almost 100%. Of course, the app started with Dagger, but with the migration to Kotlin, the new Kotlin based DI frameworks (Koin, Kodein, andmany more) became interesting.
How we used Dagger
With Dagger, we had a couple of global modules and the already mentioned ActivityBindingModule
and also a FragmentBindingModule
to provide scoped dependencies.
Our ActivityBindingModule
looked like this (of course, in reality it had way more activities):
An activity module looked like this:
We also migrated from MVP to MVVM during 2019, which meant having some more needed methods in every Activity or Fragment module. Some people use a ViewModelModule
for that, which works similarly as the ActivityBindingModule
. We were using a different approach with a dedicated ViewModelFactory
for each activity/fragment. For every ViewModel we had to write 3 additional functions in the Activity- or Fragment-related module.
In the end, we ended up writing so much boilerplate code when adding a new activity and we were really unhappy with this situation. Especially since DI is supposed to do the heavy lifting of generating as much boilerplate code as possible.
Dagger is a replacement for these
FactoryFactory
classes that implements the dependency injection design pattern without the burden of writing the boilerplate. It allows you to focus on the interesting classes. Declare dependencies, specify how to satisfy them, and ship your app.
This is a quote from the dagger website. For us, it felt like an unfulfilled promise.
Seeking for alternatives
From time to time we researched if there are alternatives and spiked them. At COYO we have free coding Fridays every 2 weeks and we can use them for learning or improving the products on our own to some certain degree of freedom.
In general, I like working on architectural topics and trying new libraries. So, I briefly searched the web, spiked Koin and Kodein, and quickly decided to go further with Koin (Honestly, I can’t remember why I ditched Kodein).
So after 2 short spikes, and also great developments on the Koin side with dedicated support for ViewModels, we decided to make the shift to Koin. To sum it up I think it took roughly 2.5 to 3 days. I like those pull requests reducing the lines of code count, and this was clearly one of them:
So.. How is Koin doing?
Let me first explain our usage of Koin. In our Application
class, we have to set up all the modules we want to use in Koin. These are basically all modules we have.
Our Koin setup looks similar to this:
The Koin journey obviously starts with startKoin { ... }
. We are telling Koin to use our application object as context. Also, we configure a list of modules: myModules
which is a list of all modules of our app.
When using by inject()
to inject a dependency, this is happening lazily. The immediate way, it’d look like this: private val tracker: Tracker = get()
. Needless to say, it’s generally better to use lazy loading to reduce loading times when spinning up a new activity.
Those modules in myModules
, which is a List
can be defined anywhere, like this:
Here can you see a module with 3 declaration, following the Singleton pattern. So it means the object is only created once, afterwards every call to this returns the already created object. The first created object in the example is (additionally) bound to its interface type CrashProxy
. This way it can be injected by base class or the actual class:
val crashProxy: CrashProxy by inject() // This works!
val crashProxy: DebugCrashProxy by inject() // Also works! 🎉
Because the classes had a Debug
prefix, you might have guessed, that we have the very same module also built for the release
buildType, because we don’t want to DebugCrashProxy
(which does nothing) to be in production releases.
So we used the power of the Gradle build system and use almost the same module under src/release
, while the other module resides in src/debug
:
By declaringTimberLogging(get())
, the dependency of TimberLogging
is resolved to the declaration with the matching type, which we defined beforehand (single<Timber.Tree> { … }
).
But what about activity scoped modules?
Good question! We have an example here:
All objects created here are inside the scope of ContactDetailsActivity
. The viewModel
node is self-explaining and the scoped
keyword is like single
, just inside a scope.
We are also using the kotlin feature of named parameters. This is not necessary, but we decided to do that for better readability. Otherwise you’d have declarations like SomeObject(get(), get(), get(), get())
which is not very readable.
You can also see another feature of named
declarations in line 9. This is mandatory if you define multiple objects of the same type. In Dagger we usually used annotations to achieve that.
The last thing I have to mention about this snippet is this: get<ContactDetailsActivity>()
. How does that work? We didn’t define how to create that activity and as you know, as an Android developer you don’t create Activities, but the system does instead. This is unfortunately not offered by Koin directly.
But we often have dependencies where you want to use your current activity or fragment as a constructor parameter. After a bit of research, I found this way to be our way to go: https://github.com/InsertKoinIO/koin/issues/428#issuecomment-595950513
This code uses one of my favorite Kotlin features, namely extension functions. It is adding a function and fields to existing classes without inheritance. This piece of code is adding the activity or fragment instance to the Koin declarations (“BeanDefinition”) and makes it accessible. When taking a short look at the usage of our scoped module we see the whole picture:
Here we are not calling by inject()
, but instead by activityScope.inject()
. By this, we are adding the activity instance to our declarations and can use it then 🎉
Other dependencies from global modules can either be injected by activityScope.inject()
or just inject()
.
Conclusion
Migrating to Koin turned out to be a good idea. We have more fun and less stress adding new features because we have to write less boilerplate code. Also, the build times decreased a bit, since less code is generated.
A couple of days ago, Google announced Hilt, which is a wrapper around Dagger, to make it easier to use on Android. This came a bit too late for us since the migration already happened in March 2020 mostly.
One last thing to mention: If you consider migration to Koin, keep in mind, that it does not have compile-time checks! So when you lack a declaration of some dependency, you will have a crash at runtime (the dreaded NoBeanDefFoundException
).
This made the initial migration quite tedious (and made me doubting of course..), but after some initial pain, this turned out to be not a big problem for us, because when adding one new feature there are not so many new dependencies to fulfill. After all, this was the only weak spot of Koin I found.