Skip to content

An annotation processor library that automatically creates Hilt's `@Binds` functions and modules.

License

Notifications You must be signed in to change notification settings

Park-SM/HiltBinder

Repository files navigation

Generic badge  Generic badge  Generic badge  Generic badge 

HiltBinder

An annotation processor library that automatically creates Hilt's @Binds functions and modules.
If you think this library is useful, please press ⭐️ Star button at upside : )



# How to use

The mavenCentral repository must be registered. Add dependencies as shown in the code below.

// build.gradle(:project)
repositories {
    ....
    mavenCentral()
}

// build.gradle(:app)
dependencies {

    def hiltBinderVersion = "1.6.0"
    implementation "com.smparkworld.hiltbinder:hiltbinder:$hiltBinderVersion"
    kapt "com.smparkworld.hiltbinder:hiltbinder-processor:$hiltBinderVersion"
}



# Basic usage

No longer need abstract module classes. Just add @HiltBinds and the Binds module will be created automatically.

interface TestRepository {
    fun printTestString()
}

@HiltBinds
class TestRepositoryImpl @Inject constructor(
    private val testString: String
) : TestRepository {

    override fun printTestString() {
        Log.d("Test!!", "TestString is $testString in UseCase.")
    }
}

And the processor automatically generates the following Java files:

@Module
@InstallIn(SingletonComponent.class)
abstract class TestUseCaseImpl_BindsModule {
    @Binds
    public abstract TestUseCase bindTestUseCaseImpl(TestUseCaseImpl target);
}



# Options

to

The return type of the Binds abstract function.

open class BaseSampleModel {
    ....
}

interface ToSampleModel {
    fun printTestString()
}

@HiltBinds(to = ToSampleModel::class)
class ToSampleModelImpl @Inject constructor(
    private val testString: String
) : BaseSampleModel(), ToSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in ToSampleModelImpl class.")
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class ToSampleModelImpl_BindsModule {
  @Binds
  public abstract ToSampleModel bindToSampleModelImpl(ToSampleModelImpl target);
}

from

The argument type of the Binds abstract function. However, from an architectural point of view, this is not recommended.

@HiltBinds(from = FromSampleModelImpl::class)
interface FromSampleModel {
    fun printTestString()
}

class FromSampleModelImpl @Inject constructor(
    private val testString: String
) : FromSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in FromSampleModelImpl class.")
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class FromSampleModel_BindsModule {
  @Binds
  public abstract FromSampleModel bindFromSampleModel(FromSampleModelImpl target);
}

component

Specifies in which component the class to be returned will be installed.

interface ComponentSampleModel {
    fun printTestString()
}

@HiltBinds(component = ActivityRetainedComponent::class)
class ComponentSampleModelImpl @Inject constructor(
    private val testString: String
) : ComponentSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in ComponentSampleModelImpl class.")
    }
}
// generated code
@Module
@InstallIn(ActivityRetainedComponent.class)
abstract class ComponentSampleModelImpl_BindsModule {
  @Binds
  public abstract ComponentSampleModel bindComponentSampleModelImpl(ComponentSampleModelImpl target);
}

scope

To specify ranges separately, apply scope annotations as in the following code snippet.

interface ScopeSampleModel {
    fun printTestString()
}

// for ActivityRetainedComponent
@HiltBinds(component = ActivityRetainedComponent::class)
@ActivityRetainedScoped
class ScopeSampleModelImpl @Inject constructor(
    private val testString: String
) : ScopeSampleModel {

    override fun printTestString() {
      Log.d("Test!!", "TestString is `$testString` in ScopeSampleModelImpl class.");
    }
}


// for Singleton
@HiltBinds
@Singleton
class SomethingSampleModelImpl @Inject constructor(
    private val testString: String
) : SomethingSampleModel {
    ....
}
// generated code
// for ActivityRetainedComponent
@Module
@InstallIn(ActivityRetainedComponent.class)
abstract class ScopeSampleModelImpl_BindsModule {
  @Binds
  @ActivityRetainedScoped
  public abstract ScopeSampleModel bindScopeSampleModelImpl(ScopeSampleModelImpl target);
}

// for SingletonComponent
@Module
@InstallIn(SingletonComponent.class)
abstract class ScopeSampleModelImpl_BindsModule {
  @Binds
  @Singleton
  public abstract ScopeSampleModel bindScopeSampleModelImpl(ScopeSampleModelImpl target);
}

qualifier

The Qualifier annotation to be applied to the return type.

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

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

interface QualifierSampleModel {
    fun printTestString()
}

@HiltBinds
@SampleQualifierA
class QualifierSampleModelImpl1 @Inject constructor(
    private val testString: String
) : QualifierSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in QualifierSampleModelImpl1 class.")
    }
}

