Search code examples
androiddagger-2daggerdagger-hilt

How to inject a Map using Hilt


I have a setup something like this:

interface Animal {

}

class Dog @Inject constructor() : Animal

class AnimalProxy @Inject constructor(
    val animalFactory: AnimalFactory,
    val animalMap: Map<AnimalType, Animal>
) : Animal

enum class AnimalType {
    Pet,
    Wild
}
class AnimalFactory @Inject constructor ()

And this is how I am binding these object in the module

@Module
@InstallIn(SingletonComponent::class)
class AnimalModule {

    @MapKey
    annotation class AnimalTypeKey(val value: AnimalType)

    @Named(DOG)
    @Provides
    fun provideDog(
    ): Animal {
        return Dog()
    }

    @Provides
    @Singleton
    @IntoMap
    @AnimalTypeKey(AnimalType.Pet)
    @Named(PROXY)
    fun provideAnimalProxy(
        animalProxy: AnimalProxy
    ) : Animal = animalProxy

    companion object {
        const val DOG = "dog"
        const val PROXY = "proxy"
    }
}

But somehow something is not quite right and I am not able to figure out what's going on. I get an error cannot be provided without an @provides-annotated method. I know something is wrong when I am creating provideAnimalProxy but can't figure it out. Other working option I have is:

@InstallIn(SingletonComponent::class)
@Module
class AnimalsModule {

    @Singleton
    @Provides
    fun provideProxy(
        
    ): Animal {
        return AnimalProxy(
            AnimalFactory(),
            mapOf(
                AnimalType.Pet to Dog(),
            ),
        )
    }
}

But this one feels redundant as for AnimalFactory I already have an inject constructor.


Solution

  • @Named(DOG)
    @Provides
    fun provideDog(
    ): Animal {        // Provides @Named(DOG) Animal
        return Dog()   //   which will return a Dog()
    }
    
    @Provides
    @Singleton
    @IntoMap
    @AnimalTypeKey(AnimalType.Pet)  // Binds the key Pet
    @Named(PROXY)                   // into @Named(PROXY) Map<AnimalType, Animal>
    fun provideAnimalProxy(
        animalProxy: AnimalProxy
    ) : Animal = animalProxy        //   which will return an AnimalProxy()
    

    What it sounds like you want is for AnimalProxy to be bound, outside of the map, using its @Inject constructor. That would look like this:

    @Provides
    @IntoMap                       // @IntoMap here means Map<key, Animal>
                                   //   because the function returns Animal
    @AnimalTypeKey(AnimalType.Pet) // @AnimalTypeKey(Pet) means the key is Pet
                                   // Nothing is @Named, so the map isn't named
    fun provideDog(
    ): Animal {                    // Binds Pet into Map<AnimalType, Animal>
        return Dog()               //   which will return a Dog()
    }
    

    Or, since Dog has an @Inject annotated constructor, you can simplify into:

    @Provides
    @IntoMap
    @AnimalTypeKey(AnimalType.Pet)
    fun provideDog(dog: Dog): Animal = dog
    

    or with @Binds, in an abstract class or interface:

    @Binds
    @IntoMap
    @AnimalTypeKey(AnimalType.Pet)
    abstract fun bindDog(dog: Dog): Animal
    

    For AnimalProxy, you shouldn't use a @Provides method for an object you're injecting; if you want @Singleton on it you can annotate the AnimalProxy class itself. You can delete that part of the Module entirely.

    However, it looks like you might want AnimalProxy to show up in the Map<AnimalType, Animal> itself, which sounds like a circular reference: to create the Map you need to create each Animal, including the proxy, which requires creating the same Map you're currently trying to create. If that's the case, you do have a workaround: rather than injecting a Map<AnimalType, Animal>, you can automatically inject a Map<AnimalType, Provider<Animal>> the same way you can inject a Provider<Dog> rather than just a Dog. That way you won't express a need for a real AnimalProxy (or its Map) in the constructor, which keeps Dagger from complaining.