Dependency Injection with Hilt in Android

Photo by Gemma Evans on Unsplash
Photo by Gemma Evans on Unsplash
Hilt is a dependency injection library based on Dagger and designed for Android. So when developing Android, using Hilt will be more convenient than using Dagger.

Hilt is a dependency injection library based on Dagger and designed for Android. So when developing Android, using Hilt will be more convenient than using Dagger. This article will introduce how to use Hilt.

The complete code for this chapter can be found in .

Hilt

Hilt is based on Dagger. If you would like get to know about Dagger, you can refer to the following article.

Before starting to use Hilt, we must first add the following dependencies. First, add hilt-android-gradle-plugin to the project level build.gradle.

plugins {
    id 'com.google.dagger.hilt.android' version '2.44' apply false
}

Add the following Gradle plugin and dependencies to the module level app/build.gradle.

plugins {
    id 'kotlin-kapt'
    id 'com.google.dagger.hilt.android'
}

android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
    def hilt_version = "2.44"
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-compiler:$hilt_version"
}

// Allow references to generated code
kapt {
  correctErrorTypes true
}

Product List Example

This article will use a product list example to explain how to use Hilt. In the example, we will use Room database to store product data. If you are not familiar with Room database, please refer to the following article first.

Below is the code for the product database.

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "products")
data class Product(
    val name: String,
    val price: Int,
    @PrimaryKey(autoGenerate = true) var id: Int? = null
)
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface ProductDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(entity: Product)

    @Query("SELECT * FROM products")
    suspend fun findAll(): List<Product>
}
import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [Product::class], version = 1)
abstract class ProductDatabase : RoomDatabase() {
    abstract fun dao(): ProductDao
}

@Inject: Injecting Dependencies

@Inject tells Hilt to inject the necessary dependencies.

In the following code, we annotate the constructor of ProductRepositoryImpl with @Inject. Therefore, when Hilt instantiates ProductRepositoryImpl, it will automatically pass in ProductDao.

If you annotate a constructor with @Inject, it is called constructor injection. And if you annotate a filed with @Inject, it is called filed injection.

interface ProductRepository {
    suspend fun addOrder(product: Product)
    suspend fun getAllOrders(): List<Product>
}
import javax.inject.Inject

class ProductRepositoryImpl @Inject constructor(private val dao: ProductDao) : ProductRepository {
    override suspend fun addOrder(product: Product) {
        dao.insert(product)
    }

    override suspend fun getAllOrders(): List<Product> {
        return dao.findAll()
    }
}

@Module: Hilt Modules

We just introduced using @Inject to ask Hilt to inject dependencies. The question is how does Hilt know how to instantiate those dependencies? We have to use Hilt modules to provide those instances.

A Hilt module is a class annotated with @Module. In a Hilt module, we need to define functions annotated with @Provides or @Binds to provide instances.

In addition, we will also annotate a Hilt module with @InstallIn to indicate to which Hilt component the Hilt module will provide instances.

@Provides: Provide Instances

Declare object AppModuleObject and annotate with @Module. A class annotated with @Module is a Hilt module. The Hilt module tells Hilt how to provide instances of certain types.

In AppModuleObject, we declare two functions and annotate them with @Provides. @Provides tells Hilt that this function will provide an instance. In addition, @Provides will provide the following information to Hilt.

  • Return type: the type of instance to be provided by this function.
  • Paratemeters: The dependencies that Hilt needs to pass in when calling this function.
  • Body: Tells Hilt how to provide an instance of the return type. In fact, Hilt will execute this function to obtain an instance.

So, AppModuleObject tells Hilt how to provide instances of ProductDatabase and ProductDao.

import android.content.Context
import androidx.room.Room
import com.waynestalk.hiltexample.product.ProductDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppModuleObject {
    @Singleton
    @Provides
    fun provideProductDatabase(@ApplicationContext appContext: Context): ProductDatabase {
        return Room.databaseBuilder(appContext, ProductDatabase::class.java, "orders").build()
    }

    @Singleton
    @Provides
    fun provideProductDao(database: ProductDatabase): ProductDao {
        return database.dao()
    }
}

@Binds: Instances that provide Interface

