Dagger-Hilt in Android

Overview

Class without dependency injection:

 class Mobile
{
private var battery : Battery? = null
private var processor : Processor? = null

init
{
battery = Battery()
processor = Processor()
}

}
In the above line of code, when you create an object of a Mobile, then its constructor will have to create the object of Battery and Processor to initialize the properties of Mobile. Henceforth, it has to violate the principle defined by "Uncle Bob" i.e. 1st principle of Solid Principle (Single responsibility Principle). 

In addition, we have to admit that whenever we are creating an object of Mobile, the class will create an object of Battery and Processor. In future, if we want to pass the object of Battery in the class then we are not allowed to do it because of scope limitation. 

What is dependency injection?

When an object of a class depends on the object of another classes that is dependency injection.
 class Mobile(var battery : Battery? = null , var processor : Processor? = null)
{

}
 class MainActivity : AppCompatActivity()
{
override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val mobile = Mobile(Battery() , Processor())
}
}
The above code is the example of dependency injection. In this case we have adding dependency injection by our own. We can achieve the same purpose with the help of Dagger. Dagger is a framework that provides dependency injection but it is mainly developed for Java. We can use it in Android but we have to use lots of boilerplate codes. So to overcome this issue we can use Hilt framework that will provide dependency injection.

Hilt

Hilt provides a standard way to incorporate Dagger dependency injection into an Android application. 

What is solid principle?

  1. Single responsibility principle: A class has the responsibility to create its own object

Steps to follow

  • Add the required dependency in build.gradle file (Project)
 dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
}
  • Add the required dependency in build.gradle file (Module)
 plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
 dependencies {
def hilt_version = "2.38.1"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
}
  • Create a class and extend it with Application() class.
 @HiltAndroidApp
class MainApplication : Application()
{

}
  • Register the Application() class in AndroidManifest.xml file.
 <application
android:name=".MainApplication">
</application>
  • Using field injection and constructor injection in Dagger-Hilt.
 class Battery @Inject constructor()
{

}
 class Processor @Inject constructor()
{

}
 class Mobile @Inject constructor(var battery : Battery , var processor : Processor)
{

}
 @AndroidEntryPoint
