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 .
Table of Contents
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:
- Application: Using @HiltAndroidApp
- ViewModel: Using @HiltViewModel
- Activity, Fragment, View, Service, BroadcastReceiver: Using @AndroidEntryPoint
@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 component | Injector for |
---|---|
SingletonComponent | Application, and also used by Broadcast Receiver |
ActivityRetainedComponent | N/A |
ViewModelComponent | ViewModel |
ActivityComponent | Activity |
FragmentComponent | Fragment |
ViewComponent | view |
ViewWithFragmentComponent | View annotated with @withFragmentBindings |
ServiceComponent | service |
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 components | Created at | Destroyed at |
---|---|---|
SingletonComponent | Application.onCreate() | Application destroyed |
ActivityRetainedComponent | First Activity. onCreate() | Last Activity. onDestroy() |
ViewModelComponent | ViewModel created | ViewModel destroyed |
ActivityComponent | Activity. onCreate() | Activity. onDestroy() |
FragmentComponent | Fragment. onAttach() | Fragment. onDestroy() |
ViewComponent | View. super() | View destroyed |
ViewWithFragmentComponent | View. super() | View destroyed |
ServiceComponent | Service. onCreate() | Service. onDestroy() |
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 class | Generated components | Scope |
---|---|---|
Application | SingletonComponent | @Singleton |
Activity | ActivityRetainedComponent | @ActivityRetainedScope |
ViewModel | ViewModelComponent | @ViewModelScoped |
Activity | ActivityComponent | @ActivityScoped |
Fragment | FragmentComponent | @FragmentScoped |
view | ViewComponent | @ViewScoped |
View annotated with @withFragmentBindings | ViewWithFragmentComponent | @ViewScoped |
service | ServiceComponent | @ServiceScope |
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.
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
- Dependency injection with Hilt , Android developers.