Using Hilt in Your Android App

Learn via video courses
Topics Covered

Hilt, a streamlined dependency injection framework for Android, emerges as an innovative solution built upon the well-established Dagger library. Historically, implementing dependency injection in Android with Dagger involved intricate setup and extensive boilerplate code. Recognizing this complexity, Hilt was introduced to simplify the process dramatically. By automating the cumbersome configuration steps and harnessing Dagger's robust features, Hilt enhances app development efficiency. Its user-friendly approach, marked by straightforward annotations and components, not only maintains Dagger's powerful capabilities but also makes dependency injection more accessible and less error-prone for Android developers.

Dependency Injection

Dependency Injection involves the management and injection of dependencies into a component or object. There are different ways to achieve dependency injection, which can be broadly categorized into the following three approaches:

Creating Dependency Within

Here, the component itself is responsible for creating any dependency when needed. The dependencies are tightly coupled with the component. The component can directly instantiate and has control over the lifecycle of its dependencies.

Let's consider an example of the UserActivity class that creates an instance of UserRepository within its constructor. It tightly couples the UserActivity with the concrete implementation of the UserRepository.

This approach can make the code less flexible, harder to test, and difficult to swap dependencies.

Injecting Dependency Manually from Outside

In this approach, dependencies are created externally and injected into the component. The component expects the dependencies to be passed in during its construction or through setter methods.

Here's an example of manually injecting UserRepository into an Activity:

By this approach, we can reduce the hassle of tight coupling because UserActivity is no longer responsible for creating any dependencies on its own.

But, what if there are several dependencies or if the dependency graph is more complex than expected? There still needs to be a workaround for such scenarios.

Injecting Dependency Using a Framework

In this approach, a dependency injection framework such as Dagger or Hilt is used to automate the creation and injection of dependencies. Here's an example of injecting UserRepository using the Hilt framework:

By applying the @AndroidEntryPoint annotation to MyActivity and annotating the userRepository field with @Inject, the Hilt framework will automatically create and inject the UserRepository instance into the activity.

The use of a framework promotes loose coupling and facilitates easier testing and refactoring of code.

Moreover, the framework takes care of dependency resolution, object creation, and lifecycle management, and thus reduces manual configuration and wiring.

Understanding the Annotations

@HiltAndroidApp

The @HiltAndroidApp annotation is used in Hilt Android and is typically applied to the Application class of an Android app.

By applying @HiltAndroidApp to the Application class, a base class is generated that initializes Hilt and also sets up the dependency injection framework for the application. Moreover, this base class is also used as the custom application class in the app's manifest file.

Here, MyApplication is our custom Application class, and by applying @HiltAndroidApp to it, we are enabling Hilt and allowing it to handle dependency injection in the Android application.

@AndroidEntryPoint

This annotation is typically applied to Android components such as Activities, Fragments, Services, BroadcastReceivers, etc., to enable dependency injection.

By applying @AndroidEntryPoint to a component, Hilt generates the necessary code to handle dependency injection for that component. It allows us to inject dependencies into the annotated Android component without manually configuring the injection process.

During the app's compilation process, Hilt will generate the necessary code to create and inject the dependencies specified by the @Inject annotations. The dependencies are resolved based on their configuration in the dependency injection graph

@ViewModelInject

@ViewModelInject is used in Hilt Android to enable dependency injection in ViewModel classes. It is specifically used for injecting dependencies into ViewModels.

By applying it to the ViewModel's constructor, Hilt automatically provides the required dependencies during the creation of a ViewModel instance.

MyViewModel is a ViewModel class, and it has a constructor annotated with @ViewModelInject. The userRepository parameter in the constructor represents the dependency that needs to be injected into the ViewModel.

When Hilt creates an instance of MyViewModel, it will automatically provide an instance of UserRepository as an argument to the constructor.

@Inject

In Hilt, the @Inject annotation is used to mark the dependencies that need to be injected into a class. It is a general-purpose annotation that is used to indicate the fields, methods, or constructors that should be automatically injected with their dependencies.

We can use the field injection approach in this manner: Here, the userRepository field is annotated with @Inject. Hilt will automatically inject an instance of UserRepository into this field when creating an instance of MyActivity.

Here, the userRepository field is annotated with @Inject. Hilt will automatically inject an instance of UserRepository into this field when creating an instance of MyActivity.

@Module

In Hilt, the @Module annotation is used to define a module class. A module class provides instructions to the Hilt dependency injection framework on how to create and provide instances of dependencies.

Here's an example of using @Module in Hilt:

Here, MyModule is a module class annotated with @Module. It provides instructions on how to create instances of UserRepository dependency.

The method annotated with @Provides is responsible for creating and returning instances of the corresponding dependencies.

Here, @InstallIn(ApplicationComponent::class) indicates that the module should be installed in the ApplicationComponent of the Hilt dependency injection graph.

We'll also discuss the @InstallIn annotation separately.

@Provides

In Hilt Android, the @Provides annotation is used within a module to indicate a method that provides a specific dependency.

Let's take another look at the above example for @Module.

The method annotated with @Provides is responsible for creating and returning instances of the corresponding dependency (UserRepository).

Using @Provides in a module allows us to have more control over how dependencies are created and provided, such as when dependencies require specific initialization or configuration.

@InstallIn

