Dependency injection on Android with Hilt[Example]

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.

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. 

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 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://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 qualifers 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:

ComponentInjector for
SingletonComponentApplication
ViewModelComponentViewModel
ActivityComponentActivity
FragmentComponentFragment
ViewComponentView
ViewWithFragmentComponentView with @WithFragmentBindings
ServiceComponentService

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:

  1. It bounds the lifetime of scoped bindings between when the component is created and when it is destroyed.
  2. 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.

ComponentScopeCreated atDestroyed at
SingletonComponent@SingletonApplication#onCreate()Application#onDestroy()
ActivityRetainedComponent@ActivityRetainedScopedActivity#onCreate()1Activity#onDestroy()1
ViewModelComponent@ViewModelScopedViewModel createdViewModel destroyed
ActivityComponent@ActivityScopedActivity#onCreate()Activity#onDestroy()
FragmentComponent@FragmentScopedFragment#onAttach()Fragment#onDestroy()
ViewComponent@ViewScopedView#super()View destroyed
ViewWithFragmentComponent@ViewScopedView#super()View destroyed
ServiceComponent@ServiceScopedService#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 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 activitiy 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 list of movies and list all the movies in recyclerview.

first, add the dependencies for 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://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

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