Understanding Dependency Injection with Hilt.
Today, we will be talking about dependency injections in android and how useful it is to have dependency injection in android app architecture. Let’s have an overview of Dependency Injection.
What is Dependency Injection?
Dependency Injection is seen to be a widely used technique in android programming and it is proven to be a good practice when this technique is followed well in android app architecture. In programming sometimes, some class, classA requires references to another class, ClassB. In this instance, classB is the dependency to classA.
public class Car {private Engine engine; public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
}}
In this instance, we could say Engine is a dependency to a car. It is wise to do this to save you in the future when a car can take different types of Engine, like Electric Engine, Manual Engine and so on. This way, the caller of Car object or the parent can provide its own engine type and give it to the car.
Think about how difficult it will be when the engine is prepared locally for the car. Then in future, should the engine type change, we would have to touch all classes having a Car as an object.
public class Car {
private Engine engine = new Engine();
public void start() {
engine.start();
}
}
Dependency Injection could have below benefits:
- Reusability of code. When dependency injection is allowed in app architecture, your piece of code stands the benefits of reusability across the entire app. A block of code which serves as a dependency yet does not change can be written once and it is served across the app when needed.
- Ease of Refactoring. Since we have a single source of this dependency, we can change the root or refactor and the effect is cascaded across to its dependencies.
- Improve Testing. One of the benefits of dependency is how it is used for testing. When writing a test for your classes and your class depends on data from remote or database source, dependency injection library can help you provide this dependency as either for testing or real application.
Ways Dependency Injection can happen
Dependency Injection could happen in the following ways.
- Through constructor Injection. This is where the dependency is provided to the constructor as a parameter.
- Through field Injection. This also happens when a field is declared but the instance is provided automatically without the help of the class object.
How Dependency Injection is created.
Now we know about Dependency Injection, let’s talk about how it is created.
We can use manual ways of creating Dependency Injection in our apps but this sometimes becomes problematic when we have a big app. In a big app, creating dependency injection manually presents some problems as to how to manage the dependencies. In a multi-layered architecture where one layer depends on another layer, using a top layer will require us to create all the necessary dependencies for the top layer to work well. Testing this flow too can cause issues.
We can also use automatic ways of injecting dependencies in our app development by using libraries. These libraries fit into two main categories:
- Reflection-based solutions that connect dependencies at runtime. Example of this solution is using the Guice library.
- Static solutions that generate codes to connect dependencies at compile time. Example is using Dagger2 library which is now managed by Google and a jetpack composed library called Hilt.
Watch this tutorials on Overview of Dependency Injection and subscribe to my channel and this content was created side by side.
What is Hilt?
Hilt is a dependency injection library that reduces a boilerplate code of doing a manual dependency injection in your project. Doing a manual injection requires you to construct every dependency by hand or organise the dependencies in a container for it to be reused and manage the dependencies.
Hilt allows you to use dependency Injection in your app and creates a container of the dependencies and automatically manages the lifecycle of the dependencies. Hilt is built on a famous DI library, Dagger2, and benefits from the compile-time correctness, runtime performance, scalability and android studio support that dagger provides.
Setting Up Hilt Dependency in Android Studio
- First, add the hilt-android-gradle-plugin plugin to your project’s root build.gradle file:
2. Then, apply the Gradle plugin and add these dependencies in your app/build.gradle file:
- Hilt uses Java 8 features. To enable Java 8 in your project, add the following to the app/build.gradle file:
Application Class
All android apps that use Hilt should have an application class and it must be annotated with @HiltAndroidApp. The Annotation triggers Hilt code generation and includes the base class for the application that serves as the application-level dependency container.
This generated Hilt component is attached to the Application object’s lifecycle and provides dependencies to it. Additionally, it is the parent component of the app, which means that other components can access the dependencies that it provides.
Watch this video with live demonstration of how Hilt is setup on my youTube Channel and don’t forget to subscribe.
Injecting Dependencies into Android Classes
Once Hilt is set up in your Application class and an application-level component is available, Hilt can provide dependencies to other Android classes that have the @AndroidEntryPoint annotation:
Hilt Modules
Hilt modules are types of a class that is used by Hilt to provide an instance of an object you want to call. There are multiple reasons why you might need Module class for Hilt to use in it’s injection.
- When the class is not owned by you. For example, using a library has different ways of calling it in an implementation, so you will need to provide it in a module class for Hilt to use.
- When using an abstract or interface class. You will need to provide its implementation in a module class.
Let’s see how to provide an instance of a library for Hilt usages in a module class.
Let’s briefly talk about these new annotations.
- @Provides is an annotation that tells Hilt how a certain binding should be provided during injections.
- @Module is an annotation that tells Hilt the instances of bindings we want to provide on our own.
- @InstallIn is an annotation which is used to tell Hilt in which of the components to install our Module.
Injecting instances of Interfaces and abstract Class in Hilt
Since interfaces and abstract classes are not concrete classes, they cannot be instantiated so in Hilt, we use a binding annotation to tell Hilt how to provide these implementations.
Providing Multiple Bindings for the Same Type
During development, you might need to perform different configurations on a certain library before using it. One best example is using a retrofit for network calls but having different interceptors. We use @Qualifier annotation to annotate the @Provide and @Binds.
Let’s create one example of a Qualifier annotation
After that we use the above annotation qualifier to mark our dependencies to create the differences.
Now after marking our dependencies with the qualifiers, we can call them uniquely with the qualifier created.
Or it can be called as a constructor injector or as a field injector.
NB: Never forget to add these qualifiers to all the dependencies you want to inject into your class lest Hilt might inject wrong dependencies.
Components for Android Classes
Hilt provides components that make you be able to inject dependencies into your android classes. The following are some of the predefined components and their domains.
- SingletonComponents — — Application
- ViewModelComponents — — ViewModel
- ActivityComponents — — Activity
- FragmentComponents — — Fragment
- ViewComponents — — View
- ViewWithFragmentComponent — — View with @WithFragmentsBindings
- ServiceComponent — — — Service
NB: If you are looking through for how to inject Dependencies into broadcast Receivers, use SingletonComponents.
Components Lifecycle
Components Scope
All the dependencies created by Hilt are all unscoped. In a sense that, whenever a particular dependency is requested for an operation, Hilt creates a fresh instance of that dependency. In this instance, if we want Hilt to use one instance across a concern activity, fragment, view, service etc, then we need to scope the dependencies to achieve the purpose.
Let’s look at the various predefined scopes provided by Hilt.
Components Hierarchy
Use the following hierarchy to know how dependencies declared in components are managed. The hierarchy grows from top to down. The child components can have access to the parents dependencies in the parents components.
Level 1.
- @Singleton — SingletonComponent
Level 2
- @ActivityRetainedScoped — ActivityRetainedComponent
- @ServiceScoped — ServiceComponent
Level 3
- @ActivityScoped — ActivityComponent
- @ViewModelScoped — ViewModelComponent
Level 4
- @FragmentScoped — FragmentComponent
- @ViewScoped — — ViewComponent
Level 5
- @ViewScoped — ViewWithFragmentComponent
Injecting dependencies into Classes not supported by Hilt
There are some classes like ContentProvider class Hilt does not support directly and if you need to inject dependencies into those classes then follow below steps.
Step 1
- Declare an interface class with @InstalledIn annotation with the relevant component that contains the dependency.
- Annotate that particular interface with @EntryPoint annotation
3. Use the appropriate entryPointAccessor class static method to retrieve the dependency.
Injecting Hilt Into ViewModel
Provide a ViewModel by annotating it with @HiltViewModel and using the @Inject annotation in the ViewModel object’s constructor.
Then, an activity or a fragment that is annotated with @AndroidEntryPoint can get the ViewModel instance as normal using ViewModelProvider or the by viewModels() KTX extensions:
Using Hilt in Multi-Module apps
Hilt code generation needs access to all the Gradle modules that use Hilt. The Gradle module that compiles your Application class needs to have all Hilt modules and constructor-injected classes in its transitive dependencies.
If the different module is not a feature module, we can just follow the normal way of creating the Hilt component module. This is because the other modules are going to be dependencies of the app module.
Using Hilt in Feature Modules
In feature modules, the way that modules usually depend on each other is inverted. Therefore, Hilt cannot process annotations in feature modules. You must use Dagger to perform dependency injection in your feature modules.
You must use component dependencies to solve this problem with feature modules. Follow these steps:
- Declare an @EntryPoint interface in the app module (or in any other module that can be processed by Hilt) with the dependencies that the feature module needs.
- Create a Dagger component that depends on the @EntryPoint interface.
- Use Dagger as usual in the feature module.
Suppose you add a login feature module to your project. You implement the login feature with an activity called LoginActivity. This means that you can get bindings only from the application component.
For this feature, you need an OkHttpClient with the authInterceptor binding.
First, create an @EntryPoint interface installed in the SingletonComponent with the bindings that the login module needs:
To perform field injection in the LoginActivity, create a Dagger component that depends on the @EntryPoint interface:
Once those steps are complete, use Dagger as usual in your feature module. For example, you can use the bindings from the SingletonComponent as a dependency of a class:
To perform field injection, create an instance of the Dagger component using the applicationContext to get the SingletonComponent dependencies:
Enjoy with easy and have a happy coding!!!. Much love and for more interesting articles on android, you can visit my channel on youTube.
References