@HiltBinds
@SampleQualifierB
class QualifierSampleModelImpl2 @Inject constructor(
    private val testString: String
) : QualifierSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in QualifierSampleModelImpl2 class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    @SampleQualifierA
    lateinit var qualifierSampleModelA: QualifierSampleModel
  
    @Inject
    @SampleQualifierB
    lateinit var qualifierSampleModelB: QualifierSampleModel
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        qualifierSampleModelA.printTestString()
        qualifierSampleModelB.printTestString()
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class QualifierSampleModelImpl1_BindsModule {
  @Binds
  @SampleQualifierA
  public abstract QualifierSampleModel bindQualifierSampleModelImpl1(QualifierSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class QualifierSampleModelImpl2_BindsModule {
  @Binds
  @SampleQualifierB
  public abstract QualifierSampleModel bindQualifierSampleModelImpl2(QualifierSampleModelImpl2 target);
}

named

The Qualifier annotation to be applied to the return type.

interface NamedSampleModel {
    fun printTestString()
}

@HiltBinds
@Named("modelA")
class NamedSampleModelImpl1 @Inject constructor(
   private val testString: String
) : NamedSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in NamedSampleModelImpl1 class.")
    }
}

@HiltBinds
@Named("modelB")
class NamedSampleModelImpl2 @Inject constructor(
   private val testString: String
) : NamedSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in NamedSampleModelImpl2 class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    @Named("modelA")
    lateinit var namedSampleModelA: NamedSampleModel

    @Inject
    @Named("modelB")
    lateinit var namedSampleModelB: NamedSampleModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        namedSampleModelA.printTestString()
        namedSampleModelB.printTestString()
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class NamedSampleModelImpl1_BindsModule {
  @Binds
  @Named("modelA")
  public abstract NamedSampleModel bindNamedSampleModelImpl1(NamedSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class NamedSampleModelImpl2_BindsModule {
  @Binds
  @Named("modelB")
  public abstract NamedSampleModel bindNamedSampleModelImpl2(NamedSampleModelImpl2 target);
}



CAUTION HERE

parameter to and from must not be signed together. Either to or from must be used. If they are signed at the same time, throws an exception. Because dependency injection can be attempted from other unrelated classes as in the code below.

@HiltBinds(
    to = SampleModel::class,
    from = SampleModelImpl::class
)
interface SomethingClass    // throws an exception.



# MultiBinding

Set Multibinding - basics

You must use @HiltSetBinds to apply Set Multibinding.

interface SetSampleModel {
    fun printTestString()
}

@HiltSetBinds
class SetSampleModelImpl1 @Inject constructor(
    private val testString: String
) : SetSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in SetSampleModelImpl1 class.")
    }
}

