Dependency Injection in Kotlin using Dagger2
This story explains various types of Dependency Injections with Sample Examples.
Also will provide GitHub link for the same for reference.
What is Dependency Injection ?
Dependency Injection Comes into picture when there are 2 classes that are dependent on each other.
Second class makes use of first class. Then 2nd class is dependent on first class.
Official definition of Dependency Injection:
Dependency Injection is based on concept called “Inversion of Control”. This concept means a class should get its dependencies from external class rather than instantiating them in the class.
Dependency Injection in Android using Dagger2:
Dagger2 is a fully static ,compile-time dependency injection framework based on the Java Specification Request (JSR) 330 used for both Android and Java. It uses code generation and is based on annotations. The generated code is very relatively easy to read and debug. The earlier version was created by Square and now its maintained by Google.
Fundamental Techniques of DI:
•Constructor Injection
•Method Injection
- Field Injection
Note: Detailed explanation is provided for each of these techniques with detailed code samples after major components section explanation .
Constructor Injection:
In Constructor Injection, the dependencies are injected as part of constructor. We use @Inject annotation at the top of the constructor.
Example:
class EmployeeInfo @Inject constructor(empDetails: EmpDetails){
var empDetails: EmpDetails = empDetails
fun empDetailsInfo(){
empDetails.displayEmpName()
}
}
Field Injection:
In this technique , the dependencies are injected as part of fields in the classes that require them.
@Inject annotation is used for the fields to be injected.
Dagger doesn’t support Injection in private fields and the final field.
Field injection is mostly frequently used dependency injection technique.
class FieldInjectionActivity : AppCompatActivity() {
@Inject
lateinit var student: Student}
Method Injection:
Method which participates in the injection is called Method injection.
Example:
public class Subject {
Math math;
private Science science;
@Inject
public Subject(Math math, Science science){}
@Inject
public void readMathBook(){ }
public void read(){ }
}
Fundamental Techniques:
•Dagger will first check the constructor, if not find, then go to Field and Method.
•Constructor and field injection are most widely in the applications.
Major Components of Dagger2:
•Dependency Provider
• Dependency Consumer
- Component
Dependency Provider:
Is the one who provide the objects that are called dependencies . The class that is responsible for providing dependencies is annotated as @Module and the methods that provides dependency(objects) in this class to be annotated as @Provides.
Dependency Consumer:
Dependency consumer is a class where we need to instantiate the objects. But we don’t need to instantiate it with the new keyword . Do not even need to get it as an argument. But dagger will provide the dependency, and for this, we just need to annotate the object declaration with @Inject.
Component:
Acts as a interface between dependency consumer and dependency provider annotated with @Component and it is an interface.
Gradle Dependencies:
dependencies {
api ‘com.google.dagger:dagger:2.x’ annotationProcessor ‘com.google.dagger:dagger-compiler:2.x’
}
Source:
Constructor Injection : Explained in Detail
Example:
Two classes ,a Component and an activity
Employee: This class takes EmployeeDetails injected as Constructor
class EmployeeInfo @Inject constructor(empDetails: EmpDetails){
var empDetails: EmpDetails = empDetails
fun empDetailsInfo(){
empDetails.displayEmpName()
}
}
EmployeeDetails: This class serves as dependency for Employee class
class EmpDetails @Inject constructor() {
fun displayEmpName(){
Log.d(TAG,"In EmpDetails Constructor Injection")
}
}
EmployeeInfoComponent : Acts as interface b/w Employee and EmployeeDetails
@Component
interface EmployeeInfoComponent {
fun getEmpl(): EmployeeInfo
}
ConstructorInjectionActivity : Access the component that is generated by Dagger2 compiler and displays Employee details
class ConstructorInjectionActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_constructor_injection)
var component = DaggerEmployeeInfoComponent.create()
var empInfo = component.getEmpl()
empInfo.empDetailsInfo()
//Displays message : In EmpDetails Constructor Injection }
}
//Note:DaggerEmployeeInfoComponent is generated by Dagger2 on rebuilding the project.
Field Injection Explained in Detail:
2 classes , module, component and an activity.
Module class provides dependencies of 2 classes to be injected in Activity through component.
//This Student class to be injected in Activity as field
class Student(studentDetails: StudentDetails) {
var studentDetails = studentDetails
fun studentInfo(){
println("In Student.......")
}
}class StudentDetails (){
fun studentInfo(){
println("In Student Details......")
}
}
StudentModule:
This class provides dependencies of Student and StudentDetails classes
provideStudent() provides Student class to which StudentDetails is passed as constructor argument. Student class gets its dependency StudentDetails which is already provided as part of provideStudentDetails() method
@Module
class StudentModule {
@Provides
fun provideStudentDetails() : StudentDetails{
var studentDetails = StudentDetails()
studentDetails.studentInfo()
return studentDetails
}
@Provides
fun provideStudent(studentDetails: StudentDetails) : Student{
return Student(studentDetails)
}
}
Component:
@Component(modules = [StudentModule::class])
interface StudentComponent {
fun inject(fieldInjectionActivity: FieldInjectionActivity)
}
Activity:
class FieldInjectionActivity : AppCompatActivity() {
@Inject
lateinit var student: Student
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_field_injection)
var studentComponent = DaggerStudentComponent.builder().build()
studentComponent.inject(this)
student.studentInfo()
}
}
Qualifiers:
The @Named annotation is good for identifying which provider to be used from module when we are trying to inject the dependency of the same type.
@Qualifier is customized version of Named which is similar . Both does the same.
Example:
We have a class that has two parameters of type String which we need to provide during run time through module. In order to identify each of these parameters of same type we go for named annotation in the module.
Sample Code: The MovieDetails class requires 2 String arguments of same type to be passed during runtime using Dependency Injection. In order for the dagger to recognize them , we use @Named annotation.
private const val TAG = "MovieDetails"
class MovieDetails ( type:String, actor:String) {
var type:String = type
var actor:String = actor
fun displayMovieDetails(){
Log.d(TAG,"Movie details: Movie Type is $type and actor in movie is $actor")
}
}
Movie Class:
class Movie constructor(movieDetails: MovieDetails) {
var movieDetails: MovieDetails = movieDetails
fun displayMovie() {
movieDetails.displayMovieDetails()
}
}
MovieModule: In the below observe movieType and movieactor are provided as dependencies . Since these 2 fields are of same data type which is String , in order for the Dagger to recognize them we need to provide @Named annotation as below .
@Module
class MovieModule(movieType:String,movieActor:String){
var movieType:String = movieType
var movieActor:String = movieActor
@Provides
@Named("type")
fun provideMovieType():String = movieType
@Provides
@Named("actor")
fun provideMovieActor():String = movieActor
@Provides
fun provideMovieDetails():MovieDetails = MovieDetails(movieType,movieActor)
@Provides
fun provideMovie(movieDetails: MovieDetails):Movie = Movie(movieDetails)
}
Component:
@Component(modules = [MovieModule::class])
interface MovieComponent {
fun inject(namedInjectionActivity : NamedInjectionActivity)
}
Activity:
class NamedInjectionActivity : AppCompatActivity() {
@Inject
@field:Named("type")
lateinit var type: String
@Inject
@field:Named("actor")
lateinit var actor: String
@Inject
lateinit var movieDetails: MovieDetails
@Inject
lateinit var movie: Movie
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_named_injection)
// DaggerMovieComponent
var component =
DaggerMovieComponent.builder().movieModule(MovieModule("Action", "Jaq")).build()
component.inject(this)
movie.displayMovie()
}
}
@Scope Annotation & SubComponents:
Dagger2 creates new instance everytime we need an instance.
But as per dependency injection principle we need to create only one instance and reuse it.
Scope annotation comes into picture for this to achieve.
@Scope annotation is provided by Dagger library to define custom scopes.
@Scope is useful to ensure a variable will get created only once.
Scope Annotation Features:
•Scope can be used to create Global and Local Singletons.
•Global Singletons: Can be used across the app, such as Context, Retrofit instance, any utility classes that influence the application work. (@singleton)
•Local Singletons: These are required in one or more modules only not across the app.
If @Singleton is placed before the method which provides a dependency Dagger will create a single version of the marked dependency (or singleton) during initialization of the component.
Scope Annotation explained in Detail with Sample Code:
There are 2 types of scope annotations explained as below:
Type 1:
When a dependency to be available per activity not across entire application, a customized scope to be created to be used per Activity.
Type 2:
In order to have dependency available across the app, the corresponding component to be instantiated in Application class and the scope to be provided as @singleton scope or custom scope.
Sample Code for Type 1:
Suppose there is an dependency to be available per activity not for entire application.
Create a custom annotation as below to be used per Activity
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
annotation class PerCricketActivity{
}
The below class to be used as Dependency in an activity which needs to available per activity
private const val TAG = "Cricket"
class Cricket constructor(player : String , sports: Sports) {
var player : String = player
var sports : Sports = sports
fun displayCricketInfo(){
Log.d(TAG,"Name of Cricket Player is :: $player and its corresponding instance is $this Corresponding sports instance is $sports")
}
}
Corresponding Module:
@Module
class CricketModule(playerName : String) {
var playerName : String = playerName
@PerCricketActivity
@Provides
fun provideCricketPlayerName():String = playerName
@PerCricketActivity
@Provides
fun provideCricket(player:String , sports: Sports) : Cricket = Cricket(player,sports)
}
Component:
@PerCricketActivity
@Subcomponent (modules = [CricketModule::class])
interface CricketComponent {
fun inject(cricketActivity: CricketActivity)
}
The above steps to be followed for other class which needs to be available per activity as dependency
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
annotation class PerTennisActivity{
}
Class:
private const val TAG = "Tennis"
class Tennis constructor(playerName : String , sports : Sports) {
var playerName : String = playerName
var sports : Sports = sports
fun displayTennisInfo(){
Log.d(TAG,"Tennis Player name is $playerName and its corresponding instance is $this and its corresponding sports instance is $sports")
}
}
Module:
@Module
class TennisModule constructor(playerName : String) {
var playerName : String = playerName
@PerTennisActivity
@Provides
fun providePlayerName():String = playerName
@PerTennisActivity
@Provides
fun provideTennis(name : String, sports: Sports) : Tennis = Tennis(name,sports)
}
Component:
@PerTennisActivity
@Subcomponent(modules = [TennisModule::class])
interface TennisComponent {
fun inject(tennisActivity: TennisActivity)
}
The above 2 components to be used as subcomponent in another component called sports as the corresponding activity uses Sports component which inturn provides Tennis and Cricket components as subcomponents.
What is SubComponent?
Subcomponent is another way of building component relationships.
When we want to define component dependencies we go for subcomponents
A component can only have 1 parent while a parent can be depended to by multiple components.
How to use Subcomponents?
There will be parent component which provides dependent components as subcomponents.
The below code sample provides 2 subcomponents which are dependends for Sports component. It means SportsComponent needs Cricket and Tennis components .Hence it provides both the components.
@Singleton
@Component(modules = [SportsModule::class])
interface SportsComponent {
fun getCricketComponent(cricketModule: CricketModule): CricketComponent
fun getTennisComponent(tennisModule: TennisModule) : TennisComponent
}
Sports class:
private const val TAG = "Sports"
class Sports constructor(sportsCenter : String) {
var sportsCenter : String = sportsCenter
fun displaySportsCenterInfo(){
Log.d(TAG, "Sports Center Info is $sportsCenter")
}
}
Sports module:
@Module
class SportsModule(sportsCenter : String){
var sportsCenter : String = sportsCenter
@Singleton
@Named("sport center")
@Provides
fun provideSportCenter(): String = sportsCenter
@Singleton
@Provides
fun provideSports(): Sports = Sports(sportsCenter)
}
Usage in Activity: The below activity uses sportscomponent to get corresponding subcomponent which is tennisComponent.
class TennisActivity : AppCompatActivity() {
@Inject
lateinit var sports : Sports
@Inject
lateinit var tennis : Tennis
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tennis)
var sportsComponent : SportsComponent = AppApplication.sportsComponent
var tennisComponent = sportsComponent.getTennisComponent(TennisModule("Roger"))
tennisComponent.inject(this)
sports.displaySportsCenterInfo()
tennis.displayTennisInfo()
}
}
For Cricket Activity:
class CricketActivity : AppCompatActivity() {
@Inject
lateinit var sports: Sports
@Inject
lateinit var cricket: Cricket
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cricket)
var sportsComponent : SportsComponent = AppApplication.sportsComponent
var cricketComponent = sportsComponent.getCricketComponent(CricketModule("Sachin"))
cricketComponent.inject(this)
sports.displaySportsCenterInfo()
cricket.displayCricketInfo()
navigate_tennis.setOnClickListener {
this?.openActivity(Class.forName("com.dagger.practice.TennisActivity"))
}
}
}
Singleton Injection using @Singleton:
@Singleton annotation is used when we want to have single instance of a dependency when component is initialized
Sample Code:
In the below module, the Driver class will have single instance where as Student class will have new instance every time component is initialized
StudentModule:
@Module
class StudentModule constructor(driverName:String,studentName : String){
var driverName:String = driverName
var studentName:String = studentName
@Named("driver")
@Singleton
@Provides
fun provideDriverName() : String = driverName
@Named("student Name")
@Provides
fun provideStudentName() : String = studentName
@Singleton
@Provides
fun provideDriver():Driver = Driver(driverName)
@Provides
fun provideStudent(driver:Driver):Student = Student(driver,studentName)
}
Component:
@Singleton
@Component(modules = [StudentModule::class])
interface StudentComponent {
fun inject(singletonInjectionActivity: SingletonInjectionActivity)
}
Driver class:
private const val TAG = "Driver"
class Driver constructor(driverName: String){
var driverName:String = driverName
fun displayDriverInfo(){
Log.d(TAG,"Driver information is $this and name is $driverName")
}
}
Student class:
private const val TAG = "Student"
class Student constructor(driver:Driver,studentName:String){
var studentName:String = studentName
var driver:Driver = driver
fun displayStudentInfo(){
Log.d(TAG,"Student Info is :: Student $studentName drives with $driver and driver Name is ${driver.driverName}")
}
}
Activity where the dependencies are injected:
class SingletonInjectionActivity : AppCompatActivity() {
@Inject
@field:Named("driver")
lateinit var driverName : String
@Inject
@field:Named("student Name")
lateinit var studentName : String
@Inject
lateinit var driver: Driver
@Inject
lateinit var student: Student
@Inject
lateinit var student1: Student
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_singleton_injection)
var component = DaggerStudentComponent.builder()
.studentModule(StudentModule("John","Pollock"))
.build()
component.inject(this)
student.displayStudentInfo()
student1.displayStudentInfo()
}
}
ApplevelSingleton:
Suppose there is another scenario where we need a class to have single instance peractivity , where as another class to have single instance across application.
The above scenario is achieved as below:
Driver class: To have single instance across app
class Driver constructor(driverName :String)
{
var driverName : String = driverName
fun printDriverInfo()
{
Log.d(TAG,"Driver Info is $this and corresponding Driver Name is ::::$driverName")
}
}
Passenger: To have single instance perActivity not across app
private const val TAG = "Passenger"
@PerActivity
class Passenger constructor( driver:Driver , passengerName:String) {
var passengerName:String = passengerName
var driver : Driver = driver
fun displayPassengerInfo(){
Log.d(TAG,"Passenger instance $this with name $passengerName and driver instance is :::$driver ::: and driver Name is ${driver.driverName}")
}
}
The above scenario can be achieved using @singleton annotation,@subcomponent annotation and also @perActivity annotation which is custom annotation
Passenger Module:
@Module
class PassengerModule constructor( passengerName:String){
var passengerName : String = passengerName
@Provides
fun providePassengerName():String = passengerName
@Provides
fun providesPassenger(driver: Driver,passengerName: String):Passenger = Passenger(driver, passengerName)
}
PerActivity annotation
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
annotation class PerActivity{
}
SubComponent: The passengerModule to be available peractivity
@PerActivity
@Subcomponent(modules = [PassengerModule::class])
interface PerActivityComponent {
fun inject(appLevelSingletonActivity: AppLevelSingletonActivity)
}
Application Module: where Driver class to be available across application
@Module
class ApplicationModule(name:String) {
var name:String = name
@Singleton
@Named("driver name")
@Provides
fun provideDriverName():String = name
@Singleton
@Provides
fun provideDriver():Driver = Driver(name)
}
Application Component: In the applevel component we obtain PerActivity module as below:
@Singleton
@Component (modules = [ApplicationModule::class])
interface AppComponent {
fun getActivityComponent(passengerModule: PassengerModule): PerActivityComponent
}
Using AppComponent and PerActivity subcomponent in Activity:
class AppLevelSingletonActivity : AppCompatActivity() {
@Inject
lateinit var passengerName : String
@Inject
lateinit var passenger: Passenger
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_app_level_singleton)
var appComponent : AppComponent = AppApplication.appComponent
appComponent.getActivityComponent(PassengerModule("Pollock"))
component.inject(this)
passenger.displayPassengerInfo()
}
}
Conclusion :
The above are all the some of the scenarios that I explored during my experience as part of Dagger2 dependency injection in Kotlin.
Github link: