Advanced Patterns
This guide covers advanced dependency injection patterns with Koin for complex scenarios and scaled applications.
Collections of Dependencies
Injecting All Implementations
Get all implementations of an interface:
interface PaymentProcessor {
fun process(amount: Double): Boolean
fun getName(): String
}
class CreditCardProcessor : PaymentProcessor {
override fun process(amount: Double) = true
override fun getName() = "Credit Card"
}
class PayPalProcessor : PaymentProcessor {
override fun process(amount: Double) = true
override fun getName() = "PayPal"
}
class CryptoProcessor : PaymentProcessor {
override fun process(amount: Double) = true
override fun getName() = "Crypto"
}
// Module definition
val paymentModule = module {
single<PaymentProcessor>(qualifier = named("creditCard")) { CreditCardProcessor() }
single<PaymentProcessor>(qualifier = named("paypal")) { PayPalProcessor() }
single<PaymentProcessor>(qualifier = named("crypto")) { CryptoProcessor() }
}
// Inject all processors
class PaymentManager(
private val processors: List<PaymentProcessor>
) {
fun getAvailablePaymentMethods(): List<String> {
return processors.map { it.getName() }
}
fun processWithMethod(methodName: String, amount: Double): Boolean {
val processor = processors.find { it.getName() == methodName }
return processor?.process(amount) ?: false
}
}
Since Koin doesn't have built-in multi-binding like Hilt's @IntoSet, use a factory pattern:
val paymentModule = module {
single {
PaymentManager(
processors = listOf(
get<PaymentProcessor>(named("creditCard")),
get<PaymentProcessor>(named("paypal")),
get<PaymentProcessor>(named("crypto"))
)
)
}
single<PaymentProcessor>(named("creditCard")) { CreditCardProcessor() }
single<PaymentProcessor>(named("paypal")) { PayPalProcessor() }
single<PaymentProcessor>(named("crypto")) { CryptoProcessor() }
}
Dynamic Collection Building
Build collections based on configuration:
data class AppConfig(
val enabledFeatures: Set<String>
)
interface FeatureModule {
val name: String
fun initialize()
}
class AnalyticsFeature : FeatureModule {
override val name = "analytics"
override fun initialize() { /* ... */ }
}
class CrashReportingFeature : FeatureModule {
override val name = "crash_reporting"
override fun initialize() { /* ... */ }
}
val featureModule = module {
single { AppConfig(enabledFeatures = setOf("analytics", "crash_reporting")) }
// All feature modules
single(named("analytics")) { AnalyticsFeature() }
single(named("crash_reporting")) { CrashReportingFeature() }
// Dynamic collection based on config
single<List<FeatureModule>> {
val config = get<AppConfig>()
config.enabledFeatures.mapNotNull { featureName ->
getOrNull<FeatureModule>(named(featureName))
}
}
}
Map of Dependencies
Create a map of named dependencies:
interface Validator {
fun validate(value: String): Boolean
}
class EmailValidator : Validator {
override fun validate(value: String) = value.contains("@")
}
class PhoneValidator : Validator {
override fun validate(value: String) = value.matches(Regex("\\d{10}"))
}
val validationModule = module {
single<Validator>(named("email")) { EmailValidator() }
single<Validator>(named("phone")) { PhoneValidator() }
single<Map<String, Validator>> {
mapOf(
"email" to get(named("email")),
"phone" to get(named("phone"))
)
}
}
class FormValidator(private val validators: Map<String, Validator>) {
fun validateField(fieldType: String, value: String): Boolean {
return validators[fieldType]?.validate(value) ?: false
}
}
Lazy Injection
Lazy Property Injection
Defer dependency creation until first use:
class UserRepository {
init {
println("UserRepository created")
}
fun getUser(id: String): User { /* ... */ }
}
class UserViewModel : ViewModel(), KoinComponent {
// Created immediately
private val repository1: UserRepository = get()
// Created only when first accessed
private val repository2: UserRepository by inject()
// Kotlin lazy + Koin
private val repository3: Lazy<UserRepository> = lazy { get() }
fun loadUser() {
// repository2 is created here on first access
val user = repository2.getUser("123")
}
}
Lazy List Injection
class FeatureManager : KoinComponent {
// Lazy list - features created when first accessed
private val features: List<FeatureModule> by lazy {
listOf(
get<FeatureModule>(named("analytics")),
get<FeatureModule>(named("crash_reporting"))
)
}
fun initializeFeatures() {
// Features are created here
features.forEach { it.initialize() }
}
}
Optional Dependencies
Nullable Injection
Handle optional dependencies gracefully:
interface AnalyticsService {
fun track(event: String)
}
class GoogleAnalytics : AnalyticsService {
override fun track(event: String) { /* ... */ }
}
// Module may or may not include analytics
val appModule = module {
// Analytics only in release builds
if (BuildConfig.ENABLE_ANALYTICS) {
single<AnalyticsService> { GoogleAnalytics() }
}
}
// Handle optional analytics
class UserRepository : KoinComponent {
// Will be null if not defined
private val analytics: AnalyticsService? = getOrNull()
fun saveUser(user: User) {
// Use analytics if available
analytics?.track("user_saved")
// Save user logic...
}
}
Default Implementations
Provide fallback for missing dependencies:
class NoOpAnalytics : AnalyticsService {
override fun track(event: String) {
// Do nothing
}
}
val analyticsModule = module {
single<AnalyticsService> {
if (BuildConfig.ENABLE_ANALYTICS) {
GoogleAnalytics()
} else {
NoOpAnalytics()
}
}
}
class UserRepository(
private val analytics: AnalyticsService // Always present, may be no-op
) {
fun saveUser(user: User) {
analytics.track("user_saved")
}
}
Optional Feature Modules
class FeatureRegistry : KoinComponent {
fun getFeature(name: String): FeatureModule? {
return getOrNull(named(name))
}
fun hasFeature(name: String): Boolean {
return getFeature(name) != null
}
}
// Usage
class App : Application() {
private val featureRegistry: FeatureRegistry by inject()
override fun onCreate() {
super.onCreate()
if (featureRegistry.hasFeature("premium")) {
val premium = featureRegistry.getFeature("premium")
premium?.initialize()
}
}
}
Injected Parameters
Factory with Parameters
Pass runtime parameters to definitions:
class OrderProcessor(
private val orderId: String,
private val userRepository: UserRepository
) {
fun process() {
println("Processing order: $orderId")
}
}
val orderModule = module {
factory { params ->
OrderProcessor(
orderId = params.get(),
userRepository = get()
)
}
}
// Usage
class CheckoutViewModel : ViewModel(), KoinComponent {
fun checkout(orderId: String) {
val processor: OrderProcessor = get { parametersOf(orderId) }
processor.process()
}
}
Multiple Parameters
class ReportGenerator(
private val reportType: String,
private val startDate: String,
private val endDate: String,
private val database: Database
) {
fun generate(): Report { /* ... */ }
}
val reportModule = module {
factory { params ->
ReportGenerator(
reportType = params.get(0),
startDate = params.get(1),
endDate = params.get(2),
database = get()
)
}
}
// Usage with destructured declarations
val reportModule2 = module {
factory { (reportType, startDate, endDate) ->
ReportGenerator(
reportType = reportType,
startDate = startDate,
endDate = endDate,
database = get()
)
}
}
// Usage
fun generateReport() {
val generator: ReportGenerator = get {
parametersOf("sales", "2024-01-01", "2024-12-31")
}
val report = generator.generate()
}
ViewModel with Parameters
class ProductDetailViewModel(
private val productId: String,
private val productRepository: ProductRepository
) : ViewModel() {
private val _product = MutableStateFlow<Product?>(null)
val product: StateFlow<Product?> = _product.asStateFlow()
init {
loadProduct()
}
private fun loadProduct() {
viewModelScope.launch {
_product.value = productRepository.getProduct(productId)
}
}
}
val productModule = module {
viewModel { params ->
ProductDetailViewModel(
productId = params.get(),
productRepository = get()
)
}
}
// Usage in Activity
class ProductDetailActivity : AppCompatActivity() {
private val productId: String by lazy { intent.getStringExtra("product_id")!! }
private val viewModel: ProductDetailViewModel by viewModel {
parametersOf(productId)
}
}
// Usage in Compose
@Composable
fun ProductDetailScreen(productId: String) {
val viewModel: ProductDetailViewModel = koinViewModel {
parametersOf(productId)
}
val product by viewModel.product.collectAsState()
// UI code...
}
Conditional Bindings
Build Variant Dependencies
interface Logger {
fun log(message: String)
}
class DebugLogger : Logger {
override fun log(message: String) {
println("[DEBUG] $message")
}
}
class ReleaseLogger : Logger {
override fun log(message: String) {
// Log to crash reporting service
}
}
val loggingModule = module {
single<Logger> {
if (BuildConfig.DEBUG) {
DebugLogger()
} else {
ReleaseLogger()
}
}
}
Feature Flag Based Binding
interface FeatureFlagService {
fun isEnabled(flag: String): Boolean
}
val experimentalModule = module {
single<FeatureFlagService> { FeatureFlagServiceImpl() }
single<PaymentProcessor> {
val featureFlags = get<FeatureFlagService>()
if (featureFlags.isEnabled("new_payment_flow")) {
NewPaymentProcessor(get())
} else {
LegacyPaymentProcessor(get())
}
}
}
Platform-Specific Bindings (KMP)
// Common code
expect val platformModule: Module
// Android implementation
actual val platformModule = module {
single<ImageLoader> { AndroidImageLoader(androidContext()) }
single<StorageService> { AndroidStorageService() }
}
// iOS implementation
actual val platformModule = module {
single<ImageLoader> { IOSImageLoader() }
single<StorageService> { IOSStorageService() }
}
// Common app initialization
fun initializeKoin() {
startKoin {
modules(
commonModule,
platformModule // Platform-specific
)
}
}
Provider Pattern
Providing Instances
Use providers when you need a factory of factories:
class DialogProvider(private val context: Context) {
fun createConfirmDialog(
title: String,
message: String,
onConfirm: () -> Unit
): AlertDialog {
return AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton("OK") { _, _ -> onConfirm() }
.create()
}
fun createErrorDialog(error: String): AlertDialog {
return AlertDialog.Builder(context)
.setTitle("Error")
.setMessage(error)
.setPositiveButton("OK", null)
.create()
}
}
val uiModule = module {
factory { DialogProvider(androidContext()) }
}
class MainActivity : AppCompatActivity() {
private val dialogProvider: DialogProvider by inject()
fun showConfirmation() {
dialogProvider.createConfirmDialog(
title = "Confirm",
message = "Are you sure?",
onConfirm = { /* action */ }
).show()
}
}
Factory Provider
interface ViewModelFactory {
fun <T : ViewModel> create(clazz: Class<T>): T
}
class KoinViewModelFactory : ViewModelFactory, KoinComponent {
override fun <T : ViewModel> create(clazz: Class<T>): T {
return when (clazz) {
HomeViewModel::class.java -> get<HomeViewModel>() as T
ProfileViewModel::class.java -> get<ProfileViewModel>() as T
else -> throw IllegalArgumentException("Unknown ViewModel: $clazz")
}
}
}
val viewModelModule = module {
single<ViewModelFactory> { KoinViewModelFactory() }
viewModel { HomeViewModel(get()) }
viewModel { ProfileViewModel(get()) }
}
Delegation Patterns
Interface Delegation with DI
interface UserDataSource {
suspend fun getUser(id: String): User
suspend fun saveUser(user: User)
}
class RemoteUserDataSource(private val api: ApiService) : UserDataSource {
override suspend fun getUser(id: String) = api.getUser(id)
override suspend fun saveUser(user: User) = api.saveUser(user)
}
class CachedUserDataSource(
private val cache: UserCache,
private val delegate: UserDataSource // Injected delegate
) : UserDataSource by delegate {
override suspend fun getUser(id: String): User {
return cache.get(id) ?: delegate.getUser(id).also {
cache.put(id, it)
}
}
}
val dataModule = module {
single { RemoteUserDataSource(get()) }
single<UserDataSource> {
CachedUserDataSource(
cache = get(),
delegate = get<RemoteUserDataSource>()
)
}
}
Decorator Pattern
interface NotificationService {
fun send(message: String)
}
class BasicNotificationService : NotificationService {
override fun send(message: String) {
println("Notification: $message")
}
}
class LoggingNotificationService(
private val delegate: NotificationService,
private val logger: Logger
) : NotificationService {
override fun send(message: String) {
logger.log("Sending notification: $message")
delegate.send(message)
logger.log("Notification sent")
}
}
class RateLimitedNotificationService(
private val delegate: NotificationService,
private val rateLimiter: RateLimiter
) : NotificationService {
override fun send(message: String) {
if (rateLimiter.tryAcquire()) {
delegate.send(message)
} else {
println("Rate limit exceeded")
}
}
}
val notificationModule = module {
single { BasicNotificationService() }
single<NotificationService> {
// Stack decorators
RateLimitedNotificationService(
delegate = LoggingNotificationService(
delegate = get<BasicNotificationService>(),
logger = get()
),
rateLimiter = get()
)
}
}
Circular Dependencies Resolution
Constructor Injection Circular (Anti-Pattern)
// ❌ This creates a circular dependency
class ServiceA(val serviceB: ServiceB)
class ServiceB(val serviceA: ServiceA)
val badModule = module {
single { ServiceA(get()) } // Depends on ServiceB
single { ServiceB(get()) } // Depends on ServiceA - CIRCULAR!
}
Solution 1: Lazy Injection
class ServiceA {
val serviceB: ServiceB by inject() // Lazy
}
class ServiceB {
val serviceA: ServiceA by inject() // Lazy
}
val module = module {
single { ServiceA() }
single { ServiceB() }
}
Solution 2: Interface Extraction
interface ServiceAInterface {
fun doSomething()
}
class ServiceA : ServiceAInterface, KoinComponent {
private val serviceB: ServiceB by inject()
override fun doSomething() { /* ... */ }
}
class ServiceB(private val serviceA: ServiceAInterface) {
fun doSomethingElse() {
serviceA.doSomething()
}
}
val module = module {
single<ServiceAInterface> { ServiceA() }
single { ServiceB(get()) }
}
Solution 3: Refactor Dependencies
// Extract common logic into a third service
class SharedService
class ServiceA(private val shared: SharedService)
class ServiceB(private val shared: SharedService)
val module = module {
single { SharedService() }
single { ServiceA(get()) }
single { ServiceB(get()) }
}
Advanced Scoping Patterns
Scoped Factory
Create new instances within a scope:
class DialogState {
var title: String = ""
var message: String = ""
}
val dialogModule = module {
scope(named("dialog")) {
factory { DialogState() } // New instance each time
scoped { DialogPresenter() } // Single instance per scope
}
}
class DialogFragment : Fragment(), KoinScopeComponent {
override val scope: Scope by lazy {
createScope(this, named("dialog"))
}
// New state each time
fun createNewDialogState(): DialogState = scope.get()
}
Hierarchical Scopes
val appModule = module {
// Application level
single { Database() }
// Session scope
scope(named("session")) {
scoped { UserSession() }
scoped { AuthToken() }
}
// Feature scope (linked to session)
scope(named("shopping")) {
scoped { ShoppingCart(get()) } // Gets UserSession from linked session scope
}
}
// Create and link scopes
val sessionScope = getKoin().createScope("user_session", named("session"))
val shoppingScope = getKoin().createScope("shopping_cart", named("shopping"))
// Link shopping scope to session scope
shoppingScope.linkTo(sessionScope)
// Shopping cart can access session
val cart = shoppingScope.get<ShoppingCart>() // Can resolve UserSession from session scope
Scope Callbacks
class ResourceManager : KoinComponent {
private val resources = mutableListOf<Resource>()
fun allocate(): Resource {
val resource = Resource()
resources.add(resource)
return resource
}
fun releaseAll() {
resources.forEach { it.close() }
resources.clear()
}
}
val resourceModule = module {
scope(named("feature")) {
scoped { ResourceManager() }
}
}
class FeatureActivity : AppCompatActivity(), AndroidScopeComponent {
override val scope: Scope by lazy {
createScope(this, named("feature"))
}
override fun onCloseScope() {
// Called before scope is closed
val manager = scope.get<ResourceManager>()
manager.releaseAll()
}
}
Type Aliases and Generics
Generic Types
interface Repository<T> {
suspend fun get(id: String): T
suspend fun save(item: T)
}
class UserRepository : Repository<User> {
override suspend fun get(id: String): User { /* ... */ }
override suspend fun save(item: User) { /* ... */ }
}
class ProductRepository : Repository<Product> {
override suspend fun get(id: String): Product { /* ... */ }
override suspend fun save(item: Product) { /* ... */ }
}
val repositoryModule = module {
single<Repository<User>> { UserRepository() }
single<Repository<Product>> { ProductRepository() }
}
// Usage
class UserViewModel(
private val userRepo: Repository<User>,
private val productRepo: Repository<Product>
) : ViewModel()
Type Aliases
typealias UserId = String
typealias UserEmail = String
class UserService {
fun getUserById(id: UserId): User { /* ... */ }
fun getUserByEmail(email: UserEmail): User { /* ... */ }
}
// Both are actually String, use qualifiers to differentiate
val userModule = module {
factory(named("userId")) { "user-123" }
factory(named("userEmail")) { "user@example.com" }
single { UserService() }
}
Best Practices
1. Prefer Composition Over Collections
// ✅ Good - Explicit composition
class PaymentService(
private val creditCard: CreditCardProcessor,
private val paypal: PayPalProcessor
) {
fun processPayment(method: PaymentMethod, amount: Double) {
when (method) {
PaymentMethod.CREDIT_CARD -> creditCard.process(amount)
PaymentMethod.PAYPAL -> paypal.process(amount)
}
}
}
// ⚠️ Use carefully - Collection injection
class PaymentService(
private val processors: List<PaymentProcessor>
) {
// Need to find the right processor somehow
}
2. Limit Injected Parameters
// ✅ Good - One or two parameters
factory { (orderId: String) ->
OrderProcessor(orderId, get())
}
// ❌ Bad - Too many parameters, hard to maintain
factory { (p1, p2, p3, p4, p5) ->
ComplexService(p1, p2, p3, p4, p5, get(), get())
}
3. Use Named Qualifiers for Collections
// ✅ Good - Clear naming
module {
single(named("credit_card")) { CreditCardProcessor() }
single(named("paypal")) { PayPalProcessor() }
}
// ❌ Bad - Unclear
module {
single(named("processor1")) { CreditCardProcessor() }
single(named("processor2")) { PayPalProcessor() }
}
4. Document Complex Patterns
/**
* Payment module with multiple processors.
*
* Available processors:
* - "credit_card": Credit card processing
* - "paypal": PayPal processing
* - "crypto": Cryptocurrency processing
*
* PaymentManager aggregates all processors.
*/
val paymentModule = module {
single(named("credit_card")) { CreditCardProcessor() }
single(named("paypal")) { PayPalProcessor() }
single(named("crypto")) { CryptoProcessor() }
single {
PaymentManager(
processors = listOf(
get(named("credit_card")),
get(named("paypal")),
get(named("crypto"))
)
)
}
}
Common Pitfalls
1. Over-Using Injected Parameters
// ❌ Anti-pattern - Too many runtime parameters
class ComplexViewModel(
private val userId: String,
private val screenId: String,
private val mode: String,
private val config: Map<String, Any>,
private val repository: Repository
) : ViewModel()
// ✅ Better - Encapsulate parameters
data class ScreenConfig(
val userId: String,
val screenId: String,
val mode: String,
val options: Map<String, Any>
)
class ComplexViewModel(
private val config: ScreenConfig,
private val repository: Repository
) : ViewModel()
2. Circular Dependencies Through Lazy
// ⚠️ Compiles but creates hidden circular dependency
class ServiceA : KoinComponent {
private val serviceB: ServiceB by inject()
fun doA() {
serviceB.doB() // Calls ServiceB
}
}
class ServiceB : KoinComponent {
private val serviceA: ServiceA by inject()
fun doB() {
serviceA.doA() // Calls ServiceA - INFINITE LOOP!
}
}
3. Scope Leaks
// ❌ Bad - Activity reference leaks through singleton
module {
single { SomeService(get<Activity>()) } // Activity will leak!
}
// ✅ Good - Use Application context or scope properly
module {
single { SomeService(androidContext()) } // Application context, safe
}
module {
activityScope {
scoped { SomeService(/* no activity reference */) }
}
}
Summary
Advanced patterns with Koin:
- Collections - Aggregate multiple implementations
- Lazy Injection - Defer creation with
by inject() - Optional Dependencies - Use
getOrNull()for optional deps - Injected Parameters - Pass runtime values with
parametersOf() - Conditional Binding - Different implementations based on conditions
- Provider Pattern - Factory of factories
- Delegation - Decorators and delegated interfaces
- Avoid Circular Dependencies - Use lazy injection or refactor
- Advanced Scoping - Hierarchical and linked scopes
- Generic Types - Fully supported with type-safe resolution
Next Steps
- Best Practices - Overall Koin best practices
- Multi-Module Apps - Organizing dependencies across modules
- Scopes - Deep dive into scoping
- Testing - Testing advanced patterns