A simple joke-telling app demonstrating the very basics of Dagger2. 🗡️
Dependency injection or DI is a 25-dollar term for a 5-cent concept. It means giving an object its instance variables. So instead of e.g. creating an OkHttpClient
instance yourself:
class ChuckApi {
private val client = OkHttpClient()
// ...
}
inject it via a constructor:
class ChuckApi(private val client: OkHttpClient) {
// ...
}
Voilà! You just performed a dependency injection. Simple, right?
One of the cornerstones of object-oriented design is the Dependency inversion principle. In essence, it focuses on what the code does, and not how it does it. Consider the following class:
class ChuckApi {
fun tellJoke(): String {
// returns a funny joke from the interwebs
}
}
If another class relies on the ChuckApi
, such as:
class Repo(private val api: ChuckApi) {
// ...
}
then any changes to the ChuckApi
most likely affect the Repo
, too. Also, assuming ChuckApi
requires a network connection, Repo
might be difficult to test. Introducing an interface such as:
interface FunnyApi {
fun tellJoke(): String
}
and having ChuckApi
implement it:
class ChuckApi : FunnyApi {
override fun tellJoke(): String {
// same as before
}
}
but making the Repo
depend on the interface, rather than a concrete class:
class Repo(private val api: FunnyApi) {
// heavy use of the api here ...
}
gives us the ability to pass Repo
any implementation of the FunnyApi
interface. This makes testing easier and ensures Repo
needs no code changes if various FunnyApi
implementations are provided.
So, how do we pass FunnyApi
instances to the Repo
class? You guessed it - it's with dependency injection!
Dagger2 is a popular DI framework for Java, Kotlin and Android. To use it, first add it to the list of your dependencies in build.gradle
:
dependencies {
implementation 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}
When using Kotlin, use kapt
instead:
apply plugin: 'kotlin-kapt'
dependencies {
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
}
To provide dependencies, you need modules. Modules are classes with the @Module
annotation, and methods annotated with @Provides
:
@Module
class NetworkModule {
@Provides fun providesHttpClient(): OkHttpClient = OkHttpClient()
}
Dagger will inspect the class, look at the return types of the annotated methods, and say: I now know how to create these objects!
Another method might need an OkHttpClient
to create a dependency:
@Module
class ApiModule {
@Provides fun providesFunnyApi(c: OkHttpClient): FunnyApi = ChuckApi(c)
}
Assuming Dagger knows about the NetworkModule
, it knows how to create the OkHttpClient
and therefore knows how to create an instance of FunnyApi
. In cases where Dagger can infer all the necessary dependencies, you can use a shorter @Binds
annotation:
@Module
interface ApiModule {
// Equivalent to the ApiModule above, just shorter.
@Binds fun providesFunnyApi(api: ChuckApi): FunnyApi
}
To let Dagger create objects on your behalf, you need to add an @Inject
annotation to your constructors:
class ChuckApi @Inject constructor(c: OkHttpClient) : FunnyApi {
// ...
}
Components stitch multiple modules together:
@Component(
modules = [
NetworkModule::class,
ApiModule::class
]
)
interface AppComponent {
fun inject(activity: MainActivity)
fun client(): OkHttpClient
}
Note the @Component
annotation, along with the list of modules it includes.
Dagger will generate a class called DaggerAppComponent
implementing the AppComponent
interface. To consume the dependencies, you need a DaggerAppComponent
instance, which is typically created in an Application
class:
class App : Application() {
private lateinit var component: AppComponent
override fun onCreate() {
super.onCreate()
component = DaggerAppComponent.builder()
.networkModule(NetworkModule())
.apiModule(ApiModule())
.build()
}
fun component(): AppComponent = component
}
You're now ready to consume dependencies. There are two ways to do it.
- Use an
@Inject
annotation:
class MainActivity : AppCompatActivity() {
@Inject lateinit var factory: JokerViewModel.Factory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// get the AppComponent instance, and invoke the
// appropriate inject method which will automatically
// populate all @Inject fields.
val component = (application as App).component()
component.inject(this)
}
}
Note that the inject
method is not magical, nor special. The name is merely a convention, we could easily call it awesomeMethod4Realz
. The important bit is the parameter - MainActivity
. Dagger will generate a class that knows how to populate all @Inject
fields, and whenever component.inject(this)
is invoked, the call will be propagated to a method looking like this:
public static void injectFactory(MainActivity instance, JokerViewModel.Factory factory) {
instance.factory = factory;
}
As you may have guessed, the JokerViewModel.Factory
is created and provided by Dagger.
- Call the appropriate accessor methods:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// get the AppComponent instance and invoke the getter.
val component = (application as App).component()
val client = component.client()
}
}
Use this repo to play around with Dagger2, and check out these links to learn more:
Copyright © 2019 tslamic
This work is free. You can redistribute it and/or modify it under the
terms of the Do What The Fuck You Want To Public License, Version 2,
as published by Sam Hocevar.