class MainActivity : AppCompatActivity()
{
@Inject
lateinit var mobile : Mobile
override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
  • If you don't own a class then you can't annotate that class with @Inject. You have to use module injection. Here is the example of module injection
    • Create a package 'modules' and also create a class/object inside this package.
    • Annotate the class/object with @Module and @InstallIn
  @Module
@InstallIn(SingletonComponent::class)
object BatteryModule
{
@Provides
fun getCobalt() : Cobalt
{
return Cobalt()
}

@Provides
fun getLithium() : Lithium
{
val lithium = Lithium()
lithium.printLog()
return lithium
}

@Provides
fun getBattery(cobalt : Cobalt , lithium : Lithium) : Battery
{
return Battery(cobalt , lithium)
}
}
    • Define the models
 class Lithium
{

// If we don't own this class then we can't annotate the class with @Inject
init
{
Log.d(Utils.TAG , "Lithium: $this")
}
fun printLog()
{
Log.d(Utils.TAG , "Printing Logs of Lithium...")
}
}
 class Cobalt
{

// If we don't own this class then we can't annotate this class with @Inject
init
{
Log.d(Utils.TAG , "Cobalt: $this")
}

fun printLogs()
{
Log.d(Utils.TAG , "Printing Logs of Cobalt...")
}
}
 class Battery(val cobalt : Cobalt , val lithium : Lithium)
{

// If we don't own this class then we can't annotate the class with @Inject
init
{
Log.d(Utils.TAG , "Battery: $this")
}
}
  • If you are using an interface or an abstract class than you have to use @Bind annotation on the abstract function in the module.
 interface Processor
{
fun start()
}
 class Snapdragon @Inject constructor() : Processor
{
override fun start()
{
Log.d(Utils.TAG , "start::")
}

init
{
Log.d(Utils.TAG , "Snapdragon :: $this")
start()
}
}
 @Module
@InstallIn(SingletonComponent::class)
abstract class SnapdragonModule
{
@Binds
abstract fun getProcessor(snapdragon : Snapdragon) : Processor
}
  • If you want you initialize a property of a class then you have to use @Named annotation in both the constructor of the class and also in the module class.
 class Snapdragon @Inject constructor(
@Named("core") var core : Int ,
@Named("clockSpeed") var clockSpeed : Int
) : Processor
{
override fun start()
{
Log.d(Utils.TAG , "start::")
}

init
{
Log.d(Utils.TAG , "Snapdragon : $this with clock: $core and clockSpeed: $clockSpeed")
start()
}
}
 @Module
@InstallIn(SingletonComponent::class)
object SnapdragonModule
{
@Provides
fun getProcessor(snapdragon : Snapdragon) : Processor = snapdragon

@Provides
@Named("core")
fun getCore() = 8

@Provides
@Named("clockSpeed")
fun getClockSpeed() = 12
}
  • @Singleton is an annotation which is used to allow only one instance of a class throughout an application. You can use @Singleton over @Provides annotation, over @Bind annotation and over a class declaration.
 @Singleton
@Provides
fun getBattery(cobalt : Cobalt , lithium : Lithium) : Battery
{
return Battery(cobalt , lithium)
}
 @Singleton
class Snapdragon @Inject constructor(
@Named("core") var core : Int , @Named("clockSpeed") var clockSpeed : Int
) : Processor
{
override fun start()
{
Log.d(Utils.TAG , "start::")
}

init
{
Log.d(Utils.TAG , "Snapdragon : $this with clock: $core and clockSpeed: $clockSpeed")
start()
}
}
 @AndroidEntryPoint
class MainActivity : AppCompatActivity()
{
@Inject
lateinit var samsung : Mobile

@Inject
lateinit var apple : Mobile
override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(Utils.TAG , "Samsung : $samsung")
Log.d(Utils.TAG , "Apple : $apple")
}
}
2022-01-26 11:46:39.869 11607-11607/com.example.dagger_hilt_android D/Dagger_Hilt: Mobile :: com.example.dagger_hilt_android.model.Mobile@ae45e87, Battery :: com.example.dagger_hilt_android.model.Battery@7be4ca1, Processor :: com.example.dagger_hilt_android.model.Snapdragon@ce5dec6
2022-01-26 11:46:39.870 11607-11607/com.example.dagger_hilt_android D/Dagger_Hilt: Mobile :: com.example.dagger_hilt_android.model
  • Use of Dagger-Hilt with other components and their scopes
 class Battery(private var cobalt : Cobalt , private var lithium : Lithium)
{
/*If you don't own the class then you can't annotate the class with @Inject*/
init
{
Log.d(Utils.TAG , "Battery :: $this, Cobalt :: $cobalt, Lithium :: $lithium")
}
}
 class Camera
{
// If you don't own the class then you can't annotate the class with @Inject
init
{
Log.d(Utils.TAG , "Camera :: $this")
}
}
 class Cobalt
{
/*If you don't own the class then you can't annotate the class with @Inject*/
init
{
Log.d(Utils.TAG , "Cobalt :: $this")
}
}
 class Lithium
{
/*If you don't own the class then you can't annotate the class with @Inject*/
init
{
Log.d(Utils.TAG , "Lithium :: $this")
}

fun start()
{
Log.d(Utils.TAG , "Lithium :: start")
}
}
 class Mobile @Inject constructor(var battery : Battery , var processor : Processor)
{
init
{
Log.d(Utils.TAG , "Mobile :: $this, Battery :: $battery, Processor :: $processor")
}
}
 class Snapdragon @Inject constructor(
@Named("core") var core : Int , @Named("clockSpeed") var clockSpeed : Int
) : Processor
{
init
{
Log.d(Utils.TAG , "Snapdragon :: $this, Core :: $core :: ClockSpeed :: $clockSpeed")
start()
}

override fun start()
{
Log.d(Utils.TAG , "Snapdragon :: start()")
}
}
 interface Processor
{
fun start()
}
 @Module
@InstallIn(ActivityComponent::class)
object ActivityModule
{
@Provides
fun getCobalt() : Cobalt = Cobalt()

@Provides
fun getLithium() : Lithium
{
val lithium = Lithium()
lithium.start()
return lithium
}

@Provides
@ActivityScoped
fun getBattery(cobalt : Cobalt , lithium : Lithium) : Battery = Battery(cobalt , lithium)

}
 @Module
@InstallIn(SingletonComponent::class)
object ApplicationModule
{
@Provides
@Singleton
fun getProcessor(snapdragon : Snapdragon) : Processor = snapdragon

@Provides
@Named("core")
fun getCore() : Int = 8

@Provides
@Named("clockSpeed")
fun getClockSpeed() : Int = 12
}
 @Module
@InstallIn(FragmentComponent::class)
object FragmentModule
{

@Provides
@FragmentScoped
fun getCamera() = Camera()
}
 @AndroidEntryPoint
class MainFragment : Fragment()
{

@Inject
lateinit var camera1 : Camera

@Inject
lateinit var camera2 : Camera

@Inject
lateinit var battery1 : Battery

@Inject
lateinit var battery2 : Battery

@Inject
lateinit var processor1 : Processor

@Inject
lateinit var processor2 : Processor

private lateinit var mView : View;

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
}