The @InstallIn annotation is used in Hilt to specify the component where a module should be installed. It is used in conjunction with the @Module annotation to define the scope and visibility of the module's provided dependencies.

Here, the @InstallIn annotation is applied to the module class to specify the component in which the module should be installed.

The @InstallIn annotation accepts a parameter that represents the component where the module should be installed. Hilt provides several pre-defined components that we can choose from based on our needs, such as ApplicationComponent, ActivityComponent, FragmentComponent, etc.

In the above case, it means that the dependencies provided by MyModule will be available throughout the entire application.

@EntryPoint

The @EntryPoint annotation is used in Hilt to mark an interface that represents an entry point into a Hilt component. It allows accessing dependencies from a specific component without using the @AndroidEntryPoint annotation on an Android component.

Remember that @EntryPoint differs from @AndroidEntryPoint as @AndroidEntryPoint is an annotation used in Hilt to enable dependency injection in Android components, such as Activities, Fragments, Services, BroadcastReceivers, etc.

On the contrary, @EntryPoint is an annotation used in Hilt to mark an interface that represents an entry point into a Hilt component. It allows accessing dependencies from a specific component without using the @AndroidEntryPoint annotation on an Android component.

To access the dependencies provided by the entry point, we need to inject EntryPointAccessors and use it to get an instance of the entry point.

@DefineComponent

@DefineComponent is an annotation in Hilt Android that defines a Hilt component and is used to provide information about the component, including its name, dependencies, and its modules. It can be used on an interface or a class.

To use it on a class, the @DefineComponent annotation should be specified on the class itself and not on any of its subclasses or interfaces.

Moreover, this annotation comprises the following properties for a class:

  • name: The name of the component.
  • modules: An array of modules that the component depends on.
  • dependencies: An array of dependencies that the component needs.

For an interface, it can have the following:

  • name: The name of the component.
  • modules: An array of modules that the component depends on.

Hilt-Android

  • This module provides the core functionality of Hilt for enabling dependency injection in Android applications.
  • It includes annotations like @HiltAndroidApp and @AndroidEntryPoint that are used to enable Hilt and inject dependencies into Android components.

Hilt-ViewModel

  • This module extends the functionality of Hilt to support dependency injection in ViewModels.
  • It includes the @ViewModelInject annotation that is used to annotate the ViewModel's constructor for dependency injection.

Hilt-Work

  • This module integrates Hilt with the WorkManager library.
  • It allows us to inject dependencies into our Worker classes by using the @HiltWorker annotation.

Hilt-Navigation

  • This module provides integration between Hilt and the Navigation component of the Android Jetpack library.
  • It enables dependency injection in fragments and other components related to navigation, using the @NavGraphScoped and @FragmentNavScoped annotations.

Best Practices for Using Hilt in Our Android App

Keep Modules Simple and Focused

When creating modules in Hilt, it's recommended to keep them simple and focused on providing dependencies for a specific feature or functionality. This helps in maintaining a clear and organized dependency graph and makes it easier to understand and manage dependencies in our app.

Use Constructor Injection

Constructor injection is the recommended approach for injecting dependencies in Hilt. It ensures that dependencies are provided during object creation and promotes better testability and maintainability of our code. Avoid using field injection or method injection unless necessary.

Avoid Circular Dependencies

Circular dependencies occur when two or more classes depend on each other directly or indirectly. They can make our code more difficult to understand, test, and maintain. It's best to design our dependencies in a way that avoids circular dependencies. If circular dependencies are unavoidable, consider refactoring our code or introducing abstractions to break the cycle.

Use Scopes

Hilt supports scoping of dependencies using annotations like @Singleton, @ActivityScoped, or custom scopes. Scoping allows us to define the lifecycle and visibility of dependencies. Use scopes appropriately to manage the lifecycle of dependencies and ensure that they are created and reused when needed.

Common Pitfalls of Using Hilt in Your Android App

Overuse of Hilt

One common pitfall is overusing Hilt by applying it to every class and injecting dependencies excessively. While Hilt simplifies dependency injection, it's important to strike a balance and use it judiciously. Not every class needs to be injected, especially small utility classes or simple data models. Overusing Hilt can lead to unnecessary complexity and may hinder code readability and maintainability.

Complexity of Dependency Graphs

As the size and complexity of your app increase, so can the complexity of your dependency graph. This can make it more challenging to understand and manage the dependencies in your app. It's important to keep the dependency graph as simple and modular as possible by breaking it down into smaller, focused components and modules. Additionally, following best practices such as avoiding circular dependencies and using appropriate scopes can help mitigate this pitfall.

Performance Overhead

While Hilt provides convenience and flexibility, it does introduce a small performance overhead compared to manual dependency injection. Hilt's generated code and reflection-based approach to dependency injection can have a minor impact on the app's startup time and overall performance. However, this overhead is typically negligible and may not be noticeable in most applications. It's important to consider the trade-off between the development productivity gained from using Hilt and the slight performance impact.

Conclusion

  • Hilt simplifies the process of injecting dependencies into your app.
  • Hilt is built on top of Dagger and automates many configuration steps.
  • It provides annotations and components to enable easy dependency injection.
  • Hilt integrates seamlessly with the Android ecosystem and Jetpack libraries.
  • It supports scoping and lifecycle management of dependencies.
  • Hilt promotes modularity, testability, and best practices for Android app development.