Post

Hilt to the rescue, part 1

Hilt is a dependency injection library for Android that reduces the boilerplate of doing manual dependency injection in your project, it also means that Hilt is just an abstraction on top of Dagger2 and it gives us some of the goodies we’ve wrote already in the previous parts but we can safely delete them now.

In the previous posts (Part #1, Part #2, Part #3, Part #4, Part #5, Part #6 and Part #7) about Dagger2, we’ve learnt that it ain’t that hard, Android runtime makes it, fast forwarding that to few months down the road, we’ve received Hilt 1.0.0 stable.

So far we’ve learnt about Modules, Submodules, Components and SubComponents, Scopes, custom Keys and MultiBindings.

Hilt Dependencies


First we have to include the necessary dependencies (as of the date when this post was written)

In the project gradle

1
2
3
4
ext {
    hiltJetpackVersion = '1.0.0'
    daggerVersion = '2.35.1'
}

again in the project gradle file, add the classpath

1
2
3
dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
    }

App’s gradle

1
2
3
4
5
6
7
8
9
10
11
12
    //dagger
    implementation "com.google.dagger:dagger:$daggerVersion"
    kapt "com.google.dagger:dagger-compiler:$daggerVersion"

    //hilt
    implementation "com.google.dagger:hilt-android:$daggerVersion"
    kapt "com.google.dagger:hilt-android-compiler:$daggerVersion"

    //hilt jetpack
    implementation "androidx.hilt:hilt-navigation-fragment:$hiltJetpackVersion"
    kapt "androidx.hilt:hilt-compiler:$hiltJetpackVersion"

You can omit the hilt jetpack if you’re not using the jetpack components, in the following parts i’ll be talking about that too, so i’m including it beforehand.

Don’t forget to apply the gradle plugin id

1
2
3
4
5
plugins {
    .
    .
    id 'dagger.hilt.android.plugin'
}

Setup


One of the most important things to note when using hilt is @HiltAndroidApp, this is your entry point to your graph and has to be present in your : Application.

We can go ahead and change our previously written DaggerIsEasyApplication from

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DaggerIsEasyApplication : Application() {

    lateinit var applicationComponent: SingletonComponent
        private set

    @Inject
    lateinit var sharedPreferencesManager: SharedPreferencesManager

    override fun onCreate() {
        applicationComponent = DaggerSingletonComponent.factory().create(this).also { it.provideGraphInside(this) }
        super.onCreate()

        if (sharedPreferencesManager.firstTimeLaunchSinceInstall) {
            Log.d(this::class.java.simpleName, "App is launched for the first time since installation time")
            sharedPreferencesManager.appHasBeenLaunchedForTheFirstTime()
        }
    }
}

to

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@HiltAndroidApp
class DaggerIsEasyApplication : Application() {

    @Inject
    lateinit var sharedPreferencesManager: SharedPreferencesManager

    override fun onCreate() {
        super.onCreate()

        if (sharedPreferencesManager.firstTimeLaunchSinceInstall) {
            Log.d(this::class.java.simpleName, "App is launched for the first time since installation time")
            sharedPreferencesManager.appHasBeenLaunchedForTheFirstTime()
        }
    }
}

Hilt generates an entry point by just looking at the annotation we’ve just added, from there on Hilt generates a graph for your application, a graph that has a hierarchy of components Component's hierarch

Hilt components

The are the pre-defined components by the Hilt dependencies.

SingletonComponent


Created when onCreate happens in your : Application terminated when onDestroy happens inside the application level, this is a parent component which means upon it’s destruction every other component that inherits from it, is destroyed with it.

This is where your singleton instances will live.

You can use the scope @Singleton to annotate every dependency that lives here.

You use @InstallIn(SingletonComponent::class) to include a module inside the component.

ActivityRetainedComponent


ActivityRetainedComponent lives across configuration changes, so it is created at the first onCreate and last onDestroy it also inherits everything provided by the SingletonComponent, since it is it’s child.

Injector for your Activity.

You can use the scope @ActivityRetainedScoped to annotate every dependency that will live within that scope.

You use @InstallIn(ActivityRetainedComponent::class) to include a module inside the component.

ServiceComponent


Created when onCreate happens inside the Service and destroyed when onDestroy happens, it also inherits everything provided by the SingletonComponent, since it is it’s child.

Injector for your Service.