override fun onCreateView(
inflater : LayoutInflater , container : ViewGroup? , savedInstanceState : Bundle?
) : View?
{ // Inflate the layout for this fragment
mView = inflater.inflate(R.layout.fragment_main , container , false)
Log.d(Utils.TAG , "===============MainFragment==================")
Log.d(Utils.TAG , "Camera 1: $camera1")
Log.d(Utils.TAG , "Camera 2: $camera2")
Log.d(Utils.TAG , "Battery 1: $battery1")
Log.d(Utils.TAG , "Battery 2: $battery2")
Log.d(Utils.TAG , "Processor 1: $processor1")
Log.d(Utils.TAG , "Processor 2: $processor2")
return mView
}

override fun onAttach(context : Context)
{
super.onAttach(context)
}

override fun onViewCreated(view : View , savedInstanceState : Bundle?)
{
super.onViewCreated(view , savedInstanceState)
}

override fun onStart()
{
super.onStart()
}

override fun onResume()
{
super.onResume()
}

}
 @AndroidEntryPoint
class MainActivity : AppCompatActivity()
{

@Inject
lateinit var battery1 : Battery

@Inject
lateinit var battery2 : Battery

@Inject
lateinit var processor1 : Processor

@Inject
lateinit var processor2 : Processor

override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(Utils.TAG , "===============MainActivity==================")
Log.d(Utils.TAG , "Battery 1: $battery1")
Log.d(Utils.TAG , "Battery 2: $battery2")
Log.d(Utils.TAG , "Processor 1: $processor1")
Log.d(Utils.TAG , "Processor 2: $processor2")
val mainFragment = MainFragment()
replaceFragment(mainFragment)
}

private fun replaceFragment(fragment : Fragment)
{
supportFragmentManager.beginTransaction().apply {
replace(R.id.frameContainer , fragment , "MainFragment")
commit()
}
}
}
 @HiltAndroidApp
class MainApplication : Application()
{

@Inject
lateinit var processor1 : Processor

@Inject
lateinit var processor2 : Processor

override fun onCreate()
{
super.onCreate()
Log.d(Utils.TAG , "===============MainApplication==================")
Log.d(Utils.TAG , "Processor 1: $processor1")
Log.d(Utils.TAG , "Processor 2: $processor2")
}

}
  • Use of Dagger-Hilt with Retrofit and LiveData
 // Dagger Hilt
def hilt_version = "2.38.1"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"

// Retrofit
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.1"

// Coroutines
def coroutines_version = "1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

// ViewModel and LiveData
def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

// AndroidX for Activity used for Dependency Injection
def activity_version = "1.4.0"
implementation "androidx.activity:activity-ktx:$activity_version"

// AndroidX for Fragment used for Dependency Injection
def fragment_version = "1.4.1"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"
 data class PostItem(
val id : Int? = null ,
val title : String? = null ,
val body : String? = null ,
val userId : Int? = null
)
 interface PostApi
{
@GET("posts")
suspend fun getPosts() : List<PostItem>
}
 object RetrofitHelper
{
const val BASE_URL = "https://jsonplaceholder.typicode.com/"
}
 class PostRepository @Inject constructor(private val postApi : PostApi)
{
suspend fun getPost() = postApi.getPosts()
}
 @Module
@InstallIn(SingletonComponent::class)
object AppModule
{
@Provides
@Singleton
fun getBaseURL() : String = RetrofitHelper.BASE_URL

@Provides
@Singleton
fun getRetrofit(baseURL : String) : Retrofit =
Retrofit.Builder().baseUrl(baseURL).addConverterFactory(GsonConverterFactory.create())
.build()

@Provides
@Singleton
fun getPostRequest(retrofit : Retrofit) : PostApi = retrofit.create(PostApi::class.java)
}
 @HiltViewModel
class PostViewModel @Inject constructor(private val postRepository : PostRepository) : ViewModel()
{
private val _postMutableLiveData : MutableLiveData<List<PostItem>> = MutableLiveData()
private val postLiveData : LiveData<List<PostItem>> = _postMutableLiveData

fun getPostLiveData() : LiveData<List<PostItem>> = postLiveData

fun getPost() = viewModelScope.launch(Dispatchers.IO) {
val posts = postRepository.getPost()
_postMutableLiveData.postValue(posts)
}
}
 @AndroidEntryPoint
class PostActivity : AppCompatActivity()
{
private val postViewModel : PostViewModel by viewModels()
override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post)

postViewModel.getPostLiveData().observe(this) {
Log.d(Utils.TAG , "PostActivity :: onCreate :: Posts :: $it")
}

postViewModel.getPost()
}
}
 @HiltAndroidApp
class MainApplication : Application()
{

}
 object Utils
{
const val TAG = "Dagger_Hilt"
}
  • Use of Dagger-Hilt with MVVM
 plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
 // Dagger Hilt
