Hilt is a modern android DI framework for dependency injection. It is merely a wrapper around Dagger2. Forget dagger-android, hilt brings a lot to our plate. This article covers steps to add hilt to the project and use along with viewmodel-fragment.
…
This is the first installment in three part series.
Part1: Android — Basic Hilt setup with viewmodel + fragment
Part2: Smoke, mirrors & HiltViewModel
Part3: Fakes and espresso
…
Sample project used for this article is available in github.
💉 What are we injecting?
For this example, we’re going to provide Profile
(a POJO) to a fragment through ViewModel
. For simplicity let’s not use LiveData
in here. This is how the dependency graph looks like.
🔘 Little about scope
Dependencies could be of different scope (how long it can be in memory/when it can be garbage collected). When we speak about the scope of a dependency, we can easily define it in terms of android components. Below is the oversimplified version of commonly used scopes.
- A user session details should be available throughout the app (Singleton)
- I have a tabbed screen and want to share some in-memory fields between fragments (Activity)
- My data is bound to current screen/fragment. When it is destroyed purge the dependency as well (Fragment)
- Associate my dependency with ViewModel. Depends on the viewmodel’s scope (activity / fragment) let my dependency live (ViewModelScope)
For our use-case, we’ll inject DataRepository to the viewmodel using hilt. And there are few improvements on creating viewmodel for the fragment. We’ll see it end to end in the following section.
💻 Code it
Build setup
Like any framework we write less with hilt because most of the code is generated
for us. For that purpose, we’ll use hilt gradle plugin. And, hilt is expected to be used in multiple modules, so extract out the version to project level gradle file and use it in submodules.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// File: build.gradle
buildscript {
ext {
hilt_version = '2.38.1'
}
repositories {
// redacted
}
dependencies {
// redacted
classpath "com.google.dagger:hilt-android-gradle-plugin:${hilt_version}"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// File: app/build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'dagger.hilt.android.plugin'
}
dependencies {
// Hilt
implementation "com.google.dagger:hilt-android:${hilt_version}"
kapt "com.google.dagger:hilt-compiler:${hilt_version}"
// Fragment / viewmodel
def lifecycle = "2.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${lifecycle}"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${lifecycle}"
implementation "androidx.fragment:fragment-ktx:1.3.6"
}
In the project level gradle we tell the build system to use hilt. And the app level gradle applies the plugin so that it can skim through our codebase and generate dependencies for us.
…
HiltAndroidApp setup (application context)
A singleton is expected to be alive through the app session. Here, the dependency lives with application. In order to tell hilt about the application scope, create and annotate application class with HiltAndroidApp
.
1
2
3
4
5
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApp : Application()
Above step is needed because the application context is provided through generated application class. Make the project and inspect the contents of Hilt_MyApp
class.
1
2
3
4
// generated source
DaggerMyApp_HiltComponents_SingletonC.builder()
.applicationContextModule(new ApplicationContextModule(Hilt_MyApp.this))
.build();
Above steps enables smoother injection of application context to RoomDB / SharedPreference - what not.
…
Creating data source and modules
Code speaks thousand words. Below code block shows resource and the data source.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// A resource
data class Profile(
val name: String,
val age: Int
)
// A simple interface which returns the resource.
// This will help us mock the data source when executing tests.
interface DataRepository {
fun getProfile(): Profile
}
// Simple implementation of data source
class DataRepoImpl : DataRepository {
override fun getProfile(): Profile =
Profile(name = "Bruce Wayne", age = 42)
}
From here, we’ll work towards creating dependencies that can be recognized by Hilt.
First, annotate DataRepoImpl
constructor with @Inject
. This puts our class under Dagger/Hilt’s radar.
1
class DataRepoImpl @Inject constructor() : DataRepository {
Second, create a module that can provide dependency to view model.
1
2
3
4
5
6
7
8
@InstallIn(ViewModelComponent::class) // Scope our dependencies
@Module
abstract class ProfileModule {
// To be read as — When someone asks for DataRepository, create a DataRepoImpl and return it.
@Binds
abstract fun getProfileSource(repo: DataRepoImpl): DataRepository
}
…
Viewmodel setup
ViewModels can tell ask hilt to provide dependencies. A simple way to ask dependencies is to mark viewmodel with HiltViewModel
annotation.
1
2
3
4
5
6
7
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val repo: DataRepository
) : ViewModel() {
fun getProfile(): Profile = repo.getProfile()
}
Here we’re doing constructor injection on viewmodel. Doing the same without Hilt will require a Factory which pass on the dependencies to the constructor. Reason is the lifecycle of the viewmodel is managed by a lifecycle owner like activity/fragment. Internal mechanics of this injection is covered in last section. On to the fragment…
…
Fragment setup
Fragment or activity is identified as AndroidEntryPoint
in hilt. A fragment which is maked with AndroidEntryPoint
will inject the dependencies without much boilerplate code. And this is our ProfileFragment which consumes the viewmodel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@AndroidEntryPoint
class ProfileFragment : Fragment() {
private val viewModel by viewModels<ProfileViewModel>()
// redacted
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Here we go -- the profile resource is shown to the UI
view.findViewById<TextView>(R.id.name_label)
.text = viewModel.getProfile().name
}
}
That’s it!!
Nothing much changed, instead of creating a factory and use it in viewModels
delegate we annotated with AndroidEntryPoint
. And the resource is available to us and components are less coupled now.