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!

PiLove
Programming Geeks
8 min readJul 19, 2020

--

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:

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.

Add following line to AndroidManifest.xml:

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:

Now let’s insert this class like this:

Example of constructor injection:

Now let’s pass this class to SomeClass as a dependency:

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.

A tier system

Example of scoping (using previous examples)

Now let’s use FragmentScope:

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:

And make SomeDependency class to implement it:

And let’s add one more change:

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:

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 :

And remove SomeClass Gson parameter:

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:

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:

And in the case that there was a parameter in SomeInterfaceImpl class, like:

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

--

--