loading...
Cover image for Dagger Hilt basics

Dagger Hilt basics

anesabml profile image Anes Abismail Originally published at anesabml.github.io Updated on ・6 min read

Photo by Marc Reichelt on Unsplash
As you may be aware, Dagger Hilt is the new Dependency Injection library introduced by Google, it's the recommended approach for DI on Android, despite beginning it in Alpha in this small talk I will try to explain some basics about Dagger Hilt.
This article assumes that you are familiar with Architecture components and MVVM.

What is Dependency Injection?

Dependency Injection is a technique that allows an object to receive other objects that it depends on, the receiving object is called the client and the provided objects are called service. A Client depends on one Service or more. This helps us follow the SOLID's Single responsibility principle.

Single responsibility principle: means that a class should have only one responsibility and that means it has only one reason to change.

Ok, you might ask how?

Let's take an example:

class Repository {

    private val remoteDataSource = RemoteDataSource()
    private val localDataSource = LocalDataSource()

    fun getUser(id: Int): User {
        var user = localDataSource.getUser(id)
        if (user == null) {
            user = remoteDataSource.getUser(id)
        }
        return user
    }
}

As you see above the Repository class has 3 responsibilities:

  1. Creating RemoteDataSource object.
  2. Creating LocalDataSource object.
  3. Retrieving Data from the data sources.

This has many problems:

  1. The Repository class can change for a variety of reasons.
  2. This makes testing the Repository class much harder, and writing tests is hard enough.

Ok, How we are going to fix this? Simply provide the RemoteDataSource and LocalDataSource in the constructor.

class Repository(
    private val remoteDataSource: RemoteDataSource,
    private val localDataSource: LocalDataSource
) {

    fun getUser(id: Int): User {
        var user = localDataSource.getUser(id)
        if (user == null) {
            user = remoteDataSource.getUser(id)
        }
        return user
    }
}

Now as you can see the Repository class has one responsibility and it's Retrieving Data from the data sources.

But you will ask how we can create an instance of the Repository class?

Let's take a look at our ViewModel

class MainViewModel: ViewModel() {

    private val remoteDataSource = RemoteDataSource()
    private val localDataSource = LocalDataSource()
    private val repository = Repository(remoteDataSource, localDataSource)

    private val _user: MutableLiveData<User> = MutableLiveData()
    val user: LiveData<User> = _user

    fun getUser(id: Int) {
        viewModelScope.launch {
            _user.value = repository.getUser(id)
        }
    }
}

And as you might guess the ViewModel has 4 responsibilities 😲:

  1. Creating RemoteDataSource object.
  2. Creating LocalDataSource object.
  3. Creating Repository object.
  4. Retrieving Data from the repository.

Ok let's fix this

class MainViewModel: ViewModel(private val repository: Repository) {

    private val _user: MutableLiveData<User> = MutableLiveData()
    val user: LiveData<User> = _user

    fun getUser(id: Int) {
        viewModelScope.launch {
            _user.value = repository.getUser(id)
        }
    }
}

Better huh. But wait we don't control the creation of a MainViewModel instance. ViewModel is an Android-specific class like Activity, Fragment and Service, so what is the solution?

Here comes our little friend Hilt that will solve our problems, but this isn't the only solution before Hilt developers use ViewModelFactory, but it introduced a lot of boilerplate code and if you have many ViewModels you will have to do some magic to only use one ViewModelFactory.

Setup

To be able to use Hilt, you need to add some dependencies. Note that at the time of writing this article, the current version is 2.28-alpha. Check out the documentation for the latest version.

  • build.gradle
dependencies {
        ...
        // Current version is 2.28-alpha
        classpath "com.google.dagger:hilt-android-gradle-plugin:2.28-alpha"
}
  • app/build.gradle:
...
apply plugin: "kotlin-kapt"
apply plugin: "dagger.hilt.android.plugin"

android {
    ...
}

dependencies {
        ...

        implementation "com.google.dagger:hilt-android:2.28-alpha"
        kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"

        // For injecting ViewModel
        implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01"
        // For injecting WorkManager    
        implementation "androidx.hilt:hilt-work:1.0.0-alpha01"

        kapt "androidx.hilt:hilt-compiler:1.0.0-alpha01"
}

How Hilt works

