Dagger-Hilt Qualifier & Retention Annotations #5

ibrahimcanerdogan
6 min readMay 21, 2023

--

Photo by Tony Hand on Unsplash

Helllooo in this article we will learn why and how to use Retention and Qualifiers.

Why dou you need Qualifiers?

Qualifiers are used in dependency injection frameworks like Hilt to disambiguate multiple implementations of the same type. They provide a way to differentiate between different instances of a particular class or interface when they are being injected into other classes.

Here are a few reasons why qualifiers are useful in dependency injection:

  1. Multiple Implementations: When you have multiple implementations of an interface or multiple classes of the same type, qualifiers allow you to specify which implementation you want to inject. Without qualifiers, the dependency injection framework may not know which implementation to choose, leading to ambiguity.
  2. Customization and Configuration: Qualifiers can be used to customize or configure the behavior of injected objects. For example, you might have different implementations of a service class with varying configurations. By using qualifiers, you can inject the appropriate implementation based on specific criteria or requirements.
  3. Scoped Instances: In some cases, you may want to have different instances of a class within different scopes or contexts. Qualifiers can be used to specify different instances based on specific scopes. For example, you might have a UserManager class that has a different instance for each user. By using qualifiers, you can inject the correct instance based on the current user.
  4. Third-Party Libraries: Qualifiers can also be used when integrating with third-party libraries that provide their own implementations. If you need to inject different instances of a type from a third-party library, you can use qualifiers to specify which implementation you want to use.

By using qualifiers, you can provide a clear and unambiguous way to identify and select the specific instances or implementations you want to use during dependency injection. It adds flexibility and control over the injection process, ensuring that the correct dependencies are provided to the requesting classes.

What is the Retention Annotations?

In Kotlin Android development with Hilt, the retention types for annotations have the same purpose as in Java. They determine how long the annotations are retained or available, specifying whether they are available during the compilation process or at runtime. The retention types used in Hilt Kotlin Android development are:

  1. SOURCE: Annotations with this retention type are discarded by the compiler and not retained in the compiled bytecode. They are only available during the compilation process and are not accessible at runtime. Hilt does not use this retention type.
  2. BINARY: Annotations with this retention type are retained in the compiled bytecode but not accessible at runtime. They are available for reflection during compile-time processing. Hilt does not use this retention type either.
  3. RUNTIME: Annotations with this retention type are retained in the compiled bytecode and available at runtime. They can be accessed and processed through reflection during runtime. Hilt uses this retention type for its annotations to perform dependency injection at runtime.

Hilt Kotlin annotations, such as @InstallIn, @EntryPoint, @Module, @Provides, @Inject, @Singleton, and others, are defined with the @Retention annotation, specifying the AnnotationRetention.RUNTIME retention type. This ensures that Hilt annotations are available at runtime for the Hilt framework to perform dependency injection.

By using RUNTIME, Hilt annotations remain accessible during runtime, allowing Hilt's annotation processors and runtime library to identify and generate the necessary code for dependency injection. Hilt can analyze the annotated classes, resolve dependencies, and generate the appropriate Dagger code to provide and inject dependencies as required.

To summarize, the RUNTIME retention type used in Hilt Kotlin Android development ensures that the annotations are retained in the compiled bytecode and available at runtime. This enables Hilt to perform runtime dependency injection by scanning and processing the annotated classes and methods, providing the necessary dependencies at runtime.

Qualifier Examples

To provide multiple bindings with qualifiers using Hilt, follow these steps:

Define your qualifier annotation: Create a custom annotation that acts as a qualifier. For example, you can define an annotation called @CustomQualifier.

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CustomQualifier

Annotate your implementations: Apply the qualifier annotation to the implementations you want to differentiate. For example, if you have two implementations of an interface MyInterface, annotate them with @CustomQualifier.

@CustomQualifier
class MyInterfaceImplementationOne : MyInterface {
// Implementation code
}

@CustomQualifier
class MyInterfaceImplementationTwo : MyInterface {
// Implementation code
}

Provide multiple bindings: In your module, use the @Binds annotation along with the qualifier annotation to provide the appropriate bindings.

@Module
@InstallIn(ApplicationComponent::class)
interface MyModule {
@Binds
@CustomQualifier
fun bindFirstImplementation(implementation: MyInterfaceImplementationOne): MyInterface

@Binds
@CustomQualifier
fun bindSecondImplementation(implementation: MyInterfaceImplementationTwo): MyInterface
}

