KMP Platform Integration
This page covers the platform details of a Kotlin Multiplatform app with Koin: using the Android Context from shared code, calling Koin from Swift, and the desktop and web targets. It assumes your project follows KMP Setup and KMP Sharing Patterns.
Complete projects: the Compose Multiplatform sample (Compiler Plugin DSL) and the annotations sample.
Android Context in shared code
Shared code can't use android.content.Context, but some shared classes need it on Android (to open files, read preferences). Wrap it in a platform type that common code can depend on:
- Compiler Plugin DSL
- Annotations
- Classic DSL
// commonMain
interface ContextWrapper {
fun getContextInfo(): String
}
class FileStore(private val ctx: ContextWrapper)
val storageModule = module {
single<FileStore>()
}
// androidMain
class AndroidContextWrapper(val context: Context) : ContextWrapper {
override fun getContextInfo() = "Android: ${context.packageName}"
}
actual fun platformModule(): Module = module {
single<AndroidContextWrapper>() bind ContextWrapper::class
}
// iosMain
class IOSContextWrapper : ContextWrapper {
override fun getContextInfo() = "iOS"
}
actual fun platformModule(): Module = module {
single<IOSContextWrapper>() bind ContextWrapper::class
}
// commonMain
interface ContextWrapper {
fun getContextInfo(): String
}
@Singleton
class FileStore(private val ctx: ContextWrapper)
// androidMain
@Singleton
class AndroidContextWrapper(val context: Context) : ContextWrapper {
override fun getContextInfo() = "Android: ${context.packageName}"
}
// iosMain
@Singleton
class IOSContextWrapper : ContextWrapper {
override fun getContextInfo() = "iOS"
}
The platform classes are found by the actual platform module with @ComponentScan (see KMP Sharing Patterns).
// commonMain
interface ContextWrapper {
fun getContextInfo(): String
}
class FileStore(private val ctx: ContextWrapper)
val storageModule = module {
singleOf(::FileStore)
}
// androidMain
actual fun platformModule(): Module = module {
singleOf(::AndroidContextWrapper) bind ContextWrapper::class
}
// iosMain
actual fun platformModule(): Module = module {
singleOf(::IOSContextWrapper) bind ContextWrapper::class
}
FileStore is compiled once in commonMain and receives the platform wrapper. Only one Android definition touches Context, and it resolves once androidContext() is set at start. Put the Android-specific operations behind methods of ContextWrapper rather than checking the platform type in common code.
iOS and Swift
Retrieving dependencies from Swift
Swift can't call Koin's reified functions such as get<T>(). At the Swift boundary, write a small Kotlin helper in iosMain that implements KoinComponent, and use it from Swift. This is an interop entry point: your shared classes keep receiving their dependencies through their constructors.
// iosMain
class GreetingHelper : KoinComponent {
private val greeting: Greeting by inject()
fun greet(): String = greeting.greeting()
}
struct ContentView: View {
let greet = GreetingHelper().greet()
var body: some View {
Text(greet)
}
}
Koin must be started before the helper is used: see the iOS entry point in KMP Setup. KoinComponent is described in KoinComponent.
Declaring a Swift instance in Koin
Sometimes a Swift class implements a Kotlin interface, and Kotlin code needs it from Koin. Register the instance at runtime with Koin.declare(). Swift passes the type as an Objective-C class or protocol, which this iosMain helper converts to a Kotlin KClass:
// iosMain
@OptIn(BetaInteropApi::class)
fun Koin.declareFromSwift(
instance: Any,
bindTo: ObjCObject,
qualifier: Qualifier? = null,
allowOverride: Boolean = true
) {
val kClass: KClass<*> = when (bindTo) {
is ObjCClass -> getOriginalKotlinClass(bindTo)
is ObjCProtocol -> getOriginalKotlinClass(bindTo)
else -> null
} ?: error("Can't resolve Kotlin KClass from $bindTo")
declare(
instance = instance,
qualifier = qualifier,
secondaryTypes = listOf(kClass),
allowOverride = allowOverride
)
}
koin.declareFromSwift(
instance: SwiftAnalytics(),
bindTo: Analytics.self,
qualifier: nil,
allowOverride: true
)
getOriginalKotlinClass() comes from kotlin.native and maps a class (.self on a class) or a protocol (.self on a protocol) back to its Kotlin type. Declared instances are not checked at compile time: a Kotlin consumer fails at runtime if Swift has not declared the instance yet. The helper is based on a proposal by @SarahDelCastillo.
Threading
Koin runs on the current Kotlin/Native memory model (the default since Kotlin 1.7.20). You can resolve dependencies and use scopes from any thread, with no freezing or @SharedImmutable annotation.
Desktop (JVM)
Start Koin in main(), before the first window:
// jvmMain
fun main() = application {
initKoin {
printLogger()
modules(desktopModule)
}
Window(onCloseRequest = ::exitApplication) {
App()
}
}
val desktopModule = module {
single<DesktopFileManager>()
}
The config parameter of the shared initKoin adds desktop-only modules and the logger. Desktop is a JVM target, so JVM-only features such as fileProperties() are available (see Starting Koin).
Web (JS and Wasm)
Start Koin in the main() of the js or wasmJs source set:
// jsMain or wasmJsMain
fun main() {
initKoin {
modules(webModule)
}
// start your UI
}
val webModule = module {
single<BrowserStorage>()
}
With Compose Multiplatform for web, you can start Koin from the root Composable with KoinApplication instead (see Koin for Compose).
Related
- KMP Setup: dependencies,
initKoin, entry points. - KMP Sharing Patterns: platform modules and
expect/actualdefinitions. - KoinComponent: retrieve dependencies from classes Koin does not create.
- Koin for Compose: Compose Multiplatform integration.
- Testing: test shared modules in
commonTest.