Hilt: God’s Response To Our Prayers | PiLove notes
Disclaimer: The goal of these notes is not to write an original commentary on this or any other topic, rather collect quotes and opinions from various sources and android experts in one place for better understanding!
What is dependency injection (DI)?
Wikipedia
Dependency injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies. The receiving object is called a client and the passed (that is, “injected”) object is called a service. The code that passes the service to the client can be many kinds of things and is called the injector. Instead of the client specifying which service it will use, the injector tells the client what service to use. The “injection” refers to the passing of dependency (a service) into the object (a client) that would use it.
The official site of Hilt and Dagger
The best classes in any application are the ones that do stuff: the BarcodeReader, and the AudioStreamer. These classes have dependencies; perhaps a BarcodeCameraFinder, DefaultPhysicsEngine, and an HttpStreamer.
In contrast, the worst classes in any application are the ones that take up space without doing much at all: the BarcodeDecoredFactory, the CameraServiceLoader, and the MutableContextWrapper. These classes are the clumsy duct tape that wires the interesting stuff together.
Dagger is a replacement for these FactoryFactory classes that implement the dependency injection design pattern without the burden of writing the boilerplate. It allows you to focus on the interesting classes. Declare dependencies, specify how to satisfy them, and ship your app.
Have you ever tried manual dependency injection in your app? Even with many of the existing dependency injection libraries today, it requires a lot of boilerplate code as your project becomes larger, since you have to construct every class and its dependencies by hand, and create containers to reuse and manage dependencies. -Manuel Vivo
By following DI principles, you lay the groundwork for good app architecture, greater code reusability, and ease of testing
Why use Hilt?
Dagger was extremely difficult to use, and dagger-android that was supposed to make implementation od DI much easier was actually a total failure, it was as complex as the original dagger, it just simply did not deliver. Hilt on the other hand absolutely did.
Hilt provides a standard way to incorporate Dagger dependency injection into an Android application.
The goals of Hilt are:
- To simplify Dagger-related infrastructure for Android apps.
- To create a standard set of components and scopes to ease setup, readability/understanding, and code sharing between apps.
- To provide an easy way to provide different bindings to various build types (e.g. testing, debug, or release).
Hilt works by code generating your Dagger setup code for you. This takes away most of the boilerplate of using Dagger and really just leaves the aspects of defining how to create objects and where to inject them. Hilt will generate the Dagger components and the code to automatically inject your Android classes (like activities and fragments) for you.
How to use Hilt?
Source: CodingWithMitch
Add to build.gradle:
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
Add to app/build.gradle:
All apps that use Hilt must contain an Application class that is annotated with @HiltAndroidApp
, because it triggers Hilt’s code generation, including a base class for your application that serves as the application-level dependency container
The application component is used to hold a reference to the app component. The dagger or dagger-android app component was used for keeping a reference to something that holds dependencies that will live for the lifetime of the application. Hilt resolve this with a single annotation.
@HiltAndroidApp
class MyApplication : Application() {
Add following line to AndroidManifest.xml:
<application
android:name = ".MyApplication"
...
HILT Field Injection and Constructor Injection
Field injection is a simpler way to do things, not many limitations or required workarounds, but constructor injection it the better way whenever possible. The main reason is that with constructor injection you’re passing parameters through the constructor therefore when that object gets instantiated you know what it needs and this is really good for production code and for testing(when building mocks).
Hilt can provide dependencies to other Android classes that have the @AndroidEntryPoint
annotation. If you annotate an Android class with @AndroidEntryPoint
then you also must annotate Android classes that depend on it. For example, if you annotate a fragment, then you must also annotate any activities where you use that fragment. @AndroidEntryPoint
generates an individual Hilt component for each Android class in your project. These components can receive dependencies from their respective parent classes.
Example of field injection:
Let’s say we want to inject parameter of type SomeClass
to our MainActivity:
class SomeClass
@Inject
constructor() {
fun doAThing(): String {
return "Look I did a thing"
}
}
Now let’s insert this class like this:
@AndroidEntryPoint
class MainActivity : AppComaptActivity() {
//field injection
@Inject
lateinit var someClass: SomeClass
Example of constructor injection:
class SomeOtherClass
@Inject
constructor() {
fun doSomeOtherThing(): String {
return "Look I did some other thing!"
}
}
Now let’s pass this class to SomeClass
as a dependency:
class SomeClass
@Inject
constructor(
private val someOtherClass: SomeOtherClass
) {
fun doAThing(): String {
return "Look I did a thing"
}
fun doSomeOtherThing(): String {
return someOtherClass.doSomeOtherThing()
}}
Behind the scenes, in the compile-time dagger is gonna create an instance of SomeOtherClass
, then it is going to build that, then dagger will create an instance of SomeClass
and pass the instance of SomeOtherClass
Scoping with HILT
Scoping allows you to “preserve” the object instance and provide it as a “local singleton” for the duration of the scoped component.
You can use scope annotations to limit the lifetime of an object to the lifetime of its component. This means that the same instance of a dependency is used every time that type needs to be provided
HILT generates all components necessary, like app component, activity component, fragment component. While it is generating all of these components it also generates scopes for all of these components, so for example the app component is automatically given the Singleton scope and this scope will live as long as the application is alive. So if dependency is annotated with Singleton it will exist as long as the application is alive.
Next, ActivityRetainedComponent(ViewModel) will receive ActivityRetainedScope. ViewModel will stay alive longer than Activity but it will die before application dies.
ActivityComponent will receive ActivityScope, and this scope will only live as an activity, so if activity dies, all the dependencies that are scoped with ActivityScope also die.
FragmentComponent will receive FragmentScope and so on.
Example of scoping (using previous examples)
//annotating this class with @Singleton means that this dependecy //will live as long as the application is alive
@Singleton
class SomeClass
@Inject
constructor() {
fun doAThing(): String {
return "Look I did a thing!"
}
}
Now let’s use FragmentScope:
@AndroidEntryPoint //meaning that this class is user of dependency
class MyFragment: Fragment() {
@Inject
lateinit var someClass: SomeClass
}
If we run the code, everything runs perfectly.
Dagger2 checks for error during the compile-time, which is better than to check for error during run-time, meaning that if there are errors in the code it will be clear when we try to build our project, not when the project is already running. Which is a huge advantage of Dagger over the KOIN (another DI)
If we swap @Singleton
annotation of SomeClass with @ActivityScoped
everything will still work fine.
But if we swap this annotation with @FragmentScoped
and try to run the project, we will receive a compile-time error and the reason for that is that we are trying to inject something that is fragment scoped into Activity.
So this is a tear system that goes downwards meaning you cannot inject something that is fragment scoped into something that is above like @ActivityScoped
Two situations where you cannot do contractors/field injections
Let’s look at another example:
What if we now add an interface like this one:
interface SomeInterface {
fun getAThing() : String
}
And make SomeDependency class to implement it:
class SomeInterfaceImpl
@Inject
constructor() : SomeInterface {
override fun getAThing() : String{
return "A Thing"
}
}
And let’s add one more change:
class SomeClass
@Inject
constructor(
private val someInterfaceImpl: SomeInterface
){
fun doAThing(): String{
return "Look I got: ${someInterfaceImpl.getAThing()}"
}
}
If we run this, we will get a compile-time error, and that’s because when we do constructor injection or field injection we cannot inject an interface.
The second situation is very similar to the first one, and it happens when we want to insert something that we don’t own, like some third-party library:
class SomeClass
@Inject
constructor(
private val someInterfaceImpl: SomeInterface,
private val gson: Gson
){
fun doAThing(): String{
return "Look I got: ${someInterfaceImpl.getAThing()}"
}
}
In both of these cases, dagger has no idea how to create these dependencies.
The solution is Hilt modules
Sometimes a type cannot be constructor-injected. This can happen for multiple reasons. For example, you cannot constructor-inject an interface. You also cannot constructor-inject a type that you do not own, such as a class from an external library. In these cases, you can provide Hilt with binding information by using Hilt modules.
A Hilt module is a class that is annotated with
@Module
. Like a Dagger module, it informs Hilt how to provide instances of certain types. Unlike Dagger modules, you must annotate Hilt modules with@InstallIn
to tell Hilt which Android class each module will be used or installed in. -from AndroidDocs
To solve this problem we can use @Provides
or @Binds
, where @Provides
is an easier option.
First, let’s consider the more complex way a@Binds
method that can't be used in all situations, to solve the first problem with interfaces. Let’s create a module to tell Hilt how to build the interface object :
@InstallIn(ApplicationComponent::class)//to install module into AppComponent
@Module
abstract class MyModule {
@Singleton
@Binds
abstract fun bindSomeDependency(
someImpl: SomeInterfaceImpl
): SomeInterface
And remove SomeClass Gson parameter:
class SomeClass
@Inject
constructor(
private val someInterfaceImpl: SomeInterface
){
fun doAThing(): String{
return "Look I got: ${someInterfaceImpl.getAThing()}"
}
}
If we run this code, it will work fine, because now Hilt knows how to create this parameter.
Interesting note, if we change ApplicationComponent to ActivityComponent we will get a compile-time error, because of the @SingletonScope
which need to change to @ActivityScoped
Now, the situation where @Binds
does not work. Let’s add back our Gson parameter:
class SomeClass
@Inject
constructor(
private val someInterfaceImpl: SomeInterface,
private val gson: Gson
){
fun doAThing(): String{
return "Look I got: ${someInterfaceImpl.getAThing()}"
}
}
And if we now provide a function to provide this parameter:
If we run this, we will receive a compile-time error, because @Binds
can’t be used for this scenario.
@Provides
method:
@InstallIn(ApplicationComponent::class)
@Module
class MyModule {
@Singleton
@Provides
fun provideSomeInterface(): SomeInterface {
return SomeInterfaceImpl()
}
And in the case that there was a parameter in SomeInterfaceImpl class, like:
class SomeInterfaceImpl
@Inject
constructor(
private val someDependency: String
) : SomeInterface {
override fun getAThing() : String{
return "A Thing"
}
}
Then we will add these lines in our module:
Also, let’s resolve Gson parameter problem:
So then what’s a difference between @Binds
and @Provides
?
@Binds
generates more efficient code than @Provides
because it never creates an implementation for that module. The method is also more concise. You can find furthermore comparison here.
How to provide instances of the same type?
What if we are providing two string for the same module. How to tell Hilt which one to use when needed? The solution is Custom annotation.
Let’s say we have a couple of classes:
Now let’s make changes so Hilt can differentiate between to interface implementations:
Now add these annotations in the constructor of SomeClass:
Please check out my main source of information about this topic: CodingWithMitch