Skip to content

@Single(binds = [...]) does not reuse the same instance for generic interfacesΒ #321

@adepto-io

Description

@adepto-io

Describe the bug
When using Koin Annotations with @Single(binds = [...]) on a provider function that returns a generic type, Koin creates separate singleton instances for each interface instead of sharing the same underlying object. This happens even when the concrete class implements all the bound interfaces.

To Reproduce

Steps to reproduce the behavior:

  1. Define generic interfaces and a concrete implementation:
interface CacheReader<K, V> {
    fun get(key: K): V?
}

interface CacheWriter<K, V> {
    fun put(key: K, value: V)
}

class Cache<K, V> : CacheReader<K, V>, CacheWriter<K, V> {
    private val storage = mutableMapOf<K, V>()
    override fun get(key: K) = storage[key]
    override fun put(key: K, value: V) { storage[key] = value }
}

data class Account(val id: String, val name: String)
  1. Create a provider with @single(binds = [...]):
@Single(binds = [CacheReader::class, CacheWriter::class])
fun provideAccountCache() = Cache<String, Account>()
  1. Inject both interfaces:
class MyViewModel(
    private val reader: CacheReader<String, Account>,
    private val writer: CacheWriter<String, Account>
) {
    init { 
        println(reader === writer) }
    }
}
  1. Observe that reader and writer are different instances.

Expected behavior

@single(binds = [...]) should bind the same singleton instance to all specified interfaces, so injecting any bound interface returns the same object.

Koin project used and used version (please complete the following information):

  • Koin Annotations (KSP) version: 2.3.1
  • Koin version: 4.1.0
  • Kotlin version: 2.2.21

Additional moduleDefinition

Workaround that currently works:

@Single
fun provideAccountCache() = Cache<String, Account>()

class MyViewModel(accountCache: Cache<String, Account>) {
    val accountCacheReader: CacheReader<String, Account> = accountCache
    val accountCacheWriter: CacheWriter<String, Account> = accountCache
}

This guarantees that CacheReader and CacheWriter share the same instance but requires casting in the consumer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions