Skip to main content
Version: 4.2

Advanced Patterns

This page covers patterns beyond one class per definition: collections of implementations, generic types, providers for runtime objects, and decorators. It assumes you know definitions and qualifiers. For classes from libraries (Retrofit, Room, OkHttp), see Classes you don't own.

Collections of dependencies​

To inject every implementation of an interface, bind each one to the interface and ask for a List:

interface PaymentProcessor {
fun process(amount: Double): Boolean
}

class CreditCardProcessor : PaymentProcessor { ... }
class PayPalProcessor : PaymentProcessor { ... }

class PaymentManager(private val processors: List<PaymentProcessor>)

val paymentModule = module {
single<CreditCardProcessor>() bind PaymentProcessor::class
single<PayPalProcessor>() bind PaymentProcessor::class
single<PaymentManager>()
}

A List<T> parameter resolves with getAll<T>(): every definition whose type, or one of its bound types, is T, with or without a qualifier. Outside a constructor, call getAll<PaymentProcessor>() on a Scope or on getKoin().

Behavior to know:

  • Empty list: if nothing is bound to T, the list is empty. No error, and the Compiler Plugin does not report a missing dependency for a List<T> parameter.

  • No order: do not rely on the order of the list. If order matters, sort it, for example by a priority property on the interface.

  • Current scope only: from a scope instance, getAll returns the definitions of that scope and of its linked scopes, including the root.

  • Single lookup: get<PaymentProcessor>() still returns only one of the bound definitions (the last one loaded). To pick one, give each a qualifier.

  • Strict override: with allowOverride(false), binding the same interface without a qualifier in two different modules fails at start with DefinitionOverrideException. Give each binding a qualifier.

When you need both the list and a specific implementation, combine binding and qualifiers. With the Compiler Plugin DSL and annotations, put @Named on the class:

@Named("creditCard")
class CreditCardProcessor : PaymentProcessor { ... }

@Named("paypal")
class PayPalProcessor : PaymentProcessor { ... }

val paymentModule = module {
single<CreditCardProcessor>() bind PaymentProcessor::class
single<PayPalProcessor>() bind PaymentProcessor::class
}

class CheckoutService(
@Named("creditCard") private val defaultProcessor: PaymentProcessor,
private val allProcessors: List<PaymentProcessor>,
)

Generic types​

Koin identifies a definition by its class and qualifier. Type arguments are erased: Repository<User> and Repository<Product> are the same key, Repository. Two definitions of the same generic class override each other, and the Compiler Plugin validates them as one.

// Does not work: the second definition overrides the first
val repositoryModule = module {
single<Repository<User>> { UserRepository() }
single<Repository<Product>> { ProductRepository() }
}

Use one of these instead:

  • Concrete types: inject UserRepository and ProductRepository directly. Each class is its own key.
  • Qualifiers: keep the generic interface and tell the definitions apart with a qualifier.
@Qualifier(User::class)
class UserRepository : Repository<User> { ... }

@Qualifier(Product::class)
class ProductRepository : Repository<Product> { ... }

val repositoryModule = module {
single<UserRepository>() bind Repository::class
single<ProductRepository>() bind Repository::class
}

class UserService(@Qualifier(User::class) private val repository: Repository<User>)

named<User>() is a type qualifier built from the class User. @Qualifier(User::class) is its annotation form.

Provider pattern​

Some objects need values that exist only at runtime: a title, a callback, a user choice. Instead of resolving them from Koin each time, inject a provider class that creates them:

class DialogProvider(private val context: Context) {

fun confirmDialog(title: String, onConfirm: () -> Unit): AlertDialog =
AlertDialog.Builder(context)
.setTitle(title)
.setPositiveButton("OK") { _, _ -> onConfirm() }
.create()
}

class SettingsScreen(private val dialogs: DialogProvider) {
fun askReset() {
dialogs.confirmDialog("Reset settings?") { reset() }.show()
}
}

Declare DialogProvider like any class (factory<DialogProvider>(), @Factory, or factoryOf(::DialogProvider)). The provider gets its dependencies from Koin, the created objects get their runtime values from the caller.

For a value known at the call site and needed in the constructor, injected parameters are the simpler option. For an instance that is costly and not always needed, inject Lazy<T>: see Constructor parameters.

Decorator pattern​

A decorator wraps an implementation to add behavior (logging, caching, rate limiting) and exposes the same interface. Koin has no decorator keyword: register the inner implementation under its own type, and bind the interface to the outer decorator.

interface NotificationService {
fun send(message: String)
}

class PushNotificationService : NotificationService { ... }

class LoggingNotificationService(
private val delegate: NotificationService,
private val logger: Logger,
) : NotificationService {
override fun send(message: String) {
logger.log("Sending: $message")
delegate.send(message)
}
}
import org.koin.plugin.module.dsl.create

fun loggingNotifications(
push: PushNotificationService,
logger: Logger,
): NotificationService = LoggingNotificationService(push, logger)

val notificationModule = module {
single<Logger>()
single<PushNotificationService>()
single { create(::loggingNotifications) }
}

Consumers ask for NotificationService and get the decorated instance. To stack several decorators, nest them in the same function: RateLimitedNotificationService(LoggingNotificationService(push, logger), limiter).

The inner implementation must not be bound to the interface too. Otherwise two definitions answer for NotificationService and one overrides the other.