Dagger-Hilt Dependency Injection on Android
What is Dependency Injection?
Before heading on to the what let’s stick to the why. I would like to see Dependency Injection as vaccination for your project and when do you get it done? Right when the project is a baby. I know that it involves a huge learning curve and you must be wondering why should you do it with any project, well you are not forced to adapt to DI, you start a project thinking it is small and it solves a specific set of problems without the need for thinking ahead that there might be more new problems in the future that requires more modules to be built on your app. Complexity increases over time even if you follow a really good architecture with clean code, so DI is basically to avoid that.
Dependency Injection (DI) is a system that ensures the simplicity of your app’s architecture and lets you forget about the worries about the scalability of your app. S.O.L.I.D principles help you make the code simple, dependency injection makes the architecture simple. It prevents your code from getting more complex in the future and easily manageable. Dagger2 and Koin are one of the most used dependency injection frameworks, Hilt is fairly new to the game but my hopes are up because even at its early stages, it has promised a lot of good things.
Why Hilt?
Boilerplates
One thing that fears most of the developers to adapt to dependency injection, (even me back in the days), is that DI by itself is complex, hard to configure, and requires a lot of things to look at and put together for it to work. Google always ensures eliminating boilerplates in their frameworks right at the start or in the middle for some frameworks, just like how they managed to bring Kotlin as the go-to language for Android instead of Java. Dagger2 has a lot of boilerplates, right from modules, providers to injections, you have to write them all with respect to how you want them to work. Hilt eliminates all the boilerplates and does all the work for you with the help of a set of pre-defined components and scopes. No more creating components by yourself and creating a component factory.
Complexity
Dagger2 is a complex library that does DI by annotations. So, you have components, modules, scopes, and ways to inject them in different parts of your application which can be done in multiple ways. The problem with Dagger2 is, there is no one way in achieving something, so, when a new developer looks at your code, a base knowledge on the components and how you did it is required for them to understand the code better. Hilt, on the other hand, has wrapped all those different possibilities and gives you a clear cut way of implementing DI in your application. One look at it, and you can understand what’s happening under the hood.
ViewModel Injections
Injecting ViewModels in your Activities was the hardest part with DI using Dagger2. You had to create a ViewModel factory that took in the type and with map bindings and then injecting it via the component in your Activity — I even get tired when explaining it to people. Well, all that has vanished with Hilt. All that it takes is a @ViewModelInject for your constructor injections and a “by viewModels() ktx” extension in your activity. Yup, it’s that good.
Hilt Application
With Dagger2, it is mandatory that you create your own components and map it to the required modules before trying to inject the module functionalities, so you go ahead and create an application-level component mapped to the modules and you write a Factory provider that helps to inject classes across the specified scope.
You need to create components for each scope types and you may also need to do custom scopes based on your requirements. We will deep dive into what components and modules are, later on when we discuss them with respect to Hilt but here’s the difference in creating an application-level component between Dagger2 and Hilt. With Dagger2, you need an AppComponent that has DaggerApplication access and you need to extend your Application class with DaggerApplication.
With Hilt, all you need to do is annotate your Application class with @HiltAndroidApp annotation and that’s it, done, Hilt takes care of all the rest of the boilerplates by itself. Isn’t this fun? There’s more coming our way, stay tuned.
Hilt Components
Components are basically different parts of your application at the top-level which may have sub-components under them or modules that are installed under them. Components help the DI framework to generate the contract for you.
Dagger2 requires you to instantiate components directly which is complex to do and repetitive. Rest assured, Hilt packs in with a set of pre-defined components that comes to the rescue. We don’t anymore have to worry about all the components that we had to create and keep track of, you are asked to choose between the components that are already there. This is a good approach to programming as it standardizes your way of approach to DI overall and it is easy to adapt between different developers and easy to onboard basically anybody into your app.
Each component available in Hilt comes with a Scope by itself which derives from its name and easy to understand. For example, ActivityComponent is @ActivityScoped. Here are all the pre-defined components that are readily available for you to use from Hilt. The hierarchies and scopes are accessible just like what is found in the graph below, a provided service declared in the ApplicationComponent is accessible right down till ViewComponents. This is fairly simple rather than defining your own components which mostly becomes a mess at some point.
Hilt Modules
A Module is installed in a Hilt Component by using @Module and @InstallIn(ApplicationComponent::class) and that’s it, you don’t need to mention the module name in any component class.
You can do any number of providers in a Module that takes care of providing the service when required if the scope is satisfied. Which means, you can’t inject a provided service by a module installed in ActivityComponent in a code block that has an application-level scope.
@Provides helps you to write the provider for a particular service. @Singleton is to specify that the provider is always accessible across the mentioned scope (component) since all components are by default mapped to a scope.
Hilt Entry Points
Entry Points allow Hilt to know where exactly to inject what. You can use @AndroidEntryPoint on Activities, Fragments, Views, Services, and BroadcastReceivers. You can use this annotation to start using the injected values right away. You get access to modules depending on the scope of your entry points. For example, an entry point on an activity will allow you to use all the provided services specified on the activity as well as application modules.
@Inject helps you to get the values from the providers, yes, it's that easy, you will never have the need to instantiate/create your components at any point during the development when Hilt is being used. @Inject can also be used for construction injections.
@ViewModelInject is used to inject repository/use-case on your constructor in a ViewModel
Testing in Hilt
Testing is also easy with Hilt as it provides separate bindings for your tests by using specific Hilt test tests and rules. You get to use annotations like @HiltAndroidTest, @Rule — HiltAndroidRule, and HiltTestApplication for testing your application class.
Thank You for reading
That concludes this article. I believe I’ve covered the basics of Hilt and I hope that I was of help. Thanks for your time and please upvote if you liked the read, I’d appreciate it. You can find the sample project where I’ve implemented Hilt below.
Github Profile: https://github.com/tizisdeepan
LinkedIn Profile: www.linkedin.com/in/tizisdeepan
Happy Coding!