We just declared interface ProductRepository and class ProductRepositoryImpl. When we ask Hilt to inject ProductRepository in the code, how does Hilt know that it is ProductRepositoryImpl to inject? For that, we need to use @Binds.

In the following code, we declare abstract class AppModuleClass and annotate it with @Module. Therefore, AppModuleClass is also a Hilt module.

In AppModuleClass, we declare a function and annotate it with @Binds. @Binds tells Hilt that this function will provide an instance. In addition, @Binds will provide the following information to Hilt.

  • Return type: the type of instance to be provided by this function.
  • Parameter: Tell Hilt which implementation to be provided.
import com.waynestalk.hiltexample.product.ProductRepository
import com.waynestalk.hiltexample.product.ProductRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
abstract class AppModuleClass {
    @Binds
    abstract fun provideOrderRepository(impl: ProductRepositoryImpl): ProductRepository
}

Inject Dependencies into Android Classes Supported by Hilt

When you annotate an Android class (such as Activity and Fragment) with @AndroidEntryPoint, you can use @Inject in that Android class to ask Hilt to perform field injection. @AndroidEntryPoint will generate a Hilt component for each Android class. Then, Hilt will obtain instances from these Hilt components to inject dependencies.

The Android classes currently supported by Hilt are as follows:

@HiltAndroidApp: Injecting Dependencies into Android Application

All apps that use Hilt must annotate the Application class with @HiltAndroidApp.

@HiltAndroidApp will start Hilt’s code generation.

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class AppApplication : Application()

@HiltViewModel: Injecting Dependencies into ViewModel

In the following code, we annotate ProductListViewModel with @HiltViewModel. So, Hilt will generate an individual Hilt component for ProductListViewModel. Then, get the instance of ProductRepository from the Hilt component and inject it into the constructor.

@HiltViewModel
class ProductListViewModel @Inject constructor(private val productRepository: ProductRepository) :
    ViewModel() {
    val orders: MutableLiveData<List<Product>> = MutableLiveData()
    val addResult: MutableLiveData<Result<Unit>> = MutableLiveData()

    fun getAllOrders() {
        viewModelScope.launch(Dispatchers.IO) {
            val list = productRepository.getAllOrders()
            orders.postValue(list)
        }
    }

    fun addProduct(product: Product) {
        viewModelScope.launch(Dispatchers.IO) {
            productRepository.addOrder(product)
            addResult.postValue(Result.success(Unit))
        }
    }
}

@AndroidEntryPoint: inject dependencies into other Android Classes

In the following code, we ask Hilt to inject ProductListAdapter in ProductListFragment. So, we have to annotate ProductListFragment with @AndroidEntryPoint. Because ProductListActivity depends on ProductListFragment, when ProductListFragment is annotated with @AndroidEntryPoint, ProductListActivity must also be annotated with @AndroidEntryPoint.

@AndroidEntryPoint
class ProductListActivity : AppCompatActivity() {
}
@AndroidEntryPoint
class ProductListFragment : Fragment() {
    @Inject
    lateinit var adapter: ProductListAdapter

    private val viewModel: ProductListViewModel by viewModels()
}
class ProductListAdapter @Inject constructor() :
    RecyclerView.Adapter<ProductListAdapter.ViewHolder>() {
}

@EntryPoint: Injecting Dependencies into Classes Not Supported by Hilt

Hilt supports most common Android classes. When you annotate an Android class that supports Hilt with @AndroidEntryPoint, it will generate a Hilt component for this Android class. Then, Hilt will get an instance from this Hilt component, and then perform field injection on the field annotated with @Inject.

So if we need to ask Hilt to perform field injection on the filed annotated with @Inject in the classes that Hilt does not support, then how do we do it. Actually, we can’t.

In classes that Hilt does not support, we cannot use @Inject to ask Hilt to perform field injection. However, we can still use @EntryPoint and @InstallIn to get instances from Hilt component.

For example, Hilt does not support ContentProvider, and we want to get the instance of ProductRepository in ContentProvider. At this time, we can declare interface ProductsContentProviderEntryPoint, and declare a function in it with a return type of ProductRepository. Annotate ProductsContentProviderEntryPoint with @EntryPoint. We also need to annotate it with @InstallIn to tell Hilt to create an entry point in SingletonComponent. So when Hilt generates the implementation code of SingletonComponent, it will implement ProductsContentProviderEntryPoint. Finally, we need to use EntryPointAccessors to get instances from Hilt components.