You can use the scope @ServiceScoped to annotate every dependency that will live within that scope.

You use @InstallIn(ServiceComponent::class) to include a module inside the component.

ActivityComponent


ActivityComponent DOESN’T across configuration changes, it is created on every onCreate and destroyed at onDestroy it also inherits everything provided by the SingletonComponent, since it is it’s child.

Injector for your Activity.

You can use the scope @ActivityScoped to annotate every dependency that will live within that scope.

You use @InstallIn(ActivityComponent::class) to include a module inside the component.

ViewModelComponent


Created when onCreate happens inside the Activity and cleared when onDestroy happens, it also inherits everything provided by the ActivityRetainedComponent (which inherits the Singleton), since it is it’s child.

Injector for your ViewModel.

You can use the scope @ViewModelScoped to annotate every dependency that will live within that scope.

You use @InstallIn(ViewModelComponent::class) to include a module inside the component.

FragmentComponent


Created when onAttach happens and destroyed when onDestroy happens in the Fragment, it also inherits everything provided by the SingletonComponent, since it is it’s child.

Injector for your Fragment.

You can use the scope @FragmentScoped to annotate every dependency that will live within that scope.

You use @InstallIn(FragmentComponent::class) to include a module inside the component.

ViewComponent


Created when view.super() happens and destroyed when the view is destroyed, it also inherits everything provided by the SingletonComponent, since it is it’s child.

Injector for your View.

You can use the scope @ViewScoped to annotate every dependency that will live within that scope.

You use @InstallIn(ViewComponent::class) to include a module inside the component.

ViewWithFragmentComponent


Created when view.super() happens and destroyed when the view is destroyed, it also inherits everything provided by the SingletonComponent, since it is it’s child, you also have access to fragment bindings, this view must always be attached through a fragment.

Injector for your View.

You can use the scope @ViewScoped to annotate every dependency that will live within that scope.

You use @InstallIn(ViewWithFragmentComponent::class) to include a module inside the component.

For the fragment bindings @WithFragmentBindings.

By default, all bindings in Dagger are “unscoped”. This means that each time the binding is requested, Dagger will create a new instance of the binding.

When we say bindings we mean the dependencies you declare using @Provides and/or @Binds.

In order to access the scopes, in your Activity, Fragment, Service, View etc.. you must annotate the class with @AndroidEntryPoint.

Activity

We had the following code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class MainActivity : AppCompatActivity() {

    init {
        addOnContextAvailableListener { availableContext->
            injector {
                activityComponentFactory().create(availableContext).also { it.inject(this@MainActivity) }
            }
        }
    }

    @Inject
    lateinit var powerReceiver: PowerReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val filter = IntentFilter(Intent.ACTION_POWER_CONNECTED).also {
            it.addAction(Intent.ACTION_POWER_DISCONNECTED)
        }
        registerReceiver(powerReceiver,filter)

        if (savedInstanceState == null) {
            supportFragmentManager
                .beginTransaction()
                .add(R.id.fragment_container, HomeFragment(), "HomeFragment")
                .commit()
        }
    }

}

let’s convert it to Hilt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var powerReceiver: PowerReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val filter = IntentFilter(Intent.ACTION_POWER_CONNECTED).also {
            it.addAction(Intent.ACTION_POWER_DISCONNECTED)
        }
        registerReceiver(powerReceiver,filter)

        if (savedInstanceState == null) {
            supportFragmentManager
                .beginTransaction()
                .add(R.id.fragment_container, HomeFragment(), "HomeFragment")
                .commit()
        }
    }

}

and also delete the following: ActivityComponent, ActivityContext, ActivityModule, ActivityScoped.

Fragment


Let’s refactor our Fragment, which looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class HomeFragment : Fragment(R.layout.home_fragment) {

    override fun onAttach(context: Context) {
        injector {
            fragmentComponentFactory().create().inject(this@HomeFragment)
        }
        super.onAttach(context)
    }

    @Inject
    lateinit var daggerViewModelFactory: ViewModelProvider.Factory

    private val testViewModel by viewModels<TestViewModel>(){
        daggerViewModelFactory
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED){
            testViewModel.list.collect {
                handleAPICall(it)
            }
        }
    }

    private fun handleAPICall(simpleResult: TestViewModel.SimpleResult<List<TestModel>>) {
        when(simpleResult){
            is TestViewModel.SimpleResult.ApiError -> {
                Log.d("SimpleResult.ApiError", "Response code ${simpleResult.responseCode}")
            }
            is TestViewModel.SimpleResult.Exception -> {
                when(simpleResult.throwable){
                   is UnauthorizedInterceptor.UnauthorizedException->{
                       Log.d("Exception", "Log off")
                   }
                }
            }
            TestViewModel.SimpleResult.Loading -> {
                Log.d("SimpleResult.Loading", "SHOW SPINNER")
            }
            is TestViewModel.SimpleResult.Success -> {
                val successData = simpleResult.data
                Log.d("SimpleResult.Success", successData.toString())
            }
        }
    }
}

