Advanced use of Hilt: @Qualifier and @Bind annotations

  • Key differences and use cases between @Binds and @Provides annotations for dependency management.
  • Implementing @Qualifier to resolve identical type conflicts in the Hilt dependency graph.
  • Advanced lifecycle management through the use of scopes such as @Singleton and @ViewModelScoped.
  • Strategies for integrating dependency injection into unsupported classes using @EntryPoint.

Advanced use of Hilt: @Qualifier and @Bind annotations

If you've ever felt like your Android app's code is becoming a complete mess, with instances being passed from one class to another like a hot potato, chances are you've fallen into the trap of... hell of dependenciesTo prevent your project from becoming unmanageable, Google has introduced Dagger Hilt, a tool that handles all the heavy lifting, automating the delivery of the objects your classes need without you having to write the same code over and over again until you're fed up.

Unlike the conventional Dagger, which can sometimes be intimidating due to its steep learning curve, Hilt offers a much more standardized and straightforward approach. It works wonderfully with Jetpack Compose and modern architectures, optimizing runtime performance and ensuring you don't encounter unpleasant surprises with missing dependency errors, as everything is validated during compilation.

Initial configuration and system startup

To start using Hilt, the first step is to set up the environment in your Gradle files. It's crucial to add the hilt-android-gradle-plugin to the root file and then apply the dependencies to your application module. A key point here is that the project must be configured to Java 17Otherwise, the current versions of Hilt and Compose could give you quite a few headaches.

For the magic to start working, any app that wants to use this system must have an Application class marked with the annotation @HiltAndroidAppThis label is the switch that activates code generation and creates the base where the entire object graph will reside; without it, Hilt would have nowhere to anchor itself and nothing you do would work.

Injecting dependencies into Android components

Once the base is set up, we need to tell Hilt which classes we want to receive the objects in. To do this we use @AndroidEntryPointThis is compatible with most parts of the framework, such as Activities, Services, and BroadcastReceivers. If you're using Compose, simply annotate the root Activity, and all composable elements will be able to access the injected ViewModels without any issues.

To insert an object directly into the code of an activity, we use field injection with @InjectYou just need to define the variable as `lateinit var` and add the annotation. However, be very careful: injected fields they cannot be privatebecause Hilt needs access to them to allocate the instance; if you try to make them private, the compiler will give you an immediate error.

mvvm
Related article:
MVVM: The Ultimate Software Architecture Pattern for Modern Apps

The art of linking: @Binds versus @Provides

Hilt needs to know how to manufacture everything we ask it to. The simplest way is constructor injection, where we annotate the class constructor with `@Inject`. But there are cases where this is impossible, such as when working with external interfaces or libraries Retrofit or Room type, where we cannot touch the source code to add annotations.

This is where modules come in, which are classes annotated with @Module and @InstallIn. If we want to link an interface to a concrete implementation, the most efficient way is to use @BindsAn abstract function is defined where the parameter is the implementation and the return is the interface; it is basically telling Hilt: "when someone wants an AnalyticsService, give them an AnalyticsServiceImpl".

On the other hand, when the creation of the object requires more complex logic or comes from a third party, we use @ProvidesHere we write a normal function where the body details exactly how the instance is constructed, as when configuring the base URL of a Retrofit client. In short, while @Binds is used for delegation, @Provides is used for manually create the dependency.

Conflict management using @Qualifier

Sometimes we need two different versions of the same object type. Imagine you have two OkHttpClient clients: one for calls requiring authentication and another for public requests. If we try to inject both without further configuration, Hilt will panic because it won't know which one to choose. To solve this problem, we use... custom qualifiers.

A qualifier is basically a self-annotation marked with @QualifierWe create tags like @AuthInterceptorOkHttpClient and place them both in the module's method that provides the instance and where we inject it. This way, Hilt can differentiate between bindings of the same type without errors. Additionally, there are very useful pre-built qualifiers such as @ApplicationContext and @ActivityContextwhich saves us from creating our own modules to obtain the Android context.

Asynchronous and Reactive Data Flows with Kotlin Flow
Related article:
Complete Guide to Essential Kotlin Concepts for Android Development

Synchronization of life cycles and scoping

By default, Hilt creates a new instance each time a dependency is requested. However, there are situations where we want the object to remain the same throughout the component's lifespan. For this, we have... scopesIf we mark a class with @Singleton and install it in SingletonComponent, we will have a single instance shared throughout the application.

There are also more specific levels. For example, @ActivityScoped `@ViewModelScoped` ensures that the same instance is used throughout the lifetime of an activity, while `@ViewModelScoped` does the same for the ViewModel, preventing the dependency from being recreated when the screen is rotated. However, it's not advisable to overuse scopes, as keeping objects in memory for too long can lead to problems. impair performance and Android memory management.

Special cases: @EntryPoint and unsupported classes

Hilt is very powerful, but it doesn't reach everywhere. There are system classes, such as ContentProvider, that don't support @AndroidEntryPoint. In these scenarios, the solution is to create an entry point using @EntryPointThis defines an interface that serves as a bridge between code managed by Hilt and external code.

To recover the facilities from this bridge, we used the EntryPointAccessorsWe pass it the appropriate context and the EntryPoint interface, which allows us to manually extract dependencies from the Hilt graph even in those corners where automatic injection cannot reach.

Differences between Hilt and Dagger

Hilt is not a replacement for Dagger, but rather built upon it. Its main advantage is that it eliminates the need to manually write components and manage their lifecycles, since It automatically generates the containers linked to Android classes. This drastically reduces repetitive code and makes the project much more readable.

This system allows inversion of control to be a natural process: the classes tell you what they need, and Hilt takes care of providing it. This not only simplifies day-to-day development but is also a blessing for the unit testsbecause we can replace the actual implementations with test doubles without having to touch the business logic.

The implementation of Hilt in Android enables robust and efficient dependency management, eliminating redundant code and facilitating the creation of scalable applications. Thanks to the precise use of @Binds, @Provides and @QualifierDevelopers can resolve type conflicts and manage the object lifecycle in a granular way, ensuring that the application is maintainable and easy to test through inversion of control.


Add as preferred source