An annotating processing library that automatically generates Dagger Hilt's @Binds
methods.
The main motivation behind this library is to eliminate boilerplate code when binding types in the Dagger Hilt library. Occasionally we have a type that should be exposed only by its supertype (a superclass or an implemented interface). In order to bind the type to its supertype in the Dagger Hilt, we have to write something like this:
@Module
@InstallIn(SingletonComponent::class)
interface BindingsModule {
@Binds
fun bindType(type: Type): Supertype
}
Can't we just automate this process and instruct the machine to generate a binding for us? This library is the answer to the given question.
First of all, make sure that you've added the mavenCentral()
repository to your top-level build.gradle
file.
buildscript {
//...
repositories {
//...
mavenCentral()
}
//...
}
If you are using pure Java (no Kotlin code), add the following to your module-level build.gradle
file.
dependencies {
implementation("com.paulrybitskyi:hilt-binder:1.1.3")
annotationProcessor("com.paulrybitskyi:hilt-binder-compiler:1.1.3")
}
If you are using Kotlin with Java, then apply the kapt plugin and declare the compiler dependency using kapt
instead of annotationProcessor
.
plugins {
kotlin("kapt")
}
dependencies {
implementation("com.paulrybitskyi:hilt-binder:1.1.3")
kapt("com.paulrybitskyi:hilt-binder-compiler:1.1.3")
}
A KSP implementation of the library. KSP is a replacement for KAPT to run annotation processors natively on the Kotlin compiler, significantly reducing build times.
To use the KSP implementation, go to your project's settings.gradle.kts
file and add google()
to repositories
for the KSP plugin.
pluginManagement {
repositories {
gradlePluginPortal()
google()
}
}
Then, in the module's build.gradle.kts
file, apply the KSP Gradle plguin and replace the kapt
configuration in your build file with ksp
.
plugins {
id("com.google.devtools.ksp") version "<latestKspVersion>"
}
dependencies {
implementation("com.paulrybitskyi:hilt-binder:1.1.3")
ksp("com.paulrybitskyi:hilt-binder-compiler:1.1.3")
}
See the KSP documentation for more details.
The main annotation of the library is @BindType. The annotation has 4 optional parameters, which are going to be explained in the following examples.
Let's say we want to bind a class to its superclass/interface:
abstract class AbstractImageLoader
interface ImageLoader
@BindType
class PicassoImageLoader @Inject constructor(): AbstractImageLoader()
@BindType
class GlideImageLoader @Inject constructor(): ImageLoader
Which generates this:
@Module
@InstallIn(SingletonComponent.class)
public interface HiltBinder_SingletonComponentModule {
@Binds
AbstractImageLoader bind_PicassoImageLoader(PicassoImageLoader binding);
@Binds
ImageLoader bind_GlideImageLoader(GlideImageLoader binding);
}
What happens if we have a class that has a superclass and also implements some interfaces? In that case, we'll have to manually specify the type we would like to bind to using to
parameter of the annotation. For example, to bind to an interface:
abstract class AbstractImageLoader
interface ImageLoader
@BindType(to = ImageLoader::class)
class PicassoImageLoader @Inject constructor(): AbstractImageLoader(), ImageLoader
Which generates this:
@Module
@InstallIn(SingletonComponent.class)
public interface HiltBinder_SingletonComponentModule {
@Binds
ImageLoader bind_PicassoImageLoader(PicassoImageLoader binding);
}
The default behavior simply tries to bind to a direct superclass or interface of the annotated type. If the processor cannot deduce the type on its own (e.g., class implements multiple interfaces, has a superclass and implements an interface), then it is going to throw an error to notify you to specify the type to bind to explicitly.
It's worth mentioning that if you need to bind to a specific type in your class hierarchy (e.g., superclass of a superclass, extended interface, etc.), then you have no other option than specifying a value for the to
parameter.
Dagger Hilt comes with predefined components for Android, but also supports creating custom ones. First, let's see how we can install a binding into predefined components.
You've probably noticed that in the previous examples all the generated files have the @InstallIn(SingletonComponent.class)
annotation. This means that by default all bindings are installed into the Hilt's predefined SingletonComponent
. There are two ways to change a predefined component: either use the installIn
parameter of the annotation or specify a scope annotation of a predefined component. For example, have a look at the following code:
interface ImageLoader
interface Logger
@BindType(installIn = BindType.Component.FRAGMENT)
class PicassoImageLoader @Inject constructor(): ImageLoader
@FragmentScoped
@BindType
class AndroidLogger @Inject constructor(): Logger
Which generates this:
@Module
@InstallIn(FragmentComponent.class)
public interface HiltBinder_FragmentComponentModule {
@Binds
ImageLoader bind_PicassoImageLoader(PicassoImageLoader binding);
@Binds
Logger bind_AndroidLogger(AndroidLogger binding);
}
Obviously, the AndroidLogger
instance will also be scoped to the FragmentComponent
, unlike the PicassoImageLoader
instance. With the AndroidLogger
example, the library simply leverages the fact that every scope is associated with its corresponding component, therefore, there is no need to specify it again using the installIn
parameter, though you can.
To install a binding into a custom component, assign BindType.Component.CUSTOM
as the value of the installIn
parameter and specify a class of the custom component itself through the customComponent
parameter. It should be mentioned that, unlike with predefined components, simply specifying a scope of the custom component won't work, since it's impossible to infer a class of the custom component from its scope annotation. For example, take a look at the following code:
// A custom component's scope annotation
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class CustomScope
// Declaration of a custom component itself
@CustomScope
@DefineComponent(parent = SingletonComponent::class)
interface CustomComponent
interface ImageLoader
interface Logger
// Binding unscoped type
@BindType(
installIn = BindType.Component.CUSTOM,
customComponent = CustomComponent::class
)
class PicassoImageLoader @Inject constructor(): ImageLoader
// Binding scoped type
@CustomScope
@BindType(
installIn = BindType.Component.CUSTOM,
customComponent = CustomComponent::class
)
class AndroidLogger @Inject constructor(): Logger
// Won't work, can't infer CustomComponent from CustomScope
// @CustomScope
// @BindType
// class AndroidLogger @Inject constructor(): Logger
Which generates the following:
@Module
@InstallIn(CustomComponent.class)
public interface HiltBinder_CustomComponentModule {
@Binds
ImageLoader bind_PicassoImageLoader(PicassoImageLoader binding);
@Binds
Logger bind_AndroidLogger(AndroidLogger binding);
}
The library also supports Dagger Multibindings. For example, to contribute elements to a multibound set:
interface UrlOpener
@BindType(contributesTo = BindType.Collection.SET)
class BrowserUrlOpener @Inject constructor(): UrlOpener
@BindType(contributesTo = BindType.Collection.SET)
class CustomTabUrlOpener @Inject constructor(): UrlOpener
@BindType(contributesTo = BindType.Collection.SET)
class NativeAppUrlOpener @Inject constructor(): UrlOpener
Which generates this:
@Module
@InstallIn(SingletonComponent.class)
public interface HiltBinder_SingletonComponentModule {
@Binds
@IntoSet
UrlOpener bind_BrowserUrlOpener(BrowserUrlOpener binding);
@Binds
@IntoSet
UrlOpener bind_CustomTabUrlOpener(CustomTabUrlOpener binding);
@Binds
@IntoSet
UrlOpener bind_NativeAppUrlOpener(NativeAppUrlOpener binding);
}
To contribute to a multibound map, we also need to provide the @Mapkey annotation. Dagger has some default ones, like @ClassKey and @StringKey, but, unfortunately, they cannot be used in this case, because they are only applicable to methods (meaning their @Target
annotation is equal to ElementType.METHOD
). Therefore, the library provides its own default ones (@MapIntKey
, @MapLongKey
, @MapStringKey
, and @MapClassKey
). For example:
interface SettingHandler
@BindType(contributesTo = BindType.Collection.MAP)
@MapStringKey("change_username")
class ChangeUsernameSettingHandler @Inject constructor(): SettingHandler
@BindType(contributesTo = BindType.Collection.MAP)
@MapStringKey("buy_subscription")
class BuySubscriptionSettingHandler @Inject constructor(): SettingHandler
@BindType(contributesTo = BindType.Collection.MAP)
@MapStringKey("log_out")
class LogOutSettingHandler @Inject constructor(): SettingHandler
Which generates this:
@Module
@InstallIn(SingletonComponent.class)
public interface HiltBinder_SingletonComponentModule {
@Binds
@IntoMap
@MapStringKey("change_username")
SettingHandler bind_ChangeUsernameSettingHandler(ChangeUsernameSettingHandler binding);
@Binds
@IntoMap
@MapStringKey("buy_subscription")
SettingHandler bind_BuySubscriptionSettingHandler(BuySubscriptionSettingHandler binding);
@Binds
@IntoMap
@MapStringKey("log_out")
SettingHandler bind_LogOutSettingHandler(LogOutSettingHandler binding);
}
Custom keys are also supported. For example:
interface SettingHandler
enum class SettingType {
CHANGE_USERNAME,
BUY_SUBSCRIPTION,
LOG_OUT
}
@MapKey
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
annotation class SettingMapKey(val type: SettingType)
@BindType(contributesTo = BindType.Collection.MAP)
@SettingMapKey(SettingType.CHANGE_USERNAME)
class ChangeUsernameSettingHandler @Inject constructor(): SettingHandler
@BindType(contributesTo = BindType.Collection.MAP)
@SettingMapKey(SettingType.BUY_SUBSCRIPTION)
class BuySubscriptionSettingHandler @Inject constructor(): SettingHandler
@BindType(contributesTo = BindType.Collection.MAP)
@SettingMapKey(SettingType.LOG_OUT)
class LogOutSettingHandler @Inject constructor(): SettingHandler
Which generates this:
@Module
@InstallIn(SingletonComponent.class)
public interface HiltBinder_SingletonComponentModule {
@Binds
@IntoMap
@SettingMapKey(SettingType.CHANGE_USERNAME)
SettingHandler bind_ChangeUsernameSettingHandler(ChangeUsernameSettingHandler binding);
@Binds
@IntoMap
@SettingMapKey(SettingType.BUY_SUBSCRIPTION)
SettingHandler bind_BuySubscriptionSettingHandler(BuySubscriptionSettingHandler binding);
@Binds
@IntoMap
@SettingMapKey(SettingType.LOG_OUT)
SettingHandler bind_LogOutSettingHandler(LogOutSettingHandler binding);
}
Qualifiers are supported by the library as well. For the qualifier to be associated with the return type, you need to provide true
to the withQualifier
parameter of the annotation. The default value is false
. Let's have a look at the following example:
interface BooksDataStore
@BindType(withQualifier = true)
@Named("books_local")
class BooksLocalDataStore @Inject constructor(): BooksDataStore
@BindType(withQualifier = true)
@Named("books_remote")
class BooksRemoteDataStore @Inject constructor(): BooksDataStore
Which generates this:
@Module
@InstallIn(SingletonComponent.class)
public interface HiltBinder_SingletonComponentModule {
@Binds
@Named("books_local")
BooksDataStore bind_BooksLocalDataStore(BooksLocalDataStore binding);
@Binds
@Named("books_remote")
BooksDataStore bind_BooksRemoteDataStore(BooksRemoteDataStore binding);
}
Binding generic types is identical to binding regular types. For example:
interface StreamingService<T>
class Netflix
class Hulu
@BindType
class NetflixService @Inject constructor(): StreamingService<Netflix>
@BindType(to = StreamingService::class)
class HuluService @Inject constructor(): StreamingService<Hulu>
Generates the following:
@Module
@InstallIn(SingletonComponent.class)
public interface HiltBinder_SingletonComponentModule {
@Binds
StreamingService<Netflix> bind_NetflixService(NetflixService binding);
@Binds
StreamingService<Hulu> bind_HuluService(HuluService binding);
}
However, if a binding is contributed to either collection (SET
or MAP
), then any type parameters of a generic return type will be replaced with wildcards. For example:
interface StreamingService<T>
class Netflix
class Hulu
@BindType(contributesTo = BindType.Collection.SET)
class NetflixService @Inject constructor(): StreamingService<Netflix>
@BindType(
to = StreamingService::class,
contributesTo = BindType.Collection.SET
)
class HuluService @Inject constructor(): StreamingService<Hulu>
Generates the following:
@Module
@InstallIn(SingletonComponent.class)
public interface HiltBinder_SingletonComponentModule {
@Binds
@IntoSet
StreamingService<?> bind_NetflixService(NetflixService binding);
@Binds
@IntoSet
StreamingService<?> bind_HuluService(HuluService binding);
}
The project contains a sample application that illustrates the aforementioned examples as well as some advanced ones.
Apart from the sample, you can also take a look at the following repositories that heavily utilize this library:
- Gamedge - An Android application for browsing video games and checking the latest gaming news from around the world.
- DocSkanner - An Android application that makes it possible to automatically scan and digitize documents from photos.
HiltBinder is licensed under the Apache 2.0 License.