to

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@AndroidEntryPoint
class HomeFragment : Fragment(R.layout.home_fragment) {

    private val testViewModel by viewModels<TestViewModel>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED){
            testViewModel.list.collect {
                handleAPICall(it)
            }
        }
    }

    private fun handleAPICall(simpleResult: TestViewModel.SimpleResult<List<TestModel>>) {
        when(simpleResult){
            is TestViewModel.SimpleResult.ApiError -> {
                Log.d("SimpleResult.ApiError", "Response code ${simpleResult.responseCode}")
            }
            is TestViewModel.SimpleResult.Exception -> {
                when(simpleResult.throwable){
                   is UnauthorizedInterceptor.UnauthorizedException->{
                       Log.d("Exception", "Log off")
                   }
                }
            }
            TestViewModel.SimpleResult.Loading -> {
                Log.d("SimpleResult.Loading", "SHOW SPINNER")
            }
            is TestViewModel.SimpleResult.Success -> {
                val successData = simpleResult.data
                Log.d("SimpleResult.Success", successData.toString())
            }
        }
    }
}

and also delete the following: FragmentComponent, FragmentModule, FragmentScoped.

Also let’s delete SingletonComponent and ApplicationContext and our DaggerExtensions.kt.

Let’s go and delete more stuff: DaggerViewModelFactory, ViewModelKey and ViewModelModule.

Delete: BroadcastComponent and BroadcastScoped (we’ll get more on that later).

Now we do a little bit of adjusting to the RetrofitModule, we’ve had

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Module
object RetrofitModule {


    @Singleton
    @Provides
    fun moshiConverterFactory() = MoshiConverterFactory.create()


    @Singleton
    @Provides
    fun okHttpClientConfiguration(
        interceptors: Set<@JvmSuppressWildcards Interceptor>
    ): OkHttpClient {
        val timeout = 10L //even this can be provided
        val timeUnit = TimeUnit.SECONDS //even this too, but for the sake of keeping this short we  aren't
        val client = OkHttpClient().newBuilder()
            .apply {
                connectTimeout(timeout, timeUnit)
                callTimeout(timeout, timeUnit)
                readTimeout(timeout, timeUnit)
                writeTimeout(timeout, timeUnit)
            }
        interceptors.forEach {
            client.addInterceptor(it)
        }
        return client.build()
    }

    @Provides
    @Singleton
    fun retrofitClient(
        moshiConverterFactory: MoshiConverterFactory,
        okHttpClient: OkHttpClient

    ) = Retrofit.Builder()
        .addConverterFactory(moshiConverterFactory)
        .client(okHttpClient)
        .baseUrl(TestApi.BASE_URL)
        .build().create<TestApi>()
}

and now we have

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Module
@InstallIn(SingletonComponent::class)
object RetrofitModule {


    @Singleton
    @Provides
    fun moshiConverterFactory() = MoshiConverterFactory.create()


    @Singleton
    @Provides
    fun okHttpClientConfiguration(
        interceptors: Set<@JvmSuppressWildcards Interceptor>
    ): OkHttpClient {
        val timeout = 10L //even this can be provided
        val timeUnit = TimeUnit.SECONDS //even this too, but for the sake of keeping this short we  aren't
        val client = OkHttpClient().newBuilder()
            .apply {
                connectTimeout(timeout, timeUnit)
                callTimeout(timeout, timeUnit)
                readTimeout(timeout, timeUnit)
                writeTimeout(timeout, timeUnit)
            }
        interceptors.forEach {
            client.addInterceptor(it)
        }
        return client.build()
    }

