Introduction

Koin's dependency injection uses different scopes (single, factory, scoped) to control instance lifecycle. When a single is defined in a module that is loaded multiple times, or when scoped is used without proper scope management, multiple instances of what should be a singleton are created. This causes state inconsistency, duplicated resources, and unexpected behavior in repository and service layers.

Symptoms

  • Repository has different state when accessed from different ViewModels
  • Database instance created multiple times causing connection conflicts
  • User session data lost because different instances are used
  • single definition creating new instance after module reload
  • Memory leak from multiple singleton instances holding resources

Debug Koin instances: ``kotlin // Check if instances are the same val repo1 = koin.get<MyRepository>() val repo2 = koin.get<MyRepository>() println("Same instance: ${repo1 === repo2}") // Should be true for single

Common Causes

  • Module loaded multiple times with loadKoinModules
  • factory used instead of single for stateful dependencies
  • Koin scope not properly created or closed
  • viewModel definition creating new ViewModel instead of using ViewModelStore
  • Module redefinition overwriting previous definitions

Step-by-Step Fix

  1. 1.**Use single for application-wide shared instances":
  2. 2.```kotlin
  3. 3.val appModule = module {
  4. 4.// CORRECT - single instance shared across entire app
  5. 5.single { MyDatabase(get()) }
  6. 6.single { MyRepository(get()) }
  7. 7.single { SharedPreferencesManager(get()) }

// WRONG - factory creates new instance every time // factory { MyRepository(get()) }

// ViewModel - Koin manages lifecycle with ViewModelStore viewModel { MyViewModel(get()) } }

// Load module ONCE in Application class class MyApp : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MyApp) modules(appModule) // Loaded once at startup } } } ```

  1. 1.**Fix scoped dependency lifecycle":
  2. 2.```kotlin
  3. 3.// WRONG - scoped without proper scope management
  4. 4.val userModule = module {
  5. 5.scoped { UserProfileService(get()) } // New instance each time scope is created
  6. 6.}

// CORRECT - create and manage scope explicitly class UserScope(private val koin: Koin) { private val scope = koin.createScope("user-session", named<UserScope>())

fun getUserProfileService(): UserProfileService { return scope.get() }

fun close() { scope.close() // Cleans up scoped instances } }

// Usage val userScope = UserScope(getKoin()) val service = userScope.getUserProfileService()

// When user logs out userScope.close() ```

  1. 1.**Debug Koin module loading":
  2. 2.```kotlin
  3. 3.// Add Koin logger to detect module loading issues
  4. 4.startKoin {
  5. 5.androidContext(this@MyApp)
  6. 6.// Enable logging
  7. 7.androidLogger(if (BuildConfig.DEBUG) Level.DEBUG else Level.NONE)
  8. 8.modules(appModule, networkModule, databaseModule)
  9. 9.}

// Check what modules are loaded fun printLoadedModules() { val koin = getKoin() koin.instanceRegistry.instances.forEach { (key, value) -> println("Instance: $key -> ${value::class.simpleName}") } }

// Verify singleton behavior @Test fun testSingletonInstance() { val app = MyApplication() app.onCreate()

val repo1 = getKoin().get<MyRepository>() val repo2 = getKoin().get<MyRepository>()

assertSame(repo1, repo2, "Repository should be a singleton") } ```

  1. 1.**Prevent module reload creating duplicate instances":
  2. 2.```kotlin
  3. 3.// WRONG - reloading module creates duplicate definitions
  4. 4.fun reloadFeature() {
  5. 5.loadKoinModules(featureModule) // First load
  6. 6.// ... later
  7. 7.loadKoinModules(featureModule) // Second load - duplicates!
  8. 8.}

// CORRECT - check if module is loaded before loading fun loadFeatureIfNotLoaded() { val koin = getKoin() val isLoaded = koin.instanceRegistry.instances .any { it.value is FeatureService }

if (!isLoaded) { loadKoinModules(featureModule) } } ```

Prevention

  • Define application-wide dependencies as single in modules loaded once
  • Use viewModel for ViewModel-scoped dependencies (Koin manages lifecycle)
  • Use scoped only with explicit scope creation and cleanup
  • Enable Koin logging in debug builds to detect duplicate loading
  • Write unit tests that verify singleton instances are shared
  • Document which scope each dependency should use in module definitions