Files
vnidrop/.codex/skills/compose-skill/references/koin.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

9.2 KiB

Dependency Injection with Koin

Multiplatform DI for Compose projects with ViewModel, Compose, and Navigation 3 integration.

For Hilt vs Koin decision guidance and shared DI concepts, see dependency-injection.md. For Hilt (Android-only), see hilt.md.

References:

Package Selection

commonMain.dependencies {
    implementation(platform("io.insert-koin:koin-bom:$koin_version"))
    implementation("io.insert-koin:koin-core")
    implementation("io.insert-koin:koin-compose")
    implementation("io.insert-koin:koin-compose-viewmodel")
    implementation("io.insert-koin:koin-compose-viewmodel-navigation")  // Nav 3
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization_version")
}

Android-only projects

dependencies {
    implementation("io.insert-koin:koin-androidx-compose:$koin_version")  // includes compose + viewmodel
    implementation("io.insert-koin:koin-compose-viewmodel-navigation:$koin_version")
}
Package Purpose
koin-core Core DI engine (multiplatform)
koin-compose Base Compose API (koinInject)
koin-compose-viewmodel ViewModel injection (koinViewModel)
koin-compose-viewmodel-navigation Nav 3 entry provider integration
koin-androidx-compose Android convenience (includes compose + viewmodel)

Platform support: Android, iOS, Desktop — full. Web — experimental.

Setup and Starting Koin

Initialize outside Compose with a shared initKoin and platform-specific config lambda:

// commonMain
fun initKoin(config: KoinAppDeclaration? = null) {
    startKoin {
        config?.invoke(this)
        modules(appModule, featureModules)
    }
}

// Android — Application class
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        initKoin { androidContext(this@MyApplication); androidLogger() }
    }
}

iOS — call from Swift. do prefix added because init is reserved:

import ComposeApp
@main struct iOSApp: App {
    init() { InitKoinKt.doInitKoin(config: nil) }
    var body: some Scene { WindowGroup { ContentView() } }
}

Alternative — Compose-managed: KoinApplication(configuration = koinConfiguration { modules(appModule) }) { MainScreen() }

Defining Modules

val appModule = module {
    // Classic DSL (manual wiring)
    single<UserRepository> { UserRepositoryImpl() }
    factory { ProductValidator() }
    viewModelOf(::ProductViewModel)

    // Compiler Plugin DSL (auto-wiring — requires Koin Compiler Plugin)
    single<ProductCalculator>()                                   // auto-resolves constructor params
    single<UserRepositoryImpl>() bind UserRepository::class       // bind exposes impl as interface
    viewModel<ProductViewModel>()
}
DSL Lifecycle When to use
single { } App lifetime (singleton) Stateless services, repositories, API clients, databases
factory { } New instance per call Stateful/short-lived — validators, formatters, use-cases with request state
scoped { } Bound to a Koin scope Shared within a flow (e.g., checkout) but not globally
viewModelOf(::Class) ViewModel lifecycle Survives recomposition + config changes, cleared when owner destroyed

Annotations (KSP)

Compile-time safety with multiplatform support. Requires KSP plugin + koin-annotations.

plugins { id("com.google.devtools.ksp") }

kotlin {
    sourceSets.commonMain.dependencies {
        implementation("io.insert-koin:koin-annotations:$koin_annotations_version")
    }
    sourceSets.named("commonMain").configure {
        kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
    }
}

dependencies {
    add("kspCommonMainMetadata", "io.insert-koin:koin-ksp-compiler:$koin_annotations_version")
    add("kspAndroid", "io.insert-koin:koin-ksp-compiler:$koin_annotations_version")
    // ... add for each target (kspIosArm64, kspIosSimulatorArm64, etc.)
}

ksp {
    arg("KOIN_USE_COMPOSE_VIEWMODEL", "true")   // multiplatform ViewModel DSL
    arg("KOIN_CONFIG_CHECK", "true")            // compile-time verification
}
Annotation Equivalent DSL Purpose
@Single single { } Singleton
@Factory factory { } New instance each time
@KoinViewModel viewModelOf(::Class) ViewModel declaration
@InjectedParam parametersOf(...) Runtime parameter
@Module + @ComponentScan module { } Auto-discover annotated classes in package

