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
OrdersApifor tests, a differentHttpClientconfig 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.