← All shorts
50Koin 3.5+2 min read

Dependency Injection in Shared Code with Koin

Koin builds one dependency graph in commonMain, started from each platform's entry point without reflection.

Sharing a networking client and a repository across platforms doesn't help much if each platform still wires them together by hand. Koin builds its dependency graph with a plain Kotlin DSL, no annotation processing, which makes it one of the few DI frameworks that works the same way in commonMain as it does on the JVM.

#The old way

Android wired OrdersApi and OrderRepository through Hilt modules and @Inject constructors; iOS constructed the same objects by hand in AppDelegate, or through a separate service-locator singleton. Adding a constructor parameter to OrderRepository meant updating a Hilt module on one side and a manual initializer on the other.

#The new way

val networkModule = module {
    single {
        HttpClient {
            install(ContentNegotiation) { json() }
        }
    }
}

val dataModule = module {
    single { OrdersApi(get()) }
    single { OrderRepository(get()) }
}

class OrderViewModel(private val repository: OrderRepository) {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    private val _orders = MutableStateFlow<List<Order>>(emptyList())
    val orders: StateFlow<List<Order>> = _orders.asStateFlow()

    fun loadOrders() {
        scope.launch {
            _orders.value = repository.fetchOrders()
        }
    }
}

val viewModelModule = module {
    factory { OrderViewModel(get()) }
}

fun initKoin() {
    startKoin {
        modules(networkModule, dataModule, viewModelModule)
    }
}

Android calls initKoin() from Application.onCreate; iOS calls the same initKoin() from its App entry point via the generated framework. Both platforms then pull OrderViewModel out of the same graph with getKoin().get<OrderViewModel>().

#Why it matters

  • One graph definition instead of two DI configurations that have to be kept in sync by hand
  • No code generation step — modules are plain Kotlin, so IDE navigation and refactoring work normally
  • Swapping an implementation (a fake OrdersApi for tests, a different HttpClient config for debug builds) is a one-line module change

#Gotcha

Calling getKoin().get<T>() before startKoin { } has run throws at that call site, not at compile time. Android's Application.onCreate runs reliably before any screen renders, but iOS app launch order is less centralized — a Compose Multiplatform or SwiftUI screen that resolves a dependency during its own init, before the host app has called initKoin(), crashes on first launch with a startup-order bug that a JVM-only test suite will never catch.

kmpkotlincross-platform

Related shorts