docs/reference/koin-mp/kmp.md
This guide covers advanced patterns for Koin in Kotlin Multiplatform projects.
:::info For basic setup, see KMP Setup. For module organization, see Sharing Patterns. For ViewModel, see ViewModel. :::
:::info You can find the Kotlin Multiplatform project here: https://github.com/InsertKoinIO/hello-kmp :::
Beyond the basic expect val platformModule: Module pattern, here are advanced approaches for platform-specific code.
Use when you need platform-specific APIs (Android Context, iOS UIDevice, etc.):
// commonMain - Declaration
expect class PlatformContext
expect fun createPlatformModule(): Module
// androidMain - Android Implementation
actual class PlatformContext(val context: Context)
actual fun createPlatformModule() = module {
single<PlatformContext>() // Compiler Plugin DSL
}
// iosMain - iOS Implementation
actual class PlatformContext
actual fun createPlatformModule() = module {
single<PlatformContext>()
}
Use when you want to inject different implementations per platform:
// commonMain - Interface
interface Logger {
fun log(message: String)
}
// androidMain
class AndroidLogger : Logger {
override fun log(message: String) {
android.util.Log.d("App", message)
}
}
val androidModule = module {
single<AndroidLogger>() bind Logger::class
}
// iosMain
class IOSLogger : Logger {
override fun log(message: String) {
println("iOS: $message")
}
}
val iosModule = module {
single<IOSLogger>() bind Logger::class
}
Combine expect/actual with annotations for cleaner code:
// commonMain
expect val platformModule: Module
// androidMain
@Module
@ComponentScan("com.myapp.android")
class AndroidPlatformModule
actual val platformModule = AndroidPlatformModule().module
// iosMain
@Module
@ComponentScan("com.myapp.ios")
class IosPlatformModule
actual val platformModule = IosPlatformModule().module
:::info When to use which pattern:
A common need is accessing Android Context in shared code. Here's the recommended pattern:
// commonMain - Wrapper interface
interface AppContext
// androidMain - Android implementation
class AndroidAppContext(val context: Context) : AppContext
val androidContextModule = module {
single<AndroidAppContext>() bind AppContext::class
}
// iosMain - Empty implementation
class IOSAppContext : AppContext
val iosContextModule = module {
single<IOSAppContext>() bind AppContext::class
}
Usage in shared code:
// commonMain - Repository uses platform context
class FileRepository(private val appContext: AppContext) {
fun saveFile(data: String) {
when (appContext) {
is AndroidAppContext -> {
val file = File(appContext.context.filesDir, "data.txt")
file.writeText(data)
}
is IOSAppContext -> {
// iOS-specific file operations
}
}
}
}
val sharedModule = module {
single<FileRepository>()
}
:::note
For pure shared logic, prefer abstracting platform operations into interfaces rather than using when statements.
:::
// commonMain
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun saveUser(user: User)
}
@Singleton
class UserRepositoryImpl(
private val api: UserApi,
private val database: UserDatabase
) : UserRepository {
override suspend fun getUser(id: String): User {
return try {
api.fetchUser(id).also { database.saveUser(it) }
} catch (e: Exception) {
database.getUser(id)
}
}
override suspend fun saveUser(user: User) {
database.saveUser(user)
api.updateUser(user)
}
}
val dataModule = module {
single<UserRepositoryImpl>() bind UserRepository::class
}
// commonMain
@Singleton
class ApiClient(private val client: HttpClient) {
suspend fun fetchUser(id: String): User {
return client.get("https://api.example.com/users/$id").body()
}
}
val networkModule = module {
single {
HttpClient {
install(ContentNegotiation) {
json()
}
}
}
single<ApiClient>()
}
// commonMain
expect class DriverFactory {
fun createDriver(): SqlDriver
}
val databaseModule = module {
single { DriverFactory().createDriver() }
single { AppDatabase(get()) }
single { get<AppDatabase>().userQueries }
}
// androidMain
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
}
// iosMain
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(AppDatabase.Schema, "app.db")
}
}
// commonTest
class UserRepositoryTest : KoinTest {
@Test
fun testGetUser() = runTest {
startKoin {
modules(module {
single<UserApi> { FakeUserApi() }
single<UserDatabase> { FakeUserDatabase() }
single<UserRepositoryImpl>() bind UserRepository::class
})
}
val repository: UserRepository = get()
val user = repository.getUser("123")
assertEquals("John", user.name)
stopKoin()
}
}
// commonTest
expect fun createTestPlatformModule(): Module
// androidTest
actual fun createTestPlatformModule() = module {
single<PlatformContext> { TestAndroidContext() }
}
// iosTest
actual fun createTestPlatformModule() = module {
single<PlatformContext> { TestIOSContext() }
}
// commonTest - Test using platform module
class PlatformDependentTest : KoinTest {
@Test
fun testWithPlatformContext() {
startKoin {
modules(
createTestPlatformModule(),
module {
single<MyService>()
}
)
}
val service: MyService = get()
// Test service
stopKoin()
}
}
// Good - Testable
interface Logger {
fun log(message: String)
}
val sharedModule = module {
single<UserService>() // Depends on Logger interface
}
// Bad - Hard to test, tight platform coupling
expect class Logger {
fun log(message: String)
}
// Good - Clear separation
fun initKoin() {
startKoin {
modules(commonModules() + platformModule)
}
}
// Bad - Platform-specific code in commonMain
val sharedModule = module {
single {
if (Platform.isAndroid) { /* ... */ } // Don't do this!
}
}
// Good - Optimize startup
val lazyFeatureModule = lazyModule {
single<HeavyService>()
}
startKoin {
modules(coreModules)
lazyModules(lazyFeatureModule)
}
// Bad - Memory leak
class FeatureScreen : KoinComponent {
val scope = getKoin().createScope<FeatureScreen>()
// Forgot to close scope!
}
// Good - Proper cleanup
class FeatureScreen : KoinComponent {
val scope = getKoin().createScope<FeatureScreen>()
fun onDestroy() {
scope.close()
}
}
For JVM Desktop apps (Compose Desktop):
// desktopMain
fun main() = application {
startKoin {
modules(
sharedModule,
desktopModule
)
}
Window(onCloseRequest = ::exitApplication) {
App()
}
}
val desktopModule = module {
single<DesktopLogger>() bind Logger::class
single<DesktopFileManager>()
}
For Kotlin/JS and Kotlin/WASM:
// jsMain or wasmJsMain
fun main() {
startKoin {
modules(
sharedModule,
webModule
)
}
// Your web app initialization
}
val webModule = module {
single<ConsoleLogger>() bind Logger::class
single<BrowserStorage>()
}
:::warning WASM support is experimental. Some features may not work as expected. :::
// shared/src/iosMain/kotlin/Helper.kt
class GreetingHelper : KoinComponent {
private val greeting: Greeting by inject()
fun greet(): String = greeting.greeting()
}
In Swift:
struct ContentView: View {
let greet = GreetingHelper().greet()
var body: some View {
Text(greet)
}
}
On iOS and other Native targets, Koin instances work seamlessly with the new memory model:
@SharedImmutable for global Koin instances if needed:::note The new Kotlin/Native memory model (default in Kotlin 1.7.20+) makes Koin usage much simpler. :::