Hilt works by code generating and it uses different annotations to know how to provide the required dependencies at runtime. let's take a look at some of those annotations:

  • @Inject: is used to define how the Client would be constructed.
  • @ViewModelInject: is used to define how the ViewModel would be constructed.
  • @WorkerInject: is used to define how the Worker would be constructed.
  • @Provides: this is used in a Module class to define how certain dependency is provided (eg: Retrofit, Room database,..)
  • @Module: is a class that acts as a bridge between the provided dependency using @Provides and the consumer class.

Hilt example:

Ok let's take an example by and improve our previous solutions, for the sake of simplicity I will remove the data source classes:

/*
    As you can see here I used @Inject annotation.
    and as you probably have guessed we need a database instance.
*/
class Repository @Inject constructor(
    private val database: MyDatabase
) {

    fun getUser(id: Int): User {
        var user = database.getUser(id)
        return user
    }
}

Ok, now how we are going to provide a MyDatabase instance? since creating a Room database instance is more than just a constructor we will use the @Provides annotation:

/* 
    As you can see here I used @Module annotation
    Hilt will use this module to call the provideRoomDb function
    And get a MyDatabase instance
    Nb: we used the object keyword to avoid creating an instance of this module
    Will talk about @InstallIn later.
*/
@Module
@InstallIn(ApplicationComponent::class)
object RoomModule {

    @Provides 
    fun provideRoomDb(application: Application): MyDatabase =
        MyDatabase.getInstance(application)
}

Now Hilt nows how to provide MyDatabase instance and use it to create a Repository instance. But did you notice something strange? where will Hilt provide an application instance to create a MyDatabase instance?

Hilt comes with a set of default bindings that can be injected as dependencies, and Application is one of those bindings.

Components:

In the RoomModule above I used @InstallIn(ApplicationComponent::class). what the hell is it?

It tells Hilt where to install the Module meaning where it should be available as a dependency, in this example, we want to be able to use MyDatabase across the Application so we use the ApplicationComponent.

There are multiple components and using them depends on how you want to scope the provided dependency:

  • ApplicationComponent.
  • ServiceComponent.
  • ActivityRetainedComponent.
  • ActivityComponent.
  • FragmentComponent.
  • ViewComponent.
  • ViewWithFragmentComponent

Dagger Hilt components

You can take a look at the documentation and read more about each component.

ViewModel:

Now let's learn something new with MainViewModel. As you can see here I used @ViewModelInject annotation, this will tell Hilt that is a ViewModel and since ViewModels survive configuration changes, Hilt will make sure to return the same instance after configuration changes.

/* 
    As you can see here I used @ViewModelInject annotation
    it will tell Hilt that is a ViewModel class and since 
    ViewModels survive configuration changes, Hilt will make sure to return
    the same instance after configuration changes.
*/
class MainViewModel @ViewModelInject constructor(
        private val repository: Repository
    ): ViewModel() {

    private val remoteDataSource = RemoteDataSource()
    private val localDataSource = LocalDataSource()
    private val repository = Repository(remoteDataSource, localDataSource)

    private val _user: MutableLiveData<User> = MutableLiveData()
    val user: LiveData<User> = _user

    fun getUser(id: Int) {
        viewModelScope.launch {
            _user.value = repository.getUser(id)
        }
    }
}

and Now let's see some magic. In order to connect the dots we need to create an Application class and annotate it with @HiltAndroidApp :

@HiltAndroidApp
class MyApplication : Application() {

  override fun onCreate() {
     // Nb: Hilt injects the dependencies in super.onCreate() 
     // if you override onCreate make sure you call super.onCreate()
     super.onCreate()
  }
}

Entry Points:

Now after we told Hilt how to provide some dependencies, it's time to use those dependencies, for this example I will need Hilt to provide me with the MainViewModel instance, @AndroidEntryPoint will take care of that, which will take care of injecting the required dependencies for our activity.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
    // Nb: Hilt injects the dependencies in super.onCreate() 
       // if you override onCreate make sure you call super.onCreate()

        viewModel.doSomething() // will throw a runtime exeption

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel.doSomething()
    }

}

And we are done. Hilt is much bigger than just what I coved in this article, but this is the should get you started.

Dependency Injection libraries can be hard to understand, so if you have any question feel free to reach me out on Twitter @anesabml I will happily answer your questions.
Lastly, if this article has helped you learn something new, feel free to share it with others and help them learn as well.

Resources:

Discussion

pic
Editor guide