Shooting a Fly With a Bazooka: Dagger vs Koin in Small Projects

In this post, I'd like to compare the steps required to apply Dagger and Koin to the MVVM project.
From the very beginnign, we can already see a couple of big differences. Dagger is written in Java and Koin in Kotlin, and using both libraries in a Kotlin project should not bring any challenges. However, implementing Koin in Java can be a little tricky. One big thing worth mentioning is that while Dagger is a fully acknowledged DI project, while Koin is only described as Service Locator.
For the purposes of this post, we will be using a single activity application with one ViewModel that requires only one dependency to be constructed.
Let's start with Dagger
We will start our comparison with Dagger because, believe me, it is a much more time-consuming task and we want to leave the easiest one for the end. We will not go through the whole implementation of every module. Let’s start with dependencies and our ViewModel:
compile 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
compile 'com.google.dagger:dagger-android:2.x'
compile 'com.google.dagger:dagger-android-support:2.x' // if you use the support libraries
annotationProcessor 'com.google.dagger:dagger-android-processor:2.x'
class SearchViewModel @Inject constructor(
private val coffeeMachine: CoffeeMachine
) : ViewModel() {
fun brewCoffee(): Coffee {
return coffeeMachine.brewCoffee()
}
}
So we have our depencies, what’s next? We need to create a ViewModelFactory. Fortunately, Google released a sample that uses MVVM and Dagger, so we will just use that as the reference (GithubBrowserSample)
Getting back to ViewModelFactory.
@Singleton
class CustomViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
try {
@Suppress("UNCHECKED_CAST")4>
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
When we have our factory, it is time for a proper module that will handle binding our ViewModels. But before we get to that, we need one little thing, and that thing is:
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
Yes - a little annotation class. You can never have too many annotations when you are dealing with Dagger.
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel
@Binds
abstract fun bindViewModelFactory(factory: CustomViewModelFactory): ViewModelProvider.Factory
}
As you can see, we introduced a MainViewModel - a simple ViewModel that will take only one class, which will be presented in a while, as the constructor parameter. So we got ourselves a nice factory, an annotation class, and a module. Are we there yet? No. It is time for the module that will provide our ViewModel with the required parameters:
@Module
class AppModule {
@Singleton
@Provides
fun provideCoffeeMachine() = CoffeeMachine()
}
class CoffeeMachine {
fun brewCoffee(): Coffee {
...
}
}
Are we there yet? No. One more module to bind our activity:
@Module
abstract class ActivityModule {
@ContributesAndroidInjector
abstract fun mainActivityInjector(): MainActivity
}
Are we there yet? No. One more module to rule them all:
@Component(
modules = [
AndroidInjectionModule::class,
AndroidSupportInjectionModule::class,
AppModule::class,
ActivityModule::class,
ViewModelModule::class
]
)
internal interface ApplicationComponent : AndroidInjector<App> {
@Component.Builder
abstract class Builder : AndroidInjector.Builder<App>()
}
Now, in order to launch Dagger, we need to extend DaggerApplication:
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjectorApp =
DaggerApplicationComponent.builder().create(this)
}
That’s easy, right? All we had to do was to create a ViewModelFactory that will resolve our problem with custom constructors, create an annotation class to help us with binding ViewModels that we will, later on, fetch using ViewModelProviders, and an injected factory using their classes as a map key - all this from the activity for which we needed the second module. Oh, right - I almost forgot about the third module that is actually holding the things that we want to inject. So what's left? Creating an Activity that will let us brew some nice coffee:
class MainActivity : DaggerAppCompatActivity() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val mainViewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[MainViewModel::class.java] }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainViewModel.brewCoffee()
}
}
Koin
Wow, that was a lot of code for one coffee. Now let’s see how all of this can be done with a couple of lines in Koin. For this implementation, CoffeeMachine will remain the same as in the previous example. We will start with dependencies and our slightly edited ViewModel:
implementation 'org.koin:koin-android:2.0.1'
implementation 'org.koin:koin-androidx-scope:2.0.1'
implementation 'org.koin:koin-androidx-viewmodel:2.0.1'class
MainViewModel(
private val coffeeMachine: CoffeeMachine
) : ViewModel() {
fun brewCoffee(): Coffee {
return coffeeMachine.brewCoffee()
}
}
First difference: we don’t need to annotate our constructor with anything. So we have our ViewModel - what now? A couple of modules? Extending it with KoinApplication? No.
class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@App)
modules(module)
}
}
companion object {
private val module = module {
single { CoffeeMachine() }
viewModel { MainViewModel(get()) }
}
}
}
Here we have it. A full Koin setup for one ViewModel with one dependency. And injecting it into our Activity is even simpler.
class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainViewModel.brewCoffee()
}
}
And there you have it. Coffee brewed in a couple of lines.
In conclusion
This post can be seen as a little bit negative towards Dagger. And it is, but just a bit - I am not trying to change your mindset on Service Locators, and I am not saying that Dagger comes with only disadvantages. It is a great tool for huge commercial projects. But sometimes using Dagger is like shooting a fly with a bazooka. So remember to always choose your tools according to your needs.