def hilt_version = "2.38.1"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"

// Retrofit
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.1"

// Coroutines
def coroutines_version = "1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

// ViewModel and LiveData
def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

// AndroidX for Activity used for Dependency Injection
def activity_version = "1.4.0"
implementation "androidx.activity:activity-ktx:$activity_version"

// AndroidX for Fragment used for Dependency Injection
def fragment_version = "1.4.1"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"

// Room Database
def room_version = "2.4.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
 @Entity(tableName = "posts")
data class Post(
@PrimaryKey(autoGenerate = true) val id : Long ,
val title : String ,
val description : String
)
 @Dao
interface PostDao
{
@Insert
suspend fun insertPost(post : Post)

@Update
suspend fun updatePost(post : Post)

@Delete
suspend fun deletePost(post : Post)

@Query("SELECT * FROM posts")
fun loadAllPosts() : LiveData<List<Post>>

@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insertAllPosts(vararg post : Post)
}
 @Database(entities = [Post::class] , version = 1 , exportSchema = false)
abstract class MyRoomDB : RoomDatabase()
{
abstract fun getPostDao() : PostDao
}
 class PostRepository @Inject constructor(private val postDao : PostDao)
{
suspend fun insertAll(vararg post : Post) = withContext(Dispatchers.IO) {
postDao.insertAllPosts(*post)
}

fun loadAll() = postDao.loadAllPosts()
}
 @HiltViewModel
class PostViewModel @Inject constructor(private val postRepository : PostRepository) : ViewModel()
{
val postLiveData = postRepository.loadAll()

fun insertAll(vararg post : Post) = viewModelScope.launch(Dispatchers.IO) {
postRepository.insertAll(*post)
}

}
 @Module
@InstallIn(SingletonComponent::class)
object AppModule
{

@Provides
@Singleton
fun getDBName() : String = "RoomDB"

@Provides
@Singleton
fun getRoomDB(@ApplicationContext context : Context , name : String) : MyRoomDB =
Room.databaseBuilder(context , MyRoomDB::class.java , name).build()

@Provides
@Singleton
fun getPostDao(db : MyRoomDB) : PostDao = db.getPostDao()
}
 @AndroidEntryPoint
class PostActivity : AppCompatActivity()
{
private val viewModel : PostViewModel by viewModels()
override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post)

viewModel.insertAll(
Post(0 , "Dagger-Hilt" , "Learn Dagger Hilt") ,
Post(0 , "Java" , "Learn Java") ,
Post(0 , "Android" , "Learn Android") ,
Post(0 , "Kotlin" , "Learn Kotlin")
)

viewModel.postLiveData.observe(this) {
Log.d(Utils.TAG , "PostActivity :: onCreate :: $it")
}
}
}
 @HiltAndroidApp
class MainApplication : Application()
{

}
 object Utils
{
const val TAG = "Dagger_Hilt"
}

Do's and Dont's

  • Do not forget to add the annotation @HiltAndroidApp in the class that is extended with Application() class 
 @HiltAndroidApp
class MainApplication : Application()
  • Do not forget to add the annotation @AndroidEnryPoint in the Android components like Activities, Fragments, Services
 @AndroidEntryPoint
class MainActivity : AppCompatActivity()
 @AndroidEntryPoint
class MainFragment : Fragment()
  • If you own a class than you can use constructor injection and if you don't own a class then you can use module injection.
  • You cannot use @Inject on a private object. If you do then you will the error "Dagger does not support injection into private fields"
  • If you forget to add @Inject annotation then it will not create the required object at the time of declaration.
  • When you are using an abstract class as a Module then you have to define and abstract function to initialize the class and annotate that abstract function with @Binds. You cannot use @Provides annotation in this class, it will throw an exception.
 @Module
@InstallIn(SingletonComponent::class)
abstract class SnapdragonModule
{
@Binds
abstract fun getProcessor(snapdragon : Snapdragon) : Processor
}
  • You can use an object to act as Module when you are using Module injection. Using an object instead of a class will give an advantage of not creating an instance of Module. You can directly the object to access the methods and properties defined in the Module.
  • When you are using the annotation @InstallIn, don't forget to define SingleTonComponent or other such components
 @InstallIn(SingletonComponent::class)
  • When you use a @Named annotation
  • When you are using other components like ActivityComponent or FragmentComponent then please take care where you are injecting their objects. This mistake can lead to error. e.g. Objects that are defined for Fragment Component can't be used in Activity Component. But objects of Android Component can be used in Fragment Component. 

Cheat Sheets



Reference

Comments

Popular posts from this blog

Architecture Components in Android

DataBinding in Android

SSLSocketFactory in Android