Dependency injection (DI) is a technique widely used in programming and well suited to Android development, where dependencies are provided to a class instead of creating them itself. By following DI principles, you lay the groundwork for good app architecture, greater code reusability, and ease of testing.
The new Hilt library defines a standard way to do DI in your application by providing containers for every Android class in your project and managing their lifecycles automatically for you.
The hilt is built on top of the popular DI library Dagger so benefits from the compile-time correctness, runtime performance, scalability, and Android Studio support that Dagger provides.
Before getting started, check out another post on the jetpack,
Android ConstraintLayout Example
View Binding in Android Jetpack [Updated]
MVVM With Retrofit and Recyclerview in Kotlin [Example]
DataStore – Jetpack alternative for SharedPreference
Setting up hilt in android project
First, add the hilt-android-gradle-plugin plugin to your project’s root build.gradle
file:
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.35'
}
}
Then, apply the Gradle plugin and add these dependencies in your app/build.gradle file:
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
}
Hilt Application
First, Enable Hilt in your app by annotating your application class with the @HiltAndroidApp
to trigger Hilt’s code generation.
@HiltAndroidApp
class HiltApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}.
Hilt Modules
A Hilt module is a class that is annotated with @Module
. Like a Dagger module, it informs Hilt on how to provide instances of certain types. Unlike Dagger modules, you must annotate Hilt modules with @InstallIn
to tell Hilt which Android class each module will be used or installed in.
Inject instances with @Provides
If you don’t directly own the class, you can tell Hilt how to provide instances of this type by creating a function inside a Hilt module and annotating that function with @Provides.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://www.howtodoandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
}
@Provides
fun provideApiClient(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
Inject interface instances with @Binds
If you have an interface, then you cannot constructor-inject it. Instead, provide Hilt with the binding information by creating an abstract function annotated with @Binds
inside a Hilt module.
@Module
@InstallIn(ViewModelComponent::class)
interface RepositoriesModule {
@Binds
fun mainRepository(mainRepositoryImpl: MainRepositoryImpl) : MainRepository
}
Provide multiple bindings for the same type
In cases where you need Hilt to provide different implementations of the same type as dependencies, you must provide Hilt with multiple bindings. You can define multiple bindings for the same type with qualifiers.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
Then, Hilt needs to know how to provide an instance of the type that corresponds with each qualifier.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@AuthInterceptorOkHttpClient
@Provides
fun provideAuthInterceptorOkHttpClient(
authInterceptor: AuthInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
}
@OtherInterceptorOkHttpClient
@Provides
fun provideOtherInterceptorOkHttpClient(
otherInterceptor: OtherInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(otherInterceptor)
.build()
}
}
You can inject the specific type that you need by annotating the field or parameter with the corresponding qualifier:
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Provides
fun provideAnalyticsService(
@AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.client(okHttpClient)
.build()
.create(AnalyticsService::class.java)
}
}
// As a dependency of a constructor-injected class.
class ExampleServiceImpl @Inject constructor(
@AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...
// At field injection.
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {
@AuthInterceptorOkHttpClient
@Inject lateinit var okHttpClient: OkHttpClient
}
Predefined qualifiers in Hilt
Hilt provides some predefined qualifiers. For example, as you might need the Context class from either the application or the activity, Hilt provides the @ApplicationContext
and @ActivityContext
qualifiers.
class MainViewModel @Inject constructor(@ActivityContext context: ActivityContext,private val mainRepository: MainRepository): ViewModel() {
}
Generated hilt components
For each Android class in which you can perform field injection, there’s an associated Hilt component that you can refer to in the @InstallIn annotation. Each Hilt component is responsible for injecting its bindings into the corresponding Android class.
Hilt provides the following components:
**Component** | **Injector for** |
---|---|
SingletonComponent | Application |
ViewModelComponent | ViewModel |
ActivityComponent | Activity |
FragmentComponent | Fragment |
ViewComponent | View |
ViewWithFragmentComponent | View with @WithFragmentBindings |
ServiceComponent | Service |
Component hierarchy
Installing a module into a component allows its bindings to be accessed as a dependency of other bindings in that component or in any child component below it in the component hierarchy:
Component lifetimes
The lifetime of a component is important because it relates to the lifetime of your bindings in two important ways:
- It bounds the lifetime of scoped bindings between when the component is created and when it is destroyed.
- It indicates when members injected values can be used (e.g. when
@Inject
fields are not null).
Component lifetimes are generally bounded by the creation and destruction of a corresponding instance of an Android class. The table below lists the scope annotation and bounded lifetime for each component.
Component | Scope | Created at | Destroyed at |
---|---|---|---|
SingletonComponent | @Singleton | Application#onCreate() | Application#onDestroy() |
ActivityRetainedComponent | @ActivityRetainedScoped | Activity#onCreate()1 | Activity#onDestroy()1 |
ViewModelComponent | @ViewModelScoped | ViewModel created | ViewModel destroyed |
ActivityComponent | @ActivityScoped | Activity#onCreate() | Activity#onDestroy() |
FragmentComponent | @FragmentScoped | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | @ViewScoped | View#super() | View destroyed |
ViewWithFragmentComponent | @ViewScoped | View#super() | View destroyed |
ServiceComponent | @ServiceScoped | Service#onCreate() | Service#onDestroy() |
Inject dependencies
Once you have enabled members injection in your Application, you can start enabling members injection in your other Android classes using the @AndroidEntryPoint annotation. You can use @AndroidEntryPoint
on the following types:
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
Note that ViewModels are supported via a separate API @HiltViewModel. The following example shows how to add the annotation to an activity, but the process is the same for other types.
To enable members injection in your activity, annotate your class with @AndroidEntryPoint.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var movieAdapter: MovieAdapter
private val viewModel : MainViewModel by viewModels()
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Hilt View Models
A Hilt View Model is a Jetpack ViewModel that is a constructor injected by Hilt. To enable the injection of a ViewModel by Hilt use the @HiltViewModel
annotation:
@HiltViewModel
class MainViewModel @Inject constructor(@ActivityContext context: ActivityContext,private val mainRepository: MainRepository): ViewModel() {
...
}
Then an activity or fragments annotated with @AndroidEntryPoint
can get the ViewModel instance as normal using ViewModelProvider or the by viewModels()
KTX extension:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var movieAdapter: MovieAdapter
private val viewModel : MainViewModel by viewModels()
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.movieList.observe(this, Observer {
movieAdapter.setMovies(it)
})
viewModel.fetchAllMovies()
}
}
Let’s dive into a real project example to see how this all works together.
Hilt Android Example
In this example, we are going to get a list of movies and list all the movies in recyclerview.
first, add the dependencies for the hilt, retrofit, coroutines, and glide.
dependencies {
//hilt
implementation "com.google.dagger:hilt-android:2.35"
kapt "com.google.dagger:hilt-compiler:2.35"
// Networking
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.okhttp3:okhttp:4.7.2"
implementation "com.squareup.okhttp3:logging-interceptor:4.7.2"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2"
implementation 'com.google.code.gson:gson:2.8.6'
implementation "androidx.activity:activity-ktx:1.2.3"
//Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
}
Next, we start with an application class:
HiltApplication.kt
@HiltAndroidApp
class HiltApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}
Then, define some modules that will provide dependencies.
NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideOkHttp() : OkHttpClient{
return OkHttpClient.Builder()
.build()
}
@Singleton
@Provides
@Named("loggingInterceptor")
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
return HttpLoggingInterceptor().apply {
this.level = HttpLoggingInterceptor.Level.BODY
}
}
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://www.howtodoandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
}
@Provides
fun provideApiClient(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
RepositoriesModule.kt
@Module
@InstallIn(ViewModelComponent::class)
interface RepositoriesModule {
@Binds
fun mainRepository(mainRepositoryImpl: MainRepositoryImpl) : MainRepository
}
Let’s set up our ViewModel:
MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(@ActivityContext context: ActivityContext,private val mainRepository: MainRepository): ViewModel() {
val movieList = MutableLiveData<List<Movie>>()
val progressBarStatus = MutableLiveData<Boolean>()
fun fetchAllMovies() {
progressBarStatus.value = true
CoroutineScope(Dispatchers.IO).launch {
val response = mainRepository.getAllMovies()
if (response.isSuccessful) {
movieList.postValue(response.body())
}
}
progressBarStatus.value = false
}
}
Having placed the @HiltViewModel above our ViewModel, we can now inject dependencies that are in either SingletonComponent or ViewModelComponent by using the @Inject annotation on the constructor or above fields or methods.
Now that we have everything in place, let’s jump to an activity:
MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var movieAdapter: MovieAdapter
private val viewModel : MainViewModel by viewModels()
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.recyclerview.adapter = movieAdapter
viewModel.movieList.observe(this, Observer {
movieAdapter.setMovies(it)
})
viewModel.progressBarStatus.observe(this, Observer {
if (it) {
binding.progressDialog.visibility = View.VISIBLE
} else {
binding.progressDialog.visibility = View.GONE
}
})
viewModel.fetchAllMovies()
}
}
That’s all. Thanks for reading. you can download this example on GITHUB.
Leave a Reply