Dagger-Hilt Qualifier & Retention Annotations #5
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:
- 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.
- 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.
- 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. - 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:
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.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.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.