Something about The Hilt

Jast Lai
Jastzeonic
Published in
7 min readJun 24, 2023

foreword

We all know we have a dependency injection in Android development called Dagger. And Hilt is also a dependency injection library in Android Development. What's the difference between those two? Why do we use Hilt, not just Dagger?

Why don't we use Dagger?

The Hilt is a dependency injection library for Android. More precisely to say about it is: Hilt is a dependency injection library for Android that reduces the boilerplate of doing manual dependency injection in your project.

For example, if we want to inject a module into an Activity, We will do like this:

I got an interface

interface MaskInterface {
}

I want to inject it into an Activity:

class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
@Inject lateinit var mask: MaskInterface
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
}
}

I wish it could be implemented like this.

class InjectionClass : MaskInterface {
}

So I define a way to provide the implementation.

@Module
class ProviderModule {

@Provides
fun provide(): MaskInterface {
return InjectionClass()
}
}

And I must tell Dagger we want to inject this module into this Activity. So that Dagger would know what it should generate:

@Singleton
@Component(modules = [ProviderModule::class])
interface ApplicationComponent {
fun inject(activity: MainActivity)
}

After that. We can finally inject the interface.

class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
@Inject lateinit var mask: MaskInterface
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
DaggerApplicationComponent.create().inject(this)
}
}

This is quite bothering, isn't it?

Then… How about the Hilt?

How should we start?

Of course, we will need to set some things.

In the application grade:

plugins {

id 'com.google.dagger.hilt.android' version '2.44' apply false
}

In the app grade:

plugins {

id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
dependencies {
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
}
// Allow references to generated code
kapt {
correctErrorTypes true
}

Then we need to set the Application first:

@HiltAndroidApp
class AppApplication : Application()

Or we will get an error :

The reason is simple if we don't have the Hitlapplication. Then we don't have any instance to generate any instance you want to inject.

How to inject instances in Activity?

We can use @AndroidEntryPoint annotation to tell Hilt we have some object that needs to be injected in a certain control flow.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
@Inject lateinit var mask: MaskInterface
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
}
}

And you will still need to add a provider, or Hilt won't find the method to generate an instance of your statement.

@Module
@InstallIn(ActivityComponent::class)
class ProviderModule {
@Provides
fun provide(): MaskInterface {
return InjectionClass()
}
}

If we use @inject without @AndroidEntryPoint annotation, we will get the same error as using the reserved word "lateinit" and using it without assignment.

What if we want to implement an injection that needs another injection?

interface MaskInterface2 {
}
class InjectionClass(maskInterface2: MaskInterface2) {
}
class InjectionClass2() : MaskInterface2

It will do like this:

class InjectionClass @Inject constructor(
maskInterface2: MaskInterface2
) : MaskInterface {
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var mask: InjectionClass
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
}
}

We can also add Context to the injection.

class InjectionClass @Inject constructor(
@ApplicationContext private val applicationContext: Context,
@ActivityContext private val activityContext: Context,
maskInterface2: MaskInterface2
)

How to inject instances in Fragment?

Suppose we want to inject an object in Fragment. It's almost the same as Activity, we just use the same annotation @AndroidEntryPoint, and we can use the same provider we use to inject in Activity.

@AndroidEntryPoint
class FirstFragment : Fragment() {
@Inject
lateinit var mask: MaskInterface
}

How to inject viewModel in Activity or Fragment?

There is a little trick in this part. In MVVM Architecture. We usually need to inject some instance and use some method to let Activity, Fragment, or control flow use it.

Because ViewModel has a longer Lifecycle than Activity. So does have different ways to invoke it in Activity. We use ViewModelFactory to get ViewModel in Activity if we need. It will look like this.

val viewModel: SampleViewModel by viewModels()

Without the Hilt. We need to create a factory class to let the compiler know how to construct the ViewModel. It's boilerplate work. But with Hilt. We can use @HiltViewModel to let Hilt know how to construct the viewModel

@HiltViewModel
class SampleViewModel
@Inject constructor(): ViewModel() {
}

Or we may have some injection used by the ViewModel:

@HiltViewModel
class SampleViewModel @Inject constructor(
@ApplicationContext private val applicationContext: Context,
@ActivityContext private val activityContext: Context,
maskInterface2: MaskInterface2
) : ViewModel() {
}

Remember @Inject constructor is necessary even though you don't need any field. Hilt needs it to know how to construct the ViewModel. It needs to be marked.