@HiltSetBinds
class SetSampleModelImpl2 @Inject constructor(
    private val testString: String
) : SetSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in SetSampleModelImpl2 class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var sampleSet: @JvmSuppressWildcards Set<SetSampleModel>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        sampleSet.forEach {
            it.printTestString()
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class SetSampleModelImpl1_BindsModule {
  @Binds
  @IntoSet
  public abstract SetSampleModel bindSetSampleModelImpl1(SetSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class SetSampleModelImpl2_BindsModule {
  @Binds
  @IntoSet
  public abstract SetSampleModel bindSetSampleModelImpl2(SetSampleModelImpl2 target);
}

Set Multibinding - qualifier

If you want to configure multiple Set Multibinding of the same type, you can use @Qualifier(javax.inject.Qualifier) annotations like this:

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

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

interface QualifiedSetSampleModel {
    fun printTestString()
}

@HiltSetBinds
@SampleSetQualifierA
class QualifiedSetSampleModelImpl1 @Inject constructor(
    private val testString: String
) : QualifiedSetSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in QualifiedSetSampleModelImpl1 class.")
    }
}

@HiltSetBinds
@SampleSetQualifierB
class QualifiedSetSampleModelImpl2 @Inject constructor(
    private val testString: String
) : QualifiedSetSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in QualifiedSetSampleModelImpl2 class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    @SampleSetQualifierA
    lateinit var sampleQualifiedSetA: @JvmSuppressWildcards Set<QualifiedSetSampleModel>

    @Inject
    @SampleSetQualifierB
    lateinit var sampleQualifiedSetB: @JvmSuppressWildcards Set<QualifiedSetSampleModel>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        sampleQualifiedSetA.forEach { it.printTestString() }
        sampleQualifiedSetB.forEach { it.printTestString() }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class QualifiedSetSampleModelImpl1_BindsModule {
  @Binds
  @IntoSet
  @SampleSetQualifierA
  public abstract QualifiedSetSampleModel bindQualifiedSetSampleModelImpl1(QualifiedSetSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class QualifiedSetSampleModelImpl2_BindsModule {
  @Binds
  @IntoSet
  @SampleSetQualifierB
  public abstract QualifiedSetSampleModel bindQualifiedSetSampleModelImpl2(QualifiedSetSampleModelImpl2 target);
}

Set Multibinding - named

If you want to configure multiple Set Multibinding of the same type, you can use @Named(javax.inject.Named) annotations like this:

interface NamedSetSampleModel {
    fun printTestString()
}

@HiltSetBinds
@Named("sampleNamedSetA")
class NamedSetSampleModelImpl1 @Inject constructor(
    private val testString: String
) : NamedSetSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in NamedSetSampleModelImpl1 class.")
    }
}

@HiltSetBinds
@Named("sampleNamedSetB")
class NamedSetSampleModelImpl2 @Inject constructor(
    private val testString: String
) : NamedSetSampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in NamedSetSampleModelImpl2 class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    @Named("sampleNamedSetA")
    lateinit var sampleNamedSetA: @JvmSuppressWildcards Set<NamedSetSampleModel>

    @Inject
    @Named("sampleNamedSetB")
    lateinit var sampleNamedSetB: @JvmSuppressWildcards Set<NamedSetSampleModel>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        sampleNamedSetA.forEach { it.printTestString() }
        sampleNamedSetB.forEach { it.printTestString() }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class NamedSetSampleModelImpl1_BindsModule {
  @Binds
  @IntoSet
  @Named("sampleNamedSetA")
  public abstract NamedSetSampleModel bindNamedSetSampleModelImpl1(NamedSetSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class NamedSetSampleModelImpl2_BindsModule {
  @Binds
  @IntoSet
  @Named("sampleNamedSetB")
  public abstract NamedSetSampleModel bindNamedSetSampleModelImpl2(NamedSetSampleModelImpl2 target);
}

Map Multibinding - basics

You must use @HiltMapBinds to apply Map Multibinding. And you must to add a Key annotation with hilt's @MapKey applied, as in the code below. You can use the @ClassKey, @StringKey, @IntKey, @LongKey provided by hilt.

interface MapStringKeySampleModel {
    fun printTestString()
}

@HiltMapBinds
@StringKey("model1")
class MapStringKeySampleModelImpl1 @Inject constructor(
    private val testString: String
) : MapStringKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in MapStringKeySampleModelImpl1 class.")
    }
}

@HiltMapBinds
@StringKey("model2")
class MapStringKeySampleModelImpl2 @Inject constructor(
    private val testString: String
) : MapStringKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in MapStringKeySampleModelImpl2 class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var stringKeySampleMap: @JvmSuppressWildcards Map<String, Provider<MapStringKeySampleModel>>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        for ((k, v) in stringKeySampleMap) {
            Log.d("Test!!", "key: $k")
            v.get().printTestString()
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class MapStringKeySampleModelImpl1_BindsModule {
  @Binds
  @IntoMap
  @StringKey("model1")
  public abstract MapStringKeySampleModel bindMapStringKeySampleModelImpl1(MapStringKeySampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class MapStringKeySampleModelImpl2_BindsModule {
  @Binds
  @IntoMap
  @StringKey("model2")
  public abstract MapStringKeySampleModel bindMapStringKeySampleModelImpl2(MapStringKeySampleModelImpl2 target);
}

Map Multibinding - custom key

And you can define and use map key annotations. In addition to enum classes, you can define other types.

enum class SampleType {
    SAMPLE1, SAMPLE2, DEFAULT
}

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class SampleMapCustomKey(val key: SampleType)

interface MapCustomKeySampleModel {
    fun printTestString()
}

@HiltMapBinds
@SampleMapCustomKey(SampleType.SAMPLE1)
class MapCustomKeySampleModelImpl1 @Inject constructor(
    private val testString: String
) : MapCustomKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in MapCustomKeySampleModelImpl1 class.")
    }
}

@HiltMapBinds
@SampleMapCustomKey(SampleType.SAMPLE2)
class MapCustomKeySampleModelImpl2 @Inject constructor(
    private val testString: String
) : MapCustomKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in MapCustomKeySampleModelImpl2 class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var customKeySampleMap: @JvmSuppressWildcards Map<SampleType, Provider<MapCustomKeySampleModel>>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        for ((k, v) in customKeySampleMap) {
            Log.d("Test!!", "key: $k")
            v.get().printTestString()
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class MapCustomKeySampleModelImpl1_BindsModule {
  @Binds
  @IntoMap
  @SampleMapCustomKey(key = SampleType.SAMPLE1)
  public abstract MapCustomKeySampleModel bindMapCustomKeySampleModelImpl1(MapCustomKeySampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class MapCustomKeySampleModelImpl2_BindsModule {
  @Binds
  @IntoMap
  @SampleMapCustomKey(key = SampleType.SAMPLE2)
  public abstract MapCustomKeySampleModel bindMapCustomKeySampleModelImpl2(MapCustomKeySampleModelImpl2 target);
}

Map Multibinding - complex custom key

You can use key annotations with multiple parameters as in the code below. Complex custom keys require dependencies from the auto-value and auto-value-annotation libraries. For more information, see References.

/***
 * Complex key require the following dependencies:
 *
 *   def autoValueVersion = "1.9"
 *   implementation "com.google.auto.value:auto-value:$autoValueVersion"
 *   implementation "com.google.auto.value:auto-value-annotations:$autoValueVersion"
 */
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MapKey(unwrapValue = false)
annotation class SampleMapComplexKey(
    val key1: String,
    val key2: KClass<*>,
    val key3: Array<String>,
    val key4: IntArray,
    val key5: SampleType
)

interface MapComplexKeySampleModel {
    fun printTestString()
}

@HiltMapBinds
@SampleMapComplexKey(
    key1 = "sample1",
    key2 = MapComplexKeySampleModelImpl1::class,
    key3 = ["s1", "s2", "s3"],
    key4 = [1, 2, 3],
    key5 = SampleType.SAMPLE1
)
class MapComplexKeySampleModelImpl1 @Inject constructor(
    private val testString: String
) : MapComplexKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in MapComplexKeySampleModelImpl1 class.");
    }
}

@HiltMapBinds
@SampleMapComplexKey(
    key1 = "sample2",
    key2 = MapComplexKeySampleModelImpl2::class,
    key3 = ["s4", "s5", "s6"],
    key4 = [4, 5, 6],
    key5 = SampleType.SAMPLE2
)
class MapComplexKeySampleModelImpl2 @Inject constructor(
    private val testString: String
) : MapComplexKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in MapComplexKeySampleModelImpl2 class.");
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var complexKeySampleMap: @JvmSuppressWildcards Map<SampleMapComplexKey, Provider<MapComplexKeySampleModel>>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        for ((k, v) in complexKeySampleMap) {
            Log.d("Test!!", "key: $k")
            v.get().printTestString()
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class MapComplexKeySampleModelImpl1_BindsModule {
  @Binds
  @IntoMap
  @SampleMapComplexKey(
          key1 = "sample1",
          key2 = MapComplexKeySampleModelImpl1.class,
          key3 = {"s1","s2","s3"},
          key4 = {1,2,3},
          key5 = SampleType.SAMPLE1
  )
  public abstract MapComplexKeySampleModel bindMapComplexKeySampleModelImpl1(MapComplexKeySampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class MapComplexKeySampleModelImpl2_BindsModule {
  @Binds
  @IntoMap
  @SampleMapComplexKey(
          key1 = "sample2",
          key2 = MapComplexKeySampleModelImpl2.class,
          key3 = {"s4","s5","s6"},
          key4 = {4,5,6},
          key5 = SampleType.SAMPLE2
  )
  public abstract MapComplexKeySampleModel bindMapComplexKeySampleModelImpl2(MapComplexKeySampleModelImpl2 target);
}

Map Multibinding - qualifier

If you want to configure multiple Map Multibinding of the same type, you can use @Qualifier(javax.inject.Qualifier) annotations like this:

enum class SampleKey {
    KEY1, KEY2, KEY3, KEY4, DEFAULT
}

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class QualifiedSampleMapCustomKey(val key: SampleKey)

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

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

interface QualifiedMapCustomKeySampleModel {
    fun printTestString()
}

@HiltMapBinds
@QualifiedSampleMapCustomKey(SampleKey.KEY1)
@SampleMapQualifierA
class QualifiedMapCustomKeySampleModelImpl1 @Inject constructor(
    private val testString: String
) : QualifiedMapCustomKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in QualifiedMapCustomKeySampleModelImpl1 class.")
    }
}

@HiltMapBinds
@QualifiedSampleMapCustomKey(SampleKey.KEY2)
@SampleMapQualifierB
class QualifiedMapCustomKeySampleModelImpl2 @Inject constructor(
    private val testString: String
) : QualifiedMapCustomKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in QualifiedMapCustomKeySampleModelImpl2 class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    @SampleMapQualifierA
    lateinit var qualifiedCustomKeySampleMapA: @JvmSuppressWildcards Map<SampleKey, Provider<QualifiedMapCustomKeySampleModel>>

    @Inject
    @SampleMapQualifierB
    lateinit var qualifiedCustomKeySampleMapB: @JvmSuppressWildcards Map<SampleKey, Provider<QualifiedMapCustomKeySampleModel>>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        for ((k, v) in qualifiedCustomKeySampleMapA) {
            Log.d("Test!!", "key: $k")
            v.get().printTestString()
        }

        for ((k, v) in qualifiedCustomKeySampleMapB) {
            Log.d("Test!!", "key: $k")
            v.get().printTestString()
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class QualifiedMapCustomKeySampleModelImpl1_BindsModule {
  @Binds
  @IntoMap
  @QualifiedSampleMapCustomKey(key = SampleKey.KEY1)
  @SampleMapQualifierA
  public abstract QualifiedMapCustomKeySampleModel bindQualifiedMapCustomKeySampleModelImpl1(QualifiedMapCustomKeySampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class QualifiedMapCustomKeySampleModelImpl2_BindsModule {
  @Binds
  @IntoMap
  @QualifiedSampleMapCustomKey(key = SampleKey.KEY2)
  @SampleMapQualifierB
  public abstract QualifiedMapCustomKeySampleModel bindQualifiedMapCustomKeySampleModelImpl2(QualifiedMapCustomKeySampleModelImpl2 target);
}

Map Multibinding - named

If you want to configure multiple Map Multibinding of the same type, you can use @Named(javax.inject.Named) annotations like this:

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class NamedSampleMapCustomKey(val key: SampleKey)

interface NamedMapCustomKeySampleModel {
    fun printTestString()
}

@HiltMapBinds
@NamedSampleMapCustomKey(SampleKey.KEY1)
@Named("sampleNamedMapA")
class NamedMapCustomKeySampleModelImpl1 @Inject constructor(
    private val testString: String
) : NamedMapCustomKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in NamedMapCustomKeySampleModelImpl1 class.")
    }
}

@HiltMapBinds
@NamedSampleMapCustomKey(SampleKey.KEY2)
@Named("sampleNamedMapB")
class NamedMapCustomKeySampleModelImpl2 @Inject constructor(
    private val testString: String
) : NamedMapCustomKeySampleModel {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in NamedMapCustomKeySampleModelImpl2 class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    @Named("sampleNamedMapA")
    lateinit var namedCustomKeySampleMapA: @JvmSuppressWildcards Map<SampleKey, Provider<NamedMapCustomKeySampleModel>>

    @Inject
    @Named("sampleNamedMapB")
    lateinit var namedCustomKeySampleMapB: @JvmSuppressWildcards Map<SampleKey, Provider<NamedMapCustomKeySampleModel>>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        for ((k, v) in namedCustomKeySampleMapA) {
            Log.d("Test!!", "key: $k")
            v.get().printTestString()
        }

        for ((k, v) in namedCustomKeySampleMapB) {
            Log.d("Test!!", "key: $k")
            v.get().printTestString()
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class NamedMapCustomKeySampleModelImpl1_BindsModule {
  @Binds
  @IntoMap
  @NamedSampleMapCustomKey(key = SampleKey.KEY1)
  @Named("sampleNamedMapA")
  public abstract NamedMapCustomKeySampleModel bindNamedMapCustomKeySampleModelImpl1(NamedMapCustomKeySampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class NamedMapCustomKeySampleModelImpl2_BindsModule {
  @Binds
  @IntoMap
  @NamedSampleMapCustomKey(key = SampleKey.KEY2)
  @Named("sampleNamedMapB")
  public abstract NamedMapCustomKeySampleModel bindNamedMapCustomKeySampleModelImpl2(NamedMapCustomKeySampleModelImpl2 target);
}



# Supported

Generic Type - single

You can set the return type to a single generic type. Not only @HiltBinds, but also @HiltSetBinds and @HiltMapBinds.

interface SingleGenericSampleModel<T> {
    fun printTestString(data: T)
}

@HiltBinds
class SingleGenericSampleModelImpl1 @Inject constructor(
    private val testString: String
) : SingleGenericSampleModel<Int> {

    override fun printTestString(data: Int) {
        Log.d("Test!!", "TestString is `$testString` in GenericSampleModelImpl1 class. :: Generic type is <Int>")
    }
}

@HiltBinds
class SingleGenericSampleModelImpl2 @Inject constructor(
    private val testString: String
) : SingleGenericSampleModel<String> {

    override fun printTestString(model: String) {
        Log.d("Test!!", "TestString is `$testString` in GenericSampleModelImpl2 class. :: Generic type is <String>")
    }
}

@HiltBinds
class SingleGenericSampleModelImpl3 @Inject constructor(
    private val testString: String
) : SingleGenericSampleModel<Any> {

    override fun printTestString(data: Any) {
        Log.d("Test!!", "TestString is `$testString` in GenericSampleModelImpl3 class. :: Generic type is <Any>")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var singleGenericSampleModel1: SingleGenericSampleModel<Int>

    @Inject
    lateinit var singleGenericSampleModel2: SingleGenericSampleModel<String>

    @Inject
    lateinit var singleGenericSampleModel3: SingleGenericSampleModel<Any>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        singleGenericSampleModel1.printTestString(1205)
        singleGenericSampleModel2.printTestString("String")
        singleGenericSampleModel3.printTestString(1205.97)
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class SingleGenericSampleModelImpl1_BindsModule {
  @Binds
  public abstract SingleGenericSampleModel<Integer> bindSingleGenericSampleModelImpl1(SingleGenericSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class SingleGenericSampleModelImpl2_BindsModule {
  @Binds
  public abstract SingleGenericSampleModel<String> bindSingleGenericSampleModelImpl2(SingleGenericSampleModelImpl2 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class SingleGenericSampleModelImpl3_BindsModule {
  @Binds
  public abstract SingleGenericSampleModel<Object> bindSingleGenericSampleModelImpl3(SingleGenericSampleModelImpl3 target);
}

Generic Type - multiple

You can set the return type to multiple generic types. Not only @HiltBinds, but also @HiltSetBinds and @HiltMapBinds.

interface MultipleGenericSampleModel<T1, T2> {
    fun printTestString(data1: T1, data2: T2)
}

@HiltBinds
class MultipleGenericSampleModelImpl @Inject constructor(
    private val testString: String
) : MultipleGenericSampleModel<Int, Any> {

    override fun printTestString(data1: Int, data2: Any) {
        Log.d("Test!!", "TestString is `$testString` in GenericSampleModelImpl1 class. :: Generic type is <Int, Any>")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var multipleGenericSampleModel: MultipleGenericSampleModel<Int, Any>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        multipleGenericSampleModel.printTestString(97, 1205)
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class MultipleGenericSampleModelImpl_BindsModule {
  @Binds
  public abstract MultipleGenericSampleModel<Integer, Object> bindMultipleGenericSampleModelImpl(MultipleGenericSampleModelImpl target);
}

Generic Type - nested type

You can set the return type as a nested generic type. There is no limit of depth because finds generic types recursively. Not only @HiltBinds, but also @HiltSetBinds and @HiltMapBinds. Of course, you can use multiple members in the generic type.

interface NestedGenericSampleModel<T> {
    fun printTest(test: T)
}

data class SampleParam<T>(
    val key: T
)

@HiltBinds
class NestedGenericSampleModelImpl @Inject constructor(
    private val testString: String
) : NestedGenericSampleModel<SampleParam<SampleParam<String>>> {

    override fun printTest(test: SampleParam<SampleParam<String>>) {
        Log.d("Test!!", "TestString is `$testString` in NestedGenericSampleModelImpl class. :: $test")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

  @Inject
  lateinit var nestedGenericSampleModel: NestedGenericSampleModel<SampleParam<SampleParam<String>>>

  override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)

      val test = SampleParam(
          key = SampleParam(
              key = "nestedTestKey"
          )
      )
      nestedGenericSampleModel.printTest(test)
  }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class NestedGenericSampleModelImpl_BindsModule {
  @Binds
  public abstract NestedGenericSampleModel<SampleParam<SampleParam<String>>> bindNestedGenericSampleModelImpl(
      NestedGenericSampleModelImpl target);
}

Generic Type - set multibinding

You can set the return type as a generic type through @HiltSetBinds. Of course, you can use multiple members in the generic type.

interface SetGenericSampleModel<T> {
    fun printTestString(data: T)
}

@HiltSetBinds
class SetGenericSampleModelImpl1 @Inject constructor(
    private val testString: String
) : SetGenericSampleModel<Int> {

    override fun printTestString(data: Int) {
        Log.d("Test!!", "TestString is `$testString` in SetGenericSampleModelImpl1 class. :: Generic type is <Int>")
    }
}

@HiltSetBinds
class SetGenericSampleModelImpl2 @Inject constructor(
    private val testString: String
) : SetGenericSampleModel<Int> {

    override fun printTestString(data: Int) {
        Log.d("Test!!", "TestString is `$testString` in SetGenericSampleModelImpl2 class. :: Generic type is <Int>")
    }
}

@HiltSetBinds
class SetGenericSampleModelImpl3 @Inject constructor(
    private val testString: String
) : SetGenericSampleModel<String> {

    override fun printTestString(data: String) {
        Log.d("Test!!", "TestString is `$testString` in SetGenericSampleModelImpl3 class. :: Generic type is <String>")
    }
}

@HiltSetBinds
class SetGenericSampleModelImpl4 @Inject constructor(
    private val testString: String
) : SetGenericSampleModel<String> { 
  
    override fun printTestString(data: String) {
        Log.d("Test!!", "TestString is `$testString` in SetGenericSampleModelImpl4 class. :: Generic type is <String>")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var setGenericSampleModelA: @JvmSuppressWildcards Set<SetGenericSampleModel<Int>>

    @Inject
    lateinit var setGenericSampleModelB: @JvmSuppressWildcards Set<SetGenericSampleModel<String>>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setGenericSampleModelA.forEach {
            it.printTestString(1) 
        }

        setGenericSampleModelB.forEach {
            it.printTestString("String1")
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class SetGenericSampleModelImpl1_BindsModule {
  @Binds
  @IntoSet
  public abstract SetGenericSampleModel<Integer> bindSetGenericSampleModelImpl1(SetGenericSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class SetGenericSampleModelImpl2_BindsModule {
  @Binds
  @IntoSet
  public abstract SetGenericSampleModel<Integer> bindSetGenericSampleModelImpl2(SetGenericSampleModelImpl2 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class SetGenericSampleModelImpl3_BindsModule {
  @Binds
  @IntoSet
  public abstract SetGenericSampleModel<String> bindSetGenericSampleModelImpl3(SetGenericSampleModelImpl3 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class SetGenericSampleModelImpl4_BindsModule {
  @Binds
  @IntoSet
  public abstract SetGenericSampleModel<String> bindSetGenericSampleModelImpl4(SetGenericSampleModelImpl4 target);
}

Generic Type - map multibinding

You can set the return type as a generic type through @HiltMapBinds. Of course, you can use multiple members in the generic type.

interface MapGenericSampleModel<T> {
    fun printTestString(data: T)
}

@HiltMapBinds
@StringKey("impl1")
class MapGenericSampleModelImpl1 @Inject constructor(
    private val testString: String
) : MapGenericSampleModel<Int> {

    override fun printTestString(data: Int) {
        Log.d("Test!!", "TestString is `$testString` in MapGenericSampleModelImpl1 class. :: Generic type is <Int>")
    }
}

@HiltMapBinds
@StringKey("impl2")
class MapGenericSampleModelImpl2 @Inject constructor(
    private val testString: String
) : MapGenericSampleModel<Int> {

    override fun printTestString(data: Int) {
        Log.d("Test!!", "TestString is `$testString` in MapGenericSampleModelImpl2 class. :: Generic type is <Int>")
    }
}

@HiltMapBinds
@StringKey("impl3")
class MapGenericSampleModelImpl3 @Inject constructor(
    private val testString: String
) : MapGenericSampleModel<String> {

    override fun printTestString(data: String) {
        Log.d("Test!!", "TestString is `$testString` in MapGenericSampleModelImpl3 class. :: Generic type is <String>")
    }
}

@HiltMapBinds
@StringKey("impl4")
class MapGenericSampleModelImpl4 @Inject constructor(
    private val testString: String
) : MapGenericSampleModel<String> {

    override fun printTestString(data: String) {
        Log.d("Test!!", "TestString is `$testString` in MapGenericSampleModelImpl4 class. :: Generic type is <String>")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var mapGenericSampleModelA: @JvmSuppressWildcards Map<String, MapGenericSampleModel<Int>>

    @Inject
    lateinit var mapGenericSampleModelB: @JvmSuppressWildcards Map<String, MapGenericSampleModel<String>>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
      
        for ((k, v) in mapGenericSampleModelA) { 
            Log.d("Test!!", "key: $k")
            v.printTestString(1234)
        }
      
        for ((k, v) in mapGenericSampleModelB) { 
            Log.d("Test!!", "key: $k")
            v.printTestString("4567")
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class MapGenericSampleModelImpl1_BindsModule {
  @Binds
  @IntoMap
  @StringKey("impl1")
  public abstract MapGenericSampleModel<Integer> bindMapGenericSampleModelImpl1(MapGenericSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class MapGenericSampleModelImpl2_BindsModule {
  @Binds
  @IntoMap
  @StringKey("impl2")
  public abstract MapGenericSampleModel<Integer> bindMapGenericSampleModelImpl2(MapGenericSampleModelImpl2 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class MapGenericSampleModelImpl3_BindsModule {
  @Binds
  @IntoMap
  @StringKey("impl3")
  public abstract MapGenericSampleModel<String> bindMapGenericSampleModelImpl3(MapGenericSampleModelImpl3 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class MapGenericSampleModelImpl4_BindsModule {
  @Binds
  @IntoMap
  @StringKey("impl4")
  public abstract MapGenericSampleModel<String> bindMapGenericSampleModelImpl4(MapGenericSampleModelImpl4 target);
}

Generic Type w/ combined - general

The "combined" value lets you set the return type to star-projections for generic type. Of course, you can use multiple members in the generic type.

interface StarSingleGenericSampleModel<T> {
    fun printTestString()
}

@HiltBinds(combined = true)
class StarSingleGenericSampleModelImpl @Inject constructor(
    private val testString: String
) : StarSingleGenericSampleModel<Int> {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in StarSingleGenericSampleModelImpl class. :: Generic type is <Int>")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var starGenericSampleModel: StarSingleGenericSampleModel<*>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
      
        starGenericSampleModel.printTestString()
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class StarSingleGenericSampleModelImpl_BindsModule {
  @Binds
  public abstract StarSingleGenericSampleModel<?> bindStarSingleGenericSampleModelImpl(StarSingleGenericSampleModelImpl target);
}

Generic Type w/ combined - set multibinding

The "combined" value lets you set the return type to star-projections for generic type. Of course, you can use multiple members in the generic type.

interface SetStarGenericSampleModel<T> {
    fun printTestString()
}

@HiltSetBinds(combined = true)
class SetStarGenericSampleModelImpl1 @Inject constructor(
    private val testString: String
) : SetStarGenericSampleModel<String> {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in SetStarGenericSampleModelImpl1 class. :: Generic type is <String>") 
    }
}

@HiltSetBinds(combined = true)
class SetStarGenericSampleModelImpl2 @Inject constructor(
    private val testString: String
) : SetStarGenericSampleModel<Int> {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in SetStarGenericSampleModelImpl2 class. :: Generic type is <Int>") 
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var setStarGenericSampleModel: @JvmSuppressWildcards Set<SetStarGenericSampleModel<*>>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setStarGenericSampleModel.forEach {
            it.printTestString()
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class SetStarGenericSampleModelImpl1_BindsModule {
  @Binds
  @IntoSet
  public abstract SetStarGenericSampleModel<?> bindSetStarGenericSampleModelImpl1(SetStarGenericSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class SetStarGenericSampleModelImpl2_BindsModule {
  @Binds
  @IntoSet
  public abstract SetStarGenericSampleModel<?> bindSetStarGenericSampleModelImpl2(SetStarGenericSampleModelImpl2 target);
}

Generic Type w/ combined - map multibinding

The "combined" value lets you set the return type to star-projections for generic type. Of course, you can use multiple members in the generic type.

interface MapStarGenericSampleModel<T> {
    fun printTestString()
}

@HiltMapBinds(combined = true)
@StringKey("impl1")
class MapStarGenericSampleModelImpl1 @Inject constructor(
    private val testString: String
) : MapStarGenericSampleModel<Int> {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in MapStarGenericSampleModelImpl1 class. :: Generic type is <Int>")
    }
}

@HiltMapBinds(combined = true)
@StringKey("impl2")
class MapStarGenericSampleModelImpl2 @Inject constructor(
    private val testString: String
) : MapStarGenericSampleModel<String> {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in MapStarGenericSampleModelImpl2 class. :: Generic type is <String>")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject 
    lateinit var mapStarGenericSampleModel: @JvmSuppressWildcards Map<String, MapStarGenericSampleModel<*>>
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        for ((k, v) in mapStarGenericSampleModel) {
            Log.d("Test!!", "key: $k")
            v.printTestString()
        }
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class MapStarGenericSampleModelImpl1_BindsModule {
  @Binds
  @IntoMap
  @StringKey("impl1")
  public abstract MapStarGenericSampleModel<?> bindMapStarGenericSampleModelImpl1(MapStarGenericSampleModelImpl1 target);
}

@Module
@InstallIn(SingletonComponent.class)
abstract class MapStarGenericSampleModelImpl2_BindsModule {
  @Binds
  @IntoMap
  @StringKey("impl2")
  public abstract MapStarGenericSampleModel<?> bindMapStarGenericSampleModelImpl2(MapStarGenericSampleModelImpl2 target);
}

Nested Type

It also supports nested class as below code. There is no limit of depth because finds generic types recursively.

interface NestedSampleModel {
    interface SampleModel {
        interface SampleModelInternal {
            fun printTestString()
        }
    }
}

@HiltBinds
class NestedSampleModelImpl @Inject constructor(
    private val testString: String
) : NestedSampleModel.SampleModel.SampleModelInternal {

    override fun printTestString() {
        Log.d("Test!!", "TestString is `$testString` in NestedSampleModelImpl class.")
    }
}

// The code to be injected.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var nestedSampleModel: NestedSampleModel.SampleModel.SampleModelInternal
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        nestedSampleModel.printTestString()
    }
}
// generated code
@Module
@InstallIn(SingletonComponent.class)
abstract class NestedSampleModelImpl_BindsModule {
  @Binds
  public abstract NestedSampleModel.SampleModel.SampleModelInternal bindNestedSampleModelImpl(
          NestedSampleModelImpl target);
}



# Performance monitoring

You can monitor the elapsed time during annotation processing in the Build > Build Output tab of Android Studio.



# License

Copyright 2022 ParkSM

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.