Skip to main content
Version: 4.2

KMP Advanced Patterns

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.

Source Project

info

You can find the Kotlin Multiplatform project here: https://github.com/InsertKoinIO/hello-kmp

Advanced expect/actual Patterns

Beyond the basic expect val platformModule: Module pattern, here are advanced approaches for platform-specific code.

Pattern 1: expect/actual Classes

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>()
}

Pattern 2: Interface + Platform Implementations

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
}

Pattern 3: expect Module with Annotations

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:

  • expect/actual classes: Platform APIs (Context, UIDevice), simple platform differences
  • Interfaces: Business logic that varies by platform, testable code
  • expect modules: Complex platform-specific dependency graphs

Android Context in Shared Code

A common need is accessing Android Context in shared code. Here's the recommended pattern:

ContextWrapper 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.

Architecture Patterns

Repository Pattern with Ktor

// 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
}

Network Layer (Ktor + Koin)

// 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>()
}

Database Layer (SqlDelight)

// 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")
}
}

Testing KMP Modules

Unit Testing Shared Modules

// 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()
}
}

Testing with Platform-Specific Dependencies

// 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()
}
}

Common Pitfalls

DO: Use interfaces for testable shared code

// Good - Testable
interface Logger {
fun log(message: String)
}

val sharedModule = module {
single<UserService>() // Depends on Logger interface
}

DON'T: Use expect classes for business logic

// Bad - Hard to test, tight platform coupling
expect class Logger {
fun log(message: String)
}

DO: Keep platform modules separate

// Good - Clear separation
fun initKoin() {
startKoin {
modules(commonModules() + platformModule)
}
}

DON'T: Mix platform-specific code in shared modules

// Bad - Platform-specific code in commonMain
val sharedModule = module {
single {
if (Platform.isAndroid) { /* ... */ } // Don't do this!
}
}

DO: Use lazy modules for large apps

// Good - Optimize startup
val lazyFeatureModule = lazyModule {
single<HeavyService>()
}

startKoin {
modules(coreModules)
lazyModules(lazyFeatureModule)
}

DON'T: Forget to close scopes

// 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()
}
}

Desktop Platform Integration

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>()
}

Web Platform Integration (Experimental)

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.

iOS Swift Interop

KoinComponent for Swift

// 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)
}
}

Threading Considerations

On iOS and other Native targets, Koin instances work seamlessly with the new memory model:

  • Koin definitions are thread-safe
  • Scopes can be created and used across threads
  • Use @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.

Next Steps