Dagger-Hilt Module and Binds Annotations #3
In Android development, Hilt is a dependency injection (DI) framework provided by Google. Hilt is built on top of the popular DI library called Dagger, and it simplifies the process of setting up and managing dependencies in your Android application.
Hilt provides two types of annotations: @Module
and @Binds
.
Module Annotation
- The
@Module
annotation is used to mark a class as a Dagger module in Hilt. A module is responsible for providing instances of dependencies that can be injected into other classes. Here's why@Module
is used:
- Dependency Provision: The main purpose of a module is to define methods that provide instances of dependencies or define how to obtain them. By annotating a class with
@Module
, you signal to Hilt that the class contains methods that contribute to the dependency graph. - Scoping: Modules can be used to define the scope of dependencies. By associating a module with a specific component using
@InstallIn
, you can ensure that the dependencies provided by the module are scoped to the component. This allows for more fine-grained control over the lifecycle and availability of dependencies. - Organization and Modularity: Modules help in organizing and modularizing your dependency injection setup. By separating the dependency creation logic into modules, you can improve code maintainability, reusability, and testability. Each module can focus on a specific set of dependencies, making it easier to reason about and update the dependency graph.
@InstallIn Annotation: The @InstallIn
annotation is used to specify the component where a module should be installed in Hilt. Here's why @InstallIn
is used:
- Component Association: Hilt organizes dependencies into components, and
@InstallIn
is used to associate a module with a specific component. By using@InstallIn
, you define the component where the module's provided dependencies will be available for injection. It establishes a relationship between the module and the component, allowing Hilt to handle the creation and management of dependencies within the specified component. - Scoping and Lifecycle: The
@InstallIn
annotation ensures that the dependencies provided by the module are scoped correctly within the associated component's lifecycle. Hilt supports various components likeApplicationComponent
,ActivityComponent
,FragmentComponent
, etc. By installing a module in a specific component, you control the visibility and availability of the dependencies within that component's lifecycle. - Simplified Configuration:
@InstallIn
simplifies the configuration of dependency injection in Hilt. By annotating a module with@InstallIn
and specifying the component, you no longer need to manually configure the bindings and associations between modules and components. Hilt takes care of generating the necessary Dagger code based on the annotations, reducing the boilerplate code required for manual configuration.
Overall, the combination of @Module
and @InstallIn
annotations in Hilt allows you to define and organize the creation and provisioning of dependencies within your application. Modules provide a way to declare how to obtain dependencies, while @InstallIn
associates the module with a specific component, defining the scope and availability of the dependencies within that component's lifecycle. This combination simplifies the configuration, enhances code organization, and improves modularity and maintainability in your dependency injection setup.
This annotation is used to mark a class as a Dagger module. A module is responsible for providing instances of dependencies that can be injected into other classes. You can define methods inside the module class and annotate them with @Provides
to specify how to create or obtain instances of specific dependencies.
@Module
class AppModule {
@Provides
fun provideSomeDependency(): SomeDependency {
return SomeDependencyImpl()
}
// Other methods providing dependencies...
}
Binds Annotation
The @Binds
annotation in Hilt is used to declare that a method in a module binds a specific implementation to its corresponding interface or abstract class.
The primary purpose of @Binds
is to simplify the code and make it more concise when you want to provide an implementation for an interface or an abstract class. Instead of writing a method with @Provides
in the module to create an instance of the implementation, you can use @Binds
to directly specify the binding relationship between the interface and the implementation.
Here are a few reasons why you might choose to use @Binds
annotation:
- Code simplicity and readability: The
@Binds
annotation allows you to directly express the relationship between an interface and its implementation without the need for additional code. It makes the code more concise and easier to understand by eliminating the need to write a separate provider method using@Provides
in the module. - Compile-time safety: When you use
@Binds
, the Hilt compiler performs static analysis on the code and verifies that the binding is correct at compile-time. This means that if there's a problem with the binding, such as providing an incompatible implementation, it will be caught during compilation rather than at runtime. - Performance optimization: By using
@Binds
, you can inform Dagger/Hilt that the implementation being bound is the only possible implementation for the given interface. This knowledge allows Dagger/Hilt to perform optimizations such as tree shaking, where it removes unused code during the build process. This can result in smaller APK sizes and improved performance. - Support for interfaces and abstract classes: In object-oriented programming, it’s common to define interfaces or abstract classes to define contracts that can be implemented by multiple classes. The
@Binds
annotation provides a convenient way to specify the relationship between the interface or abstract class and its implementation without the need for providing explicit creation logic.
It’s important to note that @Binds
can only be used in abstract classes or interfaces inside a Dagger module, as it's responsible for binding the implementation to the abstract type. The implementation itself cannot be instantiated directly and must be provided somewhere else in the codebase.
Overall, the @Binds
annotation in Hilt simplifies the process of binding an implementation to an interface or abstract class, resulting in cleaner and more readable code, compile-time safety, and potential performance optimizations.
@Module
interface SomeModule {
@Binds
fun bindSomeDependency(someDependency: SomeDependencyImpl): SomeDependency
// Other method bindings...
}
In this example, bindSomeDependency
method binds SomeDependencyImpl
class to SomeDependency
interface, allowing it to be used throughout the application when SomeDependency
is requested.
Both @Module
and @Binds
annotations are used in conjunction with Hilt's component annotations, such as @Component
or @AndroidEntryPoint
, to enable dependency injection in your Android application.
Example-1
First, let’s define an interface and its implementation:
interface SomeDependency {
fun doSomething()
}
class SomeDependencyImpl @Inject constructor() : SomeDependency {
override fun doSomething() {
// Implementation logic here
println("Doing something...")
}
}
Next, we’ll create a module using @Module
annotation and provide an instance of SomeDependency
using @Provides
:
@Module
@InstallIn(SingletonComponent::class) // Specifies the component scope
object AppModule {
@Provides
fun provideSomeDependency(): SomeDependency {
return SomeDependencyImpl()
}
}
In this example, we use @InstallIn
annotation to specify the component scope, which is SingletonComponent
. This means that the instances provided by this module will have a singleton scope.
Now, let’s use @Binds
annotation in a separate module to bind the interface SomeDependency
to its implementation SomeDependencyImpl
:
@Module
@InstallIn(ActivityComponent::class) // Specifies the component scope
interface SomeModule {
@Binds
fun bindSomeDependency(someDependency: SomeDependencyImpl): SomeDependency
}
In this module, we use @InstallIn
annotation to specify the component scope as ActivityComponent
. This means that the instances provided by this module will have an activity scope.
Finally, in your activity or fragment, you can use Hilt’s @Inject
annotation to inject the SomeDependency
instance:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var someDependency: SomeDependency
// ...
}
With these annotations and modules set up, Hilt will automatically generate the necessary code to provide and inject the dependencies in your Android application. The SomeDependency
instance will be injected into the MainActivity
by Hilt, and you can use it as needed.
Example-2
First, let’s define an interface Logger
:
interface Logger {
fun log(message: String)
}
Next, we’ll create a concrete implementation of the Logger
interface:
class ConsoleLogger : Logger {
override fun log(message: String) {
println("Logging: $message")
}
}
Now, let’s create a module called LoggerModule
using the @Module
annotation. Inside the module, we'll define a method that provides an instance of the Logger
interface by binding it to the ConsoleLogger
implementation using the @Binds
annotation:
@Module
abstract class LoggerModule {
@Binds
abstract fun bindLogger(consoleLogger: ConsoleLogger): Logger
}
The bindLogger
method binds the ConsoleLogger
class to the Logger
interface, allowing us to use Logger
throughout the application.
Next, let’s create a class called MainActivity
that requires an instance of the Logger
interface:
class MainActivity {
@Inject
lateinit var logger: Logger
fun doSomething() {
logger.log("Hello, Ibrahim Can Erdogan!")
}
}
In the MainActivity
class, we annotate the logger
property with @Inject
to indicate that we want Hilt to inject an instance of the Logger
interface into it.
To enable Hilt and make the dependencies available, we need to create a Hilt component. Let’s create an ApplicationComponent
that uses the LoggerModule
:
@Component(modules = [LoggerModule::class])
interface ApplicationComponent {
fun inject(mainActivity: MainActivity)
}
The @Component
annotation marks the interface as a Hilt component, and we specify the modules to be used within the component using the modules
parameter.
Example-3
@Module
interface StorageModule {
@Binds
fun bindStorageManager(storageManager: StorageManagerImpl): StorageManager
}
n this example, the StorageModule
interface is annotated with @Module
, indicating that it's a Dagger module. The bindStorageManager
method is annotated with @Binds
, indicating that it binds the StorageManagerImpl
class to the StorageManager
interface. This allows instances of StorageManager
to be injected throughout the application and resolved to StorageManagerImpl
.
Note that in the second example, we use an interface instead of a class for the module. It’s valid to define modules as interfaces in Hilt.
These examples demonstrate how to use the @Module
and @Binds
annotations to define Dagger modules and provide or bind instances of dependencies. Remember that you'll also need to set up the Hilt components, such as @Component
or @AndroidEntryPoint
, in your Android application to enable dependency injection using Hilt.
I hope it was useful. You can follow me on my social media accounts.