class ProductsContentProvider : ContentProvider {
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface ProductsContentProviderEntryPoint {
        fun productRepository(): ProductRepository
    }

  override fun query(...): Cursor {
    val appContext = context?.applicationContext ?: throw IllegalStateException()
    val hiltEntryPoint =
      EntryPointAccessors.fromApplication(appContext, ProductsContentProviderEntryPoint::class.java)

    val productRepository = hiltEntryPoint.productRepository()
    ...
  }
}

We can find that @AndroidEntryPoint and @Inject are actually helping us deal with the above steps. However, unlike @EntryPoint, @AndroidEntryPoint will generate individual Hilt components for each Android class.

Hilt Components and @InstallIn

For each Android class annotated with @AndroidEntryPoint, Hilt will generate individual Hilt components. Then, we can use @InstallIn to indicate these Hilt components.

Hilt provides the following components for supported Android classes:

Hilt componentInjector for
SingletonComponentApplication, and also used by Broadcast Receiver
ActivityRetainedComponentN/A
ViewModelComponentViewModel
ActivityComponentActivity
FragmentComponentFragment
ViewComponentview
ViewWithFragmentComponentView annotated with @withFragmentBindings
ServiceComponentservice
Hilt components from Android developers.

Component Lifecycles

For each Android class annotated with @AndroidEntryPoint, Hilt will generate an individual Hilt component. The following table lists when these Hilt components are created and destroyed in the corresponding Android class.

Generated componentsCreated atDestroyed at
SingletonComponentApplication.onCreate()Application destroyed
ActivityRetainedComponentFirst Activity. onCreate()Last Activity. onDestroy()
ViewModelComponentViewModel createdViewModel destroyed
ActivityComponentActivity. onCreate()Activity. onDestroy()
FragmentComponentFragment. onAttach()Fragment. onDestroy()
ViewComponentView. super()View destroyed
ViewWithFragmentComponentView. super()View destroyed
ServiceComponentService. onCreate()Service. onDestroy()
The lifecycles of Hilt components from Android developers.

Component Scopes

All bindings are unscoped by default. That is to say, each time an instance is retrieved from a Hilt component, Hilt create a new instance.

Hilt also allows a binding to be scoped to a particular component. That is to say, between this component is created and destroyed, the instance of a certain type retrieved from this component will be the same instance all the time.

For example, if we annotate ProductListAdapter with @ActivityScoped, when ask Hilt to perform field injection in any Fragment in ProductListActivity, the same instance will be retrieved.

@ActivityScoped
class ProductListAdapter @Inject constructor() :
    RecyclerView.Adapter<ProductListAdapter.ViewHolder>() {
}

The table below lists all scopes.

Android classGenerated componentsScope
ApplicationSingletonComponent@Singleton
ActivityActivityRetainedComponent@ActivityRetainedScope
ViewModelViewModelComponent@ViewModelScoped
ActivityActivityComponent@ActivityScoped
FragmentFragmentComponent@FragmentScoped
viewViewComponent@ViewScoped
View annotated with
@withFragmentBindings
ViewWithFragmentComponent@ViewScoped
serviceServiceComponent@ServiceScope
The scopes of Hilt components from Android developers.

Component Hierarchy

Hilt will generate many Hilt components. There is a hierarchical relationship between these components. When using @InstallIn to indicate a Hilt module to a Hilt component, any child component of this component can use this Hilt module to create instances.

Hierarchy of the components that Hilt generates from <a href=
Hierarchy of the components that Hilt generates from Android developers.

Conclusion

Compared with Dagger, Hilt is indeed much more convenient to use. Hilt automatically generates some useful components for us. If you use Dagger, you have to manually generate these components yourself. Therefore, when using Hilt, we can save these codes.

References

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like
Photo by Willian Justen de Vasconcellos on Unsplash
Read More

Android Looper and Handler Tutorial

Looper and Handler are one of the Android core components, and many high-level components are built on top of them. Understanding them helps us understand how some core components work. This article will introduce Looper and Handler and related components.
Read More