Dagger2 is a library that allows us to achieve dependency injection. It has three main concepts.
@Inject
tells dagger which variables to inject.@Module
defines how to create the objects that we want to inject.@Component
links the two together.
When we make any changes to the data code, we need to rebuild the project and the system will generate for us the classes that we need in order to perform the injections.
Implementation
Go into app.build
and add the following
apply plugin: 'kotlin-kapt'
def daggerVersion = '2.13'
dependencies {
implementation "com.google.dagger:dagger:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion
// kotlin annotation processor
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
}
Constructor Injection
We may use @Inject
with constructors. This avoids us the need for modules. We should try to implement it whenever possible.
In order for this to work, we need to make the constructor visible by explicitly using the constructor
keyword.
This is somewhat recursive, so (almost) all the dependencies need to be set with @Inject
in their constructors. Remember this.
class Smartphone @Inject constructor(val battery: Battery)
class Battery @Inject constructor()
We create a @Component
interface. The name method is not important. Only the methods return type are.
@Component
interface SmartphoneComponent {
// this are the classes that need to be injected
fun inject(mainActivity: MainActivity)
}
Rebuild the project, otherwise it won’t work. Now dagger has created a DaggerSmartphoneComponent
class, which we may use to mark the class we want to contain the injections.
class MainActivity: AppCompatActivity() {
// dependency to be injected
@Inject
lateinit var smartphone: SmartPhone
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// injection into this class
DaggerSmartphoneComponent.create()
.inject(this)
}
}
Modules injection
This is for cases where we cannot open the class and add @Inject
to the constructor, or we’re not allowed to modify the class at all (third-party libraries).
Let’s assume we don’t own the previous Battery
class.
class Battery
We create a new di
package and we create a new @Module
inside.
@Module
class BatteryModule {
// declares how to initialize the classes that are going to be injected
@Provides
fun provideBattery(): Battery {
return Battery()
}
}
We also need to link this @Module
at our @Component
class. The @Component
also goes inside di
package. Modules don’t hold a state.
@Component(modules = [BatteryModule::class])
interface SmartphoneComponent {
// this declares this class needs the injection
fun getSmartphone(): Smartphone
}
Rebuild the project. Then we may inject it into the class that holds the dependency.
class MainActivity: AppCompatActivity() {
@Inject
lateinit var smartphone: SmartPhone
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerSmartphoneComponent.create()
.inject(this)
}
}
Working with interfaces
Interfaces don’t have constructors so we cannot inject them. We need to create a class which implements the interface, and then create a module to provide an instance of that class.
interface Battery
class Smartphone @Inject constructor(val battery: Battery)
We need to create a class which implements the Battery
interface and provides the dependency through a module.
class NickelCadmiumBattery @Inject constructor(): Battery
@Module
abstract class NCBatteryModule {
@Binds
abstract fun bindsNCBattery(nickelCadmiumBattery: NickelCadmiumBattery): Battery
}
Then we need to add this module to our @Component
class.
@Component(modules = [NCBatteryModule::class])
interface SmartphoneComponent {
fun getSmartphone(): Smartphone
}
Module’s State
This is discouraged, but for some scenarios this is a requirement.
We might need to define an instance variable inside a @Module
. We provide it through its constructor.
@Module
class MemoryCardModule(val memorySize: Int) {
@Provides
fun providesMemoryCard() {
Log.i("MYTAG", "Memory size is $memorySize")
return MemoryCard()
}
}
Since at least one of our modules has a state, we cannot use the normal Dagger*Component.create()
function.
We have to use .builder()
instead. When we use it, we have to add each module with a state here.
class MainActivity: AppCompatActivity() {
@Inject
lateinit var smartphone: SmartPhone
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerSmartphoneComponent.builder()
.memoryCardModule(MemoryCardModule(1000))
.build()
.inject(this)
}
}
Application class
This class is the base for any Android App. We cannot change it, but we can give additional instructions by extending it.
Application class and its subclasses will be instantiated before all activities, fragments or any other objects.
We extend this class
- If there are tasks that need to run before the first activity’s creation.
- If there are immutable data or global objects that need to be shared across all components.
We will use the code from this previous example. We should implement this DaggerSmartphoneComponent.builder()
into a subclass of the Application class, as otherwise we’d need to duplicate this in every Activity that had this dependency.
We create a SmartphoneApplication
for this.
class SmartphoneApplication: Application() {
lateinit var smartphoneComponent: SmartphoneComponent
override fun onCreate() {
smartPhoneComponent = initDagger()
super.onCreate()
}
private fun initDagger(): SmartphoneComponent =
DaggerSmartphoneComponent.builder()
.memoryCardModule(MemoryCardModule(1000))
.build()
}
We substitute the call in MainActivity
.
class MainActivity: AppCompatActivity() {
@Inject
lateinit var smartphone: SmartPhone
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
(application as SmartphoneApplication).smartphoneComponent
.inject(this)
}
}
There’s one special thing left to do. We need to include the name of the application class we created to the Manifest. Open AndroidManifest.xml
and add the following line.
<manifest>
<application
android:name=".SmartphoneApplication"/>
</manifest>
Rebuild the project once more.
Singletons
From the previous example, every time we call this .inject(this)
function, it creates a new Smartphone
object.
To avoid this, we just need to go to Smartphone
class and annotate it as @Singleton
.
@Singleton
class Smartphone @Inject constructor(val battery: Battery)
We also need to annotate its component interface.
@Singleton
@Component(modules = [NCBatteryModule::class])
interface SmartphoneComponent {
fun getSmartphone(): Smartphone
}