Now, when you inject MyInterface using Hilt, you can specify the specific implementation you want by applying the corresponding qualifier.

@Inject
@CustomQualifier
lateinit var myInterface: MyInterface

Hilt will then provide the correct implementation based on the qualifier specified.

Note that the above example assumes you have already set up Hilt in your Android project, including the necessary dependencies and annotations.

Example-1

First, define the AudioPlayer interface:

interface AudioPlayer {
fun play()
fun pause()
// Other methods
}

Next, create different implementations of the AudioPlayer interface:

class ExoPlayerImpl : AudioPlayer {
override fun play() {
// Implementation for ExoPlayer
}

override fun pause() {
// Implementation for ExoPlayer
}
}

class MediaPlayerImpl : AudioPlayer {
override fun play() {
// Implementation for MediaPlayer
}

override fun pause() {
// Implementation for MediaPlayer
}
}

To differentiate between these implementations using qualifiers, create custom qualifier annotations:

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ExoPlayerQualifier

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class MediaPlayerQualifier

Now, provide the implementations using the qualifiers in a module:

@Module
@InstallIn(ApplicationComponent::class)
class AudioPlayerModule {

@ExoPlayerQualifier
@Provides
fun provideExoPlayer(): AudioPlayer {
return ExoPlayerImpl()
}

@MediaPlayerQualifier
@Provides
fun provideMediaPlayer(): AudioPlayer {
return MediaPlayerImpl()
}
}

In your activity or any class where you need to inject the AudioPlayer, use the corresponding qualifier:

class MusicPlayerActivity : AppCompatActivity() {

@Inject
@ExoPlayerQualifier
lateinit var audioPlayer: AudioPlayer

// ...
}

y using the @ExoPlayerQualifier qualifier, Hilt will inject the ExoPlayerImpl instance of AudioPlayer into the audioPlayer variable.

Similarly, you can inject the MediaPlayerImpl instance by using the @MediaPlayerQualifier qualifier.

Using qualifiers allows you to differentiate between multiple implementations of the same type (AudioPlayer in this case) and inject the appropriate implementation based on the qualifier used.

Example-2

Define the data source interface:

interface DataSource {
fun fetchData(): String
}

Implement the two data sources:

class LocalDataSource @Inject constructor() : DataSource {
override fun fetchData(): String {
return "Data fetched from local database"
}
}

class RemoteDataSource @Inject constructor() : DataSource {
override fun fetchData(): String {
return "Data fetched from remote API"
}
}

Create qualifiers:

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Local

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Remote

Provide bindings in the module using qualifiers:

@Module
@InstallIn(ApplicationComponent::class)
object DataModule {
@Local
@Provides
fun provideLocalDataSource(): DataSource {
return LocalDataSource()
}

@Remote
@Provides
fun provideRemoteDataSource(): DataSource {
return RemoteDataSource()
}
}

Use the qualified injection in the consuming class:

class DataRepository @Inject constructor(
@Local private val localDataSource: DataSource,
@Remote private val remoteDataSource: DataSource
) {
fun getDataFromLocal(): String {
return localDataSource.fetchData()
}

fun getDataFromRemote(): String {
return remoteDataSource.fetchData()
}
}

In this example, we have defined LocalDataSource and RemoteDataSource as two different implementations of the DataSource interface. We then created qualifiers @Local and @Remote using the @Qualifier annotation.

Inside the DataModule, we provided bindings for DataSource using the respective qualifiers, distinguishing between the local and remote data sources.

Finally, in the DataRepository class, we injected the DataSource using the qualifiers @Local and @Remote. This allows the DataRepository to be aware of which implementation of DataSource should be used for each specific use case.

By using qualifiers, we can easily switch between different implementations of the DataSource based on the requirement. This approach enables flexibility and abstraction when dealing with multiple data sources in the application.

I hope it was useful. You can follow me on my social media accounts.

LINKEDIN

UDEMY

GITHUB

--

--

ibrahimcanerdogan
ibrahimcanerdogan

Written by ibrahimcanerdogan

Hi, My name is Ibrahim, I am developing ebebek android app within Ebebek. I publish various articles in the field of programming and self-improvement.

No responses yet