Use generated .module property: modules(AppModule().module).

Feature-first module organization

val productModule = module {
    single<ProductRepository> { ProductRepositoryImpl(get()) }
    viewModelOf(::ProductViewModel)
}
val appModule = module { includes(productModule, settingsModule, coreModule) }

Platform-specific implementations

Use expect/actual modules when implementations differ per platform:

// commonMain
expect val platformModule: Module

// androidMain
actual val platformModule = module { single<HapticFeedback> { AndroidHapticFeedback(get()) } }

// iosMain
actual val platformModule = module { single<HapticFeedback> { IosHapticFeedback() } }

startKoin { modules(appModule, platformModule) }

For platform dependencies (e.g., Android Context) in expect/actual classes, use KoinComponent with inject() — justified because constructors must match across platforms. Avoid KoinComponent elsewhere.

Injection in Compose

// Any dependency
val service: MyService = koinInject()

// ViewModel — lifecycle-aware
val viewModel = koinViewModel<HomeViewModel>()

// With runtime parameters
val viewModel = koinViewModel<DetailViewModel> { parametersOf(itemId) }

// Keyed — unique instance per entity
val viewModel = koinViewModel<DetailViewModel>(key = "detail_$itemId", parameters = { parametersOf(itemId) })

Inject as default parameters for testability: fun MyScreen(service: MyService = koinInject()).

Function Platform When to use
koinInject<T>() All Non-ViewModel dependencies inside @Composable
koinViewModel<T>() All ViewModel — lifecycle-aware, survives recomposition
koinActivityViewModel<T>() Android Share ViewModel across all composables in an Activity
koinEntryProvider<T>() All Wire Nav 3 NavDisplay to Koin navigation<T> entries
parametersOf(...) All Pass runtime values to koinViewModel or koinInject
get<T>() All Resolve inside module { } only — never in composables

Navigation 3 Integration

Two approaches for Nav 3 + DI. For full patterns, entry-scoped ViewModels, and modularization, see navigation-3-di.md.

// Koin DSL — entries declared in modules
val appModule = module {
    navigation<HomeRoute> { HomeScreen(viewModel = koinViewModel()) }
    navigation<DetailRoute> { route -> DetailScreen(viewModel = koinViewModel { parametersOf(route.id) }) }
}
NavDisplay(backStack = backStack, onBack = { backStack.removeLastOrNull() }, entryProvider = koinEntryProvider())

For Nav 2 patterns, see navigation-2-di.md. For migration, see navigation-migration.md.

Scopes

val appModule = module {
    scope<CheckoutFlow> {
        scoped { CheckoutState() }
        viewModel<CheckoutViewModel>()
    }
}

scope<T> works on all platforms. On Android, activityRetainedScope { } survives config changes (same idea, platform-specific).

Koin in MVI

MVI is framework-agnostic — see architecture.md. The Koin-specific parts are constructor injection and koinViewModel():

class ProductViewModel(private val repository: ProductRepository) : ViewModel() {
    // StateFlow<State>, Channel<Effect>, onEvent() — see architecture.md
}
// Module: viewModelOf(::ProductViewModel)
// Route:  val viewModel = koinViewModel<ProductViewModel>()

Testing

verify() performs a dry-run check — catches missing declarations before runtime:

class KoinModuleCheck : KoinTest {
    @Test
    fun verifyAllModules() {
        appModule.verify(extraTypes = listOf(SavedStateHandle::class))
    }
}
// commonTest.dependencies { implementation("io.insert-koin:koin-test:$koin_version") }

For ViewModel event→state→effect testing, see testing.md.

Anti-Patterns

Anti-pattern Why it is harmful Better approach
factory { MyViewModel() } for ViewModels Not lifecycle-aware, new instance on recomposition viewModelOf(::MyViewModel)
Not using parametersOf for runtime params Constructor params unresolved koinViewModel { parametersOf(id) }
koin-compose without koin-compose-viewmodel koinViewModel() unavailable Add koin-compose-viewmodel
Calling startKoin multiple times KoinAppAlreadyStartedException Call once, use loadKoinModules for dynamic additions
Android Context in commonMain modules Breaks multiplatform expect/actual platform modules