    @Provides
    @Singleton
    fun retrofitClient(
        moshiConverterFactory: MoshiConverterFactory,
        okHttpClient: OkHttpClient

    ) = Retrofit.Builder()
        .addConverterFactory(moshiConverterFactory)
        .client(okHttpClient)
        .baseUrl(TestApi.BASE_URL)
        .build().create<TestApi>()
}
1
@InstallIn(SingletonComponent::class)

Means that everything in this module will be scoped to the SingletonComponent that we’ve mentioned above.

Let’s go and annotate our InterceptorsModule with the same annotation

1
2
3
4
5
6
@Module
@InstallIn(SingletonComponent::class)
object InterceptorsModule {
.
.
.

ViewModel

Next to refactor is our TestViewModel which looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class TestViewModel @AssistedInject constructor(
    private val testApi: TestApi,
    @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    @AssistedFactory
    interface TestViewModelFactory {
        fun create(savedStateHandle: SavedStateHandle): TestViewModel
    }

    private val listData: MutableStateFlow<SimpleResult<List<TestModel>>> =
        MutableStateFlow(SimpleResult.Loading)
    val list = listData.asStateFlow()

    init {
        viewModelScope.launch(Dispatchers.IO) {
            listData.value = try {
                transformPosts(testApi.getPostsAdapter())
            } catch (throwable: Throwable) {
                SimpleResult.Exception(throwable)
            }
        }

        Log.d("HANDLE", savedStateHandle.keys().toString())
    }

    private fun transformPosts(response: Response<List<TestModel>>): SimpleResult<List<TestModel>> =
        if (response.isSuccessful) {
            SimpleResult.Success(response.body() ?: emptyList())
        } else {
            SimpleResult.ApiError(response.code(), response.errorBody())
        }


    sealed class SimpleResult<out T> {
        data class Exception(val throwable: Throwable) : SimpleResult<Nothing>()
        data class ApiError(val responseCode: Int, val errorBody: ResponseBody?) :
            SimpleResult<Nothing>()

        data class Success<T>(val data: T) : SimpleResult<T>()
        object Loading : SimpleResult<Nothing>()
    }
}

and now we have

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@HiltViewModel
class TestViewModel @Inject constructor(
    private val testApi: TestApi,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val listData: MutableStateFlow<SimpleResult<List<TestModel>>> =
        MutableStateFlow(SimpleResult.Loading)
    val list = listData.asStateFlow()

    init {
        viewModelScope.launch(Dispatchers.IO) {
            listData.value = try {
                transformPosts(testApi.getPostsAdapter())
            } catch (throwable: Throwable) {
                SimpleResult.Exception(throwable)
            }
        }

        Log.d("HANDLE", savedStateHandle.keys().toString())
    }

    private fun transformPosts(response: Response<List<TestModel>>): SimpleResult<List<TestModel>> =
        if (response.isSuccessful) {
            SimpleResult.Success(response.body() ?: emptyList())
        } else {
            SimpleResult.ApiError(response.code(), response.errorBody())
        }


    sealed class SimpleResult<out T> {
        data class Exception(val throwable: Throwable) : SimpleResult<Nothing>()
        data class ApiError(val responseCode: Int, val errorBody: ResponseBody?) :
            SimpleResult<Nothing>()

        data class Success<T>(val data: T) : SimpleResult<T>()
        object Loading : SimpleResult<Nothing>()
    }
}

@HiltViewModel’s annotation makes the view model aware of the SingletonComponent and ActivityRetainedComponent dependencies, which in this case

1
private val savedStateHandle: SavedStateHandle

is a default one alongside

1
private val application: Application

from the factory that’s generated for each view model.

ApplicationContext

if we build our project, we will notice that we deleted our @ApplicationContext qualifier, but no worries, Hilt comes with the same Qualifier for accessing the application context, head over to the SharedPreferencesManager and just import

1
import dagger.hilt.android.qualifiers.ApplicationContext

Clean the project and build it, notice that we deleted most of the things and changing just minimal things in order for everything to work the way it was without a lot of boilerplate, Hilt does that for you.

Downside


The downside to this is KAPT, which adds to the build time, hopefully in the future we’ll get KSP migration which’ll help a lot with the build overhead.

Up next


In the next part we’ll see how to add our custom component, then how we can access dependencies from other modules using Hilt, assisted inject with Hilt, after that we’ll explore how Hilt plays with Compose UI or we should be looking at other solutions.

PS: The Android team provided a cheat sheet for Hilt.

This post is licensed under CC BY 4.0 by the author.