Skip to main content
Version: 4.2

Qualifiers

This guide covers how to use qualifiers in Koin to distinguish between multiple bindings of the same type.

What are Qualifiers?

Qualifiers allow you to differentiate between multiple definitions of the same type in your Koin modules. Without qualifiers, Koin would not know which instance to inject when you have multiple bindings of the same type.

When You Need Qualifiers

You need qualifiers when:

  • You have multiple implementations of the same interface
  • You need different configurations of the same type
  • You want to distinguish between instances with different purposes

Example scenario:

// You need two different OkHttpClient instances:
// - One with encryption for sensitive data
// - One with logging for debugging

// Without qualifiers, this won't work:
val networkModule = module {
single { OkHttpClient.Builder()...build() } // Which one?
single { OkHttpClient.Builder()...build() } // Conflict!
}

Named Dependencies

Koin uses the named() qualifier to distinguish between definitions of the same type.

Basic Named Qualifier

import org.koin.core.qualifier.named

val networkModule = module {
// Define with qualifiers
single(named("encrypted")) {
OkHttpClient.Builder()
.addInterceptor(EncryptionInterceptor())
.build()
}

single(named("logging")) {
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
}
}

Injecting Named Dependencies

In Module Definitions

class ApiService(
private val encryptedClient: OkHttpClient,
private val loggingClient: OkHttpClient
)

val apiModule = module {
single {
ApiService(
encryptedClient = get(named("encrypted")),
loggingClient = get(named("logging"))
)
}
}

With Field Injection

class NetworkActivity : AppCompatActivity() {
// Lazy injection with qualifier
private val encryptedClient: OkHttpClient by inject(named("encrypted"))
private val loggingClient: OkHttpClient by inject(named("logging"))

// Or eager injection
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val client: OkHttpClient = get(named("encrypted"))
}
}

With Autowire DSL

// Autowire doesn't automatically resolve named dependencies
// Use classic DSL when you need qualifiers

val appModule = module {
single(named("encrypted")) { OkHttpClient.Builder().build() }
single(named("logging")) { OkHttpClient.Builder().build() }

// Classic DSL - specify qualifiers explicitly
single {
ApiService(
encryptedClient = get(named("encrypted")),
loggingClient = get(named("logging"))
)
}
}
note

The autowire DSL (singleOf, factoryOf) does not automatically resolve named qualifiers. Use the classic DSL when definitions require qualifiers.

Common Use Cases

Multiple Network Clients

val networkModule = module {
// Base URL variants
single(named("api_v1")) {
Retrofit.Builder()
.baseUrl("https://api.example.com/v1/")
.build()
}

single(named("api_v2")) {
Retrofit.Builder()
.baseUrl("https://api.example.com/v2/")
.build()
}

// Different timeout configurations
single(named("fast")) {
OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.build()
}

single(named("slow")) {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
}

Multiple Database Instances

val databaseModule = module {
single(named("user_db")) {
Room.databaseBuilder(
androidContext(),
UserDatabase::class.java,
"user-database"
).build()
}

single(named("cache_db")) {
Room.databaseBuilder(
androidContext(),
CacheDatabase::class.java,
"cache-database"
).build()
}

// DAOs from different databases
single(named("user_dao")) {
get<UserDatabase>(named("user_db")).userDao()
}

single(named("cache_dao")) {
get<CacheDatabase>(named("cache_db")).cacheDao()
}
}

Build Variant Configurations

val configModule = module {
single(named("debug_config")) {
AppConfig(
apiUrl = "https://dev.example.com",
loggingEnabled = true,
crashReportingEnabled = false
)
}

single(named("release_config")) {
AppConfig(
apiUrl = "https://api.example.com",
loggingEnabled = false,
crashReportingEnabled = true
)
}

// Select config based on build type
single<AppConfig> {
if (BuildConfig.DEBUG) {
get(named("debug_config"))
} else {
get(named("release_config"))
}
}
}

Android Context Qualifiers

Unlike Hilt, Koin automatically provides the Android Context without requiring qualifiers.

Koin's Context Resolution

val androidModule = module {
// Context is automatically available
single {
SharedPreferences(
androidContext() // Application context automatically provided
)
}

single {
NotificationManager(
androidContext().getSystemService(Context.NOTIFICATION_SERVICE)
)
}
}

No Need for @ApplicationContext or @ActivityContext

In Hilt, you need:

// Hilt requires explicit qualifiers
class MyRepository @Inject constructor(
@ApplicationContext private val context: Context
)

In Koin, it's automatic:

class MyRepository(
private val context: Context // Just use Context - it's the Application context
)

val appModule = module {
single { MyRepository(androidContext()) }
}
info

Koin Advantage: The androidContext() function always provides the Application context. You don't need qualifiers to distinguish between Application and Activity contexts because Activity context should typically not be injected into long-lived objects.

When You Need Activity Context

For the rare cases where you need Activity context:

class ScreenMetrics(private val activity: Activity) {
fun getScreenSize(): Point {
val display = activity.windowManager.defaultDisplay
return Point().also { display.getSize(it) }
}
}

// Don't define in modules - create directly in Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Activity context used directly, not injected
val metrics = ScreenMetrics(this)
}
}
danger

