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:
- Compiler Plugin DSL
- Annotations
- Constructor / Classic DSL
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>()
}
interface PaymentProcessor {
fun process(amount: Double): Boolean
}
// bound to PaymentProcessor automatically
@Singleton
class CreditCardProcessor : PaymentProcessor { ... }
@Singleton
class PayPalProcessor : PaymentProcessor { ... }
@Singleton
class PaymentManager(private val processors: List<PaymentProcessor>)
val paymentModule = module {
singleOf(::CreditCardProcessor) bind PaymentProcessor::class
singleOf(::PayPalProcessor) bind PaymentProcessor::class
single { PaymentManager(getAll()) }
}
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 aList<T>parameter. -
No order: do not rely on the order of the list. If order matters, sort it, for example by a
priorityproperty on the interface. -
Current scope only: from a scope instance,
getAllreturns 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 withDefinitionOverrideException. 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
UserRepositoryandProductRepositorydirectly. Each class is its own key. - Qualifiers: keep the generic interface and tell the definitions apart with a qualifier.
- Compiler Plugin DSL
- Annotations
- Constructor / Classic DSL
@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>)
@Singleton
@Qualifier(User::class)
class UserRepository : Repository<User> { ... }
@Singleton
@Qualifier(Product::class)
class ProductRepository : Repository<Product> { ... }
@Singleton
class UserService(@Qualifier(User::class) private val repository: Repository<User>)
val repositoryModule = module {
single<Repository<User>>(named<User>()) { UserRepository() }
single<Repository<Product>>(named<Product>()) { ProductRepository() }
single { UserService(get(named<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)
}
}
- Compiler Plugin DSL
- Annotations
- Constructor / Classic DSL
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) }
}
@Module
class NotificationModule {
@Singleton
fun notifications(push: PushNotificationService, logger: Logger): NotificationService =
LoggingNotificationService(push, logger)
}
Declare PushNotificationService with @Singleton(binds = []), so it is not bound to NotificationService as well.
val notificationModule = module {
singleOf(::Logger)
singleOf(::PushNotificationService)
single<NotificationService> {
LoggingNotificationService(delegate = get<PushNotificationService>(), logger = get())
}
}
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.
Related
- Definitions: definition types, binding, classes you don't own.
- Qualifiers:
named(), type qualifiers and@Named. - Injected Parameters: runtime values passed at resolution.
- Injection:
get(),getAll(),inject()outside constructors. - Troubleshooting: override and missing definition errors.