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:
- Creating
RemoteDataSource
object. - Creating
LocalDataSource
object. - Retrieving Data from the data sources.
This has many problems:
- The
Repository
class can change for a variety of reasons. - 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 😲:
- Creating
RemoteDataSource
object. - Creating
LocalDataSource
object. - Creating
Repository
object. - 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
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.
Top comments (0)