Best Practice: Avoid injecting Activity context into long-lived objects. It can cause memory leaks. Use Application context (androidContext()) for dependencies that outlive a single Activity.

Type-Safe Qualifiers

Beyond string-based qualifiers, Koin supports type-safe qualifiers using types and enums for better compile-time safety.

Using Types as Qualifiers

Use named<T>() with any type (object, class, or sealed class) as a qualifier:

// Define qualifier types
object EncryptedClient
object LoggingClient
object FastClient

val networkModule = module {
single(named<EncryptedClient>()) {
OkHttpClient.Builder()
.addInterceptor(EncryptionInterceptor())
.build()
}

single(named<LoggingClient>()) {
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor())
.build()
}

single(named<FastClient>()) {
OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.build()
}
}

// Inject with type-safe qualifiers
val apiModule = module {
single {
ApiService(
encryptedClient = get(named<EncryptedClient>()),
loggingClient = get(named<LoggingClient>())
)
}
}

// In Activities
class NetworkActivity : AppCompatActivity() {
private val fastClient: OkHttpClient by inject(named<FastClient>())
}

Benefits:

  • ✅ Compile-time type safety
  • ✅ No string typos
  • ✅ IDE autocomplete and refactoring support
  • ✅ Clear intent with semantic types

Using Enums as Qualifiers

For better type safety and IDE autocomplete, use enums with the standard named() function:

Define Qualifier Enum

enum class NetworkClient {
ENCRYPTED,
LOGGING,
FAST,
SLOW
}

Use Enum Qualifiers

import org.koin.core.qualifier.named

val networkModule = module {
single(named(NetworkClient.ENCRYPTED)) {
OkHttpClient.Builder()
.addInterceptor(EncryptionInterceptor())
.build()
}

single(named(NetworkClient.LOGGING)) {
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor())
.build()
}

single(named(NetworkClient.FAST)) {
OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.build()
}

single(named(NetworkClient.SLOW)) {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
}

Inject with Enum Qualifiers

class ApiService(
private val encryptedClient: OkHttpClient,
private val loggingClient: OkHttpClient
)

val apiModule = module {
single {
ApiService(
encryptedClient = get(named(NetworkClient.ENCRYPTED)),
loggingClient = get(named(NetworkClient.LOGGING))
)
}
}

// In Activities
class NetworkActivity : AppCompatActivity() {
private val fastClient: OkHttpClient by inject(named(NetworkClient.FAST))
private val slowClient: OkHttpClient by inject(named(NetworkClient.SLOW))
}

Benefits of Enum Qualifiers

Type safety - Compile-time checking ✅ Refactoring support - Rename propagates automatically ✅ IDE autocomplete - Discover available qualifiers ✅ No typos - Compiler catches mistakes

// String qualifiers - easy to make mistakes
get(named("encryped")) // Typo! Runtime error

// Enum qualifiers - compile-time safety
get(named(NetworkClient.ENCRYPTED)) // Typo impossible

JSR-330 @Qualifier Annotations (Koin Annotations)

When using Koin Annotations, you can use JSR-330 standard @Qualifier annotations - the same ones used by Hilt, Dagger, and other DI frameworks.

Define Custom Qualifier Annotation

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DefaultDispatcher

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class EncryptedClient

Use in Koin Modules

import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

@Module
class DispatcherModule {

@Single
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

@Single
@DefaultDispatcher
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
}

@Module
class NetworkModule {

@Single
@EncryptedClient
fun provideEncryptedClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(EncryptionInterceptor())
.build()
}
}

Inject with Custom Qualifiers

import javax.inject.Inject
import org.koin.core.annotation.Single

@Single
class MyRepository @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@EncryptedClient private val httpClient: OkHttpClient
) {
suspend fun fetchData() = withContext(ioDispatcher) {
// Use encrypted client for API calls
httpClient.newCall(request).execute()
}
}

Benefits of JSR-330 @Qualifier

Standard - Works across multiple DI frameworks (Koin, Hilt, Dagger, Spring) ✅ Migration-friendly - Keep existing qualifier annotations when migrating from Hilt ✅ Type-safe - Custom annotations prevent typos ✅ Self-documenting - Annotation names explain the purpose

info

This JSR-330 support makes migrating from Hilt to Koin seamless - you can keep your existing @Qualifier annotations unchanged!

Naming Conventions

Follow these conventions for clear, maintainable code:

String-Based Names

// Good - descriptive, lowercase with underscores
single(named("encrypted_client")) { ... }
single(named("user_database")) { ... }
single(named("api_v2")) { ... }

// Avoid - unclear or inconsistent
single(named("client1")) { ... } // What does "1" mean?
single(named("ENCRYPTED")) { ... } // Inconsistent casing
single(named("encryptedClient")) { ... } // Camel case for qualifiers

Enum-Based Names

// Good - clear enum names, SCREAMING_SNAKE_CASE
enum class DatabaseType {
USER_DATA,
CACHE,
ANALYTICS
}

enum class ApiVersion {
V1,
V2,
V3
}

// Good - grouped related qualifiers
enum class NetworkConfig {
PRODUCTION,
STAGING,
DEVELOPMENT
}
// Group related qualifiers with prefixes
single(named("db_users")) { ... }
single(named("db_cache")) { ... }
single(named("db_analytics")) { ... }

single(named("retrofit_api")) { ... }
single(named("retrofit_cdn")) { ... }

single(named("client_encrypted")) { ... }
single(named("client_logging")) { ... }

Best Practices

1. Use Qualifiers Sparingly

// Good - qualifiers only when necessary
val appModule = module {
single { UserRepository(get()) } // No qualifier needed
single { AuthRepository(get()) } // Different types
}

// Over-qualification - avoid
val appModule = module {
single(named("user_repository")) { UserRepository(get()) }
single(named("auth_repository")) { AuthRepository(get()) }
// Unnecessary - types are already different
}

2. Prefer Type Differentiation Over Qualifiers

// Better - use different types
interface EncryptedHttpClient
interface LoggingHttpClient

class EncryptedOkHttpClient : OkHttpClient(), EncryptedHttpClient
class LoggingOkHttpClient : OkHttpClient(), LoggingHttpClient

val networkModule = module {
single<EncryptedHttpClient> { EncryptedOkHttpClient() }
single<LoggingHttpClient> { LoggingOkHttpClient() }
}

// Less ideal - same type with qualifiers
val networkModule = module {
single(named("encrypted")) { OkHttpClient() }
single(named("logging")) { OkHttpClient() }
}

3. Document Qualifiers

val networkModule = module {
// Client for secure API calls with encryption
single(named("encrypted")) {
OkHttpClient.Builder()
.addInterceptor(EncryptionInterceptor())
.build()
}

// Client for debugging with full request/response logging
single(named("logging")) {
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor())
.build()
}
}

4. Avoid Qualifier Chains

// Bad - complex qualifier dependencies
val badModule = module {
single(named("a")) { A() }
single(named("b")) { B(get(named("a"))) }
single(named("c")) { C(get(named("b"))) }
single(named("d")) { D(get(named("c"))) }
}

// Better - flatten or use different types
val goodModule = module {
single { A() }
single { B(get()) }
single { C(get()) }
single { D(get()) }
}
val networkModule = module {
// Production network stack
single(named("prod_client")) { createProductionClient() }
single(named("prod_api")) { createProductionApi(get(named("prod_client"))) }

// Development network stack
single(named("dev_client")) { createDevelopmentClient() }
single(named("dev_api")) { createDevelopmentApi(get(named("dev_client"))) }

// Select based on build variant
single<ApiService> {
if (BuildConfig.DEBUG) {
get(named("dev_api"))
} else {
get(named("prod_api"))
}
}
}

Common Pitfalls

Forgetting Qualifiers on Injection

val module = module {
single(named("encrypted")) { OkHttpClient() }
}

class MyRepository(
private val client: OkHttpClient // Which client?
)

val repoModule = module {
single {
MyRepository(get()) // ❌ Error: No definition for OkHttpClient
// Should be: get(named("encrypted"))
}
}

Mismatched Qualifier Names

val module = module {
single(named("encrypted_client")) { OkHttpClient() }
}

val repoModule = module {
single {
ApiService(
get(named("encrypted")) // ❌ Typo! Should be "encrypted_client"
)
}
}

Overusing Qualifiers

// Over-qualification anti-pattern
val badModule = module {
single(named("user_repository")) { UserRepository() }
single(named("order_repository")) { OrderRepository() }
single(named("product_repository")) { ProductRepository() }
// These are all different types - qualifiers unnecessary!
}

// Better - let types distinguish
val goodModule = module {
singleOf(::UserRepository)
singleOf(::OrderRepository)
singleOf(::ProductRepository)
}

Summary

Qualifiers in Koin allow you to:

  • Distinguish between multiple bindings of the same type
  • Configure different variants of the same dependency
  • Organize related definitions with meaningful names

Key takeaways:

  • Use named() for string qualifiers
  • Prefer enums for type-safe qualifiers
  • Avoid overusing qualifiers when types can differentiate
  • Document qualifier purposes
  • Use consistent naming conventions
  • Koin provides Android Context automatically - no qualifiers needed

Next Steps