koin icon indicating copy to clipboard operation
koin copied to clipboard

Multibinding like in Dagger 2

Open kirich1409 opened this issue 5 years ago • 31 comments

Dagger 2 has a good feature "Multibinding". It allows injection of multiple dependencies into collection. It's useful to make access by a key in a map of objects to make pluggable architecture.

kirich1409 avatar Apr 08 '20 18:04 kirich1409

Could you give some specs or proposal? Where? DSL | API ? a pseudo example

thanks 👍

arnaudgiuliani avatar Apr 09 '20 12:04 arnaudgiuliani

module {
    intoMap<KClass<Item>, ItemFactory>(ClassMapKey(Item1::class)) {
        Item1Factory()
    }

    intoMap<KClass<Item>, ItemFactory>(ClassMapKey(Item2::class)) {
        Item2Factory()
    }
}
class Sample: KoinComponent {

    val itemFactories: Map<MapKey<KClass<Item>, ItemFactory> by injectMultiMap()
}
interface MapKey<E> {
     val key: E
}

class <E : Enum<E>> EnumMapKey(override val key : E) : MapKey<E>

class <E : Any> ClassMapKey(val value: KClass<E>) : MapKey<Class<E>>

kirich1409 avatar Apr 09 '20 13:04 kirich1409

Why do you add the MapKey type here? I think we could just use a generic K as the key.

IVIanuu avatar Apr 10 '20 22:04 IVIanuu

@IVIanuu you mean something like this?

module {
    intoMap<KClass<Item>, ItemFactory>(Item1::class) {
        Item1Factory()
    }

    intoMap<KClass<Item>, ItemFactory>(Item2::class) {
        Item2Factory()
    }
}
class Sample: KoinComponent {

    val itemFactories: Map<MapKey<KClass<Item>, ItemFactory> by injectMultiMap()
}

kirich1409 avatar Apr 11 '20 05:04 kirich1409

One of the possible variants it to use the qualifier as the key. But I am not sure about that option

kirich1409 avatar Apr 13 '20 07:04 kirich1409

If I wanted to multibind aSet

class Foo(value: Int)

// equivalent to:
//    setOf(
//        Foo(1), Foo(2)
//    )
val myFirstSet: Set<Foo> by inject(named("first"))

// equivalent to:
//    setOf(
//        Foo(3), Foo(4)
//    )
val mySecondSet: Set<Foo> by inject(named("second"))

would that be declared as

module {
    intoSet(named("first")) { Foo(1) }
    intoSet(named("first")) { Foo(2) }

    intoSet(named("second")) { Foo(3) }
    intoSet(named("second")) { Foo(4) }
}

And then for a map

class Foo(value: Int)
class Bar(value: Int)

// equivalent to:
//    mapOf(
//        "one" to Foo(1), 
//        "two" to Foo(2)
//    )
val myMap: Map<String, Foo> by inject(named("first"))

// equivalent to:
//    mapOf(
//        Bar(2) to Foo(3), 
//        Bar(3) to Foo(4)
//    )
val myOtherMap: Map<Bar, Foo> by inject(named("second")) 

could I get away with a declaration like this -

module {
    intoMap(key = "one", qualifier = named("first")) { Foo(1) }
    intoMap(key = "two", qualifier = named("first")) { Foo(2) }

    intoMap(key = "three") { Foo(101) } // unqualified, so not injected into 'myMap'

    intoMap(key = Bar(2), qualifier = named("second")) { Foo(3) }
    intoMap(key = Bar(3), qualifier = named("second")) { Foo(4) }

    intoMap(key = Bar(7)) { Foo(99) } // unqualified, so not injected into 'myOtherMap'
}

Without using named parameters or qualifiers

module {
    intoMap(Bar(4), named("second")) { Foo(5) } // without using named parameters

    intoMap("three") { Foo(6) } // technically possible, but harder to read

    intoMap(Bar(5)) { Foo(7) } // technically possible, but harder to read
}

That seems to hang together if I declare

inline fun <reified T> Module.intoSet(
    qualifier: Qualifier? = null,
    noinline definition: Definition<T>
) { ... }

inline fun <reified T> Module.intoMap(
    key: Any,
    qualifier: Qualifier? = null,
    noinline definition: Definition<T>
) { ... }

psh avatar May 02 '20 15:05 psh

You can make a proposal via PR. The problem of such, is the reuse of strings naming 🤔 that can lead to configuration error (misspelling)

arnaudgiuliani avatar Oct 28 '20 09:10 arnaudgiuliani

Is there still any interest in this? It seems to have been a while, with no PR made :(

Cloudate9 avatar May 24 '22 06:05 Cloudate9

Imo this would be a great feature in line with koin's dynamic approach to loading and unload modules

cholwell avatar May 27 '22 16:05 cholwell

This seems to work fairly well for anyone that stumbles upon this

fun <T> Module.set(qualifier: Qualifier) = single(qualifier) {
    mutableSetOf<T>()
}

fun <T> Scope.getSet(qualifier: Qualifier) = get<MutableSet<T>>(qualifier)

fun <T> Module.intoSet(
    setQualifier: Qualifier, valueQualifier: Qualifier, definition: Definition<T>
) {
    single(valueQualifier, true) {
        getSet<T>(setQualifier).add(definition(this, parametersOf()))
        definition
    }
}

cholwell avatar May 27 '22 23:05 cholwell

what's the interest here? Inject into a set/map, some values?

arnaudgiuliani avatar May 28 '22 09:05 arnaudgiuliani

In my case, I'm interested in binding multiple implementations to some type, which can then be injected together in a collection in order to contribute functionality based on whether a module is loaded or not.

cholwell avatar May 28 '22 11:05 cholwell

Same here with cholwell. The goal is to inject multiple different implementations using a set/map, and use them as necessary by the program. For e.g, each implementation can have a different required condition to run, and the correct implementation can be selected at runtime, instead for hard coding the maximum number of implementation we can use in the constructor

Cloudate9 avatar May 28 '22 23:05 Cloudate9

Hello! You can find the example of injecting to a map with loading/unloading modules here.

qwert2603 avatar May 29 '22 13:05 qwert2603

Hi @qwert2603, unless I've misunderstood your article we're actually looking for a slightly more general case here. We need to be able to use definitions for the types contained in the multi binder in order to inject into them.

cholwell avatar May 29 '22 17:05 cholwell

This would also be an interesting use-case for Koin-annotations. With Dagger and Anvil, the qualifier is set on the injected type rather than the module having to list all the qualifier/value pairs. See here for an example. (Note that while @Named is used there, any qualifier can be used, so no need to rely on arbitrary strings).

Kernald avatar Jul 27 '22 00:07 Kernald

what's the interest here? Inject into a set/map, some values?

Interested in this for slightly different use case than what's been mentioned so far:

I want each Koin module to provide kotlinx.serialization rules for its models via a SerializersModule

Then, I want a top-level Koin module to get all SerializersModules from the graph and build a single Json instance that combines the rules of all downstream Koin modules

ankushg avatar Aug 19 '22 02:08 ankushg

I love this suggestion, something I've had to do was to create multiple factories of SerializersModules with a named qualifier for each of them. Then when I need to build an instance of Json I use the getAll() as demonstrated below, would be nice to get some sort of standardised api for this though 💯

factory {
    val json = Json {
        ignoreUnknownKeys = true
        isLenient = true
        coerceInputValues = true
    }
    getAll<SerializersModule>().forEach {
        json.serializersModule.plus(it)
    }
    return@factory json
}

wax911 avatar Jan 10 '23 16:01 wax911

what's the interest here? Inject into a set/map, some values?

Interested in this for slightly different use case than what's been mentioned so far:

I want each Koin module to provide kotlinx.serialization rules for its models via a SerializersModule

Then, I want a top-level Koin module to get all SerializersModules from the graph and build a single Json instance that combines the rules of all downstream Koin modules

If I get the case here, the idea is to declare for example a JsonSerialiser in each module. In the main (o another) module, gather those serializers into Json config.

arnaudgiuliani avatar Jan 19 '23 10:01 arnaudgiuliani

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jun 19 '23 03:06 stale[bot]

Seems a shame for this to die of staleness when there's a fair bit of interest 😁

cholwell avatar Jul 03 '23 15:07 cholwell

Please add this feature 🙏

hoc081098 avatar Aug 23 '23 03:08 hoc081098

Do you use Koin Property? it's a way to add field and you can inject them later if you need: https://insert-koin.io/docs/reference/koin-core/start-koin#read-property-from-a-module

arnaudgiuliani avatar Aug 29 '23 13:08 arnaudgiuliani

Seems like Injecting a list of dependencies should do. Here the question I have is will this be aggregated over all other modules?

mslalith avatar Jan 01 '24 03:01 mslalith

you mean aggregating values along the module declaration?

arnaudgiuliani avatar Jan 25 '24 09:01 arnaudgiuliani

interface LoggerDataSource

@Single
@Named("InMemoryLogger")
class LoggerInMemoryDataSource : LoggerDataSource

@Single
@Named("DatabaseLogger")
class LoggerLocalDataSource(private val logDao: LogDao) : LoggerDataSource

Let's take the example from docs. Say logging is being kept in it's own module, only LoggerDataSource was make public and all implementations were made internal. Now LoggerDataSource is being injected in another module (say :data). Although it's implementations are private to module will koin pick up and provide proper impl to :data

mslalith avatar Feb 24 '24 04:02 mslalith

I maybe mistaken @mslalith but if I understand your question what you want to know is what Named qualifier of LoggerDataSource impl (LoggerInMemoryDataSource or LoggerInMemoryDataSource) if you simply define LoggerDataSource as an input e.g.

class DataRepository(logger: LoggerDataSource) {
    // .....
}

Theoretically should result in some sort of runtime error as you've not defined a qualifier to inject possible with @Named e.g. class DataRepository(@Named("InMemoryLogger") logger: LoggerDataSource) with Annotations otherwise DataRepostitory(get(named("InMemoryLogger"))) with DSL

The only case this would possible differ is if you have another logger that is not bound to a named qualifier e.g.

@Single
class LoggerLocalDataSource(private val source: FileSystemManager) : LoggerDataSource

@Single
@Named("InMemoryLogger")
class LoggerInMemoryDataSource : LoggerDataSource

@Single
@Named("DatabaseLogger")
class LoggerLocalDataSource(private val logDao: LogDao) : LoggerDataSource

In the case of class DataRepository(logger: LoggerDataSource) the impl type that would possibly match this would be LoggerLocalDataSource

P.S I would wait for @arnaudgiuliani to confirm the above just to be sure, but you could also test this too 😃

wax911 avatar Feb 29 '24 12:02 wax911

Yes I would say that by default a qualifier also cover default type is there is no other definition for this type

arnaudgiuliani avatar Mar 29 '24 09:03 arnaudgiuliani