Dagger2 (dependency injection)

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
}