That's all. You can use viewModels() to invoke ViewModel in Activity and Fragment.

How to inject the Activity instance between Fragments?

You might notice when we write a provider. We have @Module annotation, we have @Provides annotation, and we also have @InstallIn(ActivityComponent::class).

This annotation declares the lifecycle of the instance provider by annotated class. In this case, we can see ActivityComponet::class. That means the lifecycle of this instance provided by the annotated class is between Activity onCreate and onDestroy.

That also means we can use this provider with the same instance between two Fragments in the same Activity.

Wait … What?

It's easy to make people so confused here. You annotate InstallIn(ActivityCompont::class) in a class doesn't mean the class provides the instance that lifecycle binds with Activity.

We still have one step to let the provider provide the instance lifecycle bind with Activity. We need to use @ActivityScoped.

@Module
@InstallIn(ActivityComponent::class)
class ProviderModule {
@ActivityScoped
@Provides
fun provide(): MaskInterface {
return InjectionClass()
}
}

That means the instance provided by this method will live with the Activity.

So you can share the same instance between two Fragments owned by one Activity like this using @ActivityScoped

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var mask: MaskInterface
}
@AndroidEntryPoint
class FirstFragment : Fragment() {
@Inject
lateinit var mask: MaskInterface
}
@AndroidEntryPoint
class SecondFragment : Fragment() {
@Inject
lateinit var mask: MaskInterface
}

How is the injection instance lifecycle?

Sometimes, we may want to provide an instance live with Fragment. Or maybe live with a ViewModel or even a View. Yap~ Hilt has supported this. I got some tables from the Android Developer's official document.

Those tables show how to use the lifecycle scope at Hilt. It's very obvious how to use the kind with @Singleton @ActivitySoped @ViewModelScoped @FragmentScoped.

But I wonder how to use @ViewScpoend

It's just like @FragmentScoped

@Module
@InstallIn(ViewComponent::class)
class ProviderModule {
@ViewScoped
@Provides
fun provide(): MaskInterface {
return InjectionClass()
}
}
@AndroidEntryPoint
class CustomView(context: Context, attrs: AttributeSet? = null) :
View(context, attrs) {
@Inject
lateinit var mask: MaskInterface
}

It should work.

But I wonder when we should use it like this.

There is a scenario if the Fragment creates a view.

The Fragment would destroy the view frequently but not destroy itself, so we need to let the LiveData observer viewLifecycleOwner instead Fragment's lifecycle in some cases.

According to this thinking. We may have a scenario needing instance life bind with fragment view. It seems like the previous example.

Then why do we need @WithFragmentBindings? It seems viewScope already satisfies fragment view's lifecycle. But we may want to restrict view usage only on Fragment because it may design to align by Fragment. Using @WithFragmentBidnings means this injection object must always be attached through a fragment. Or it will throw an exception like this if we use this view but not inflate by Fragment.

How to inject different instances with the same interface.

Sometimes, we may need to inject different instances with the same interface.

class LocalInstance @Inject constructor(
) : MaskInterface

class RemoteInstance @Inject constructor() : MaskInterface@Module
@InstallIn(ActivityComponent::class)
class ProviderModule {
@ActivityScoped
@Provides
fun provideLocal(): MaskInterface {
return LocalInstance()
}
@ActivityScoped
@Provides
fun provideRemote(): MaskInterface {
return RemoteInstance()
}
}

Then we got an error.

How should we split those two?

First, we need to add two annotation class

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Local

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Remote

Then we can use it to declare provided method:

@Module
@InstallIn(ActivityComponent::class)
class ProviderModule {
@ActivityScoped
@Local
@Provides
fun provideLocal(): MaskInterface {
return LocalInstance()
}

@ActivityScoped
@Remote
@Provides
fun provideRemote(): MaskInterface {
return RemoteInstance()
}
}

Then we should be able to inject the kind of instance that we want by those annotations

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
@Local
lateinit var localInterface: MaskInterface

@Inject
@Remote
lateinit var remoteInterface: MaskInterface
}

Summary

Compare with Dagger. It seems like Hilt is a lot easier to use and understand than Dagger. Well, it's a good thing. Although It still makes people confuse when we see the error message from the compiler when we use something wrong or in some way wrong .

In my opinion. Koin is still my priority. But it’s still worth using Hilt when you have some dependency injection requirements.

Reference

https://developer.android.com/training/dependency-injection/hilt-android

--

--

Jast Lai
Jastzeonic

A senior who happened to be an Android engineer.