Achieving Platform-Specific Implementations with Koin in KMM

Mirzamehdi Karimov
ProAndroidDev
Published in
3 min readMay 22, 2023

--

During the migration process of the Quotes project to KMM (Kotlin Mobile Multiplatform), I had to implement different classes for each platform, and I was using Koin for dependency injection.

One of such classes was NetworkHandler, which had a single method called hasNetworkConnection() that was checking network connectivity, and I needed to implement this class on both Android and iOS platforms. The way of doing this in multiplatform development, we declare something as expected and then provide the actual implementation for each platform( https://kotlinlang.org/docs/multiplatform-connect-to-apis.html).

I had two initial ideas for implementing this, and I’ll go through each of them.

Naive approach — NetworkHandler class as expected class:
The idea was to declare the NetworkHandler class as an expected class, and then provide the actual implementation for each platform. Something like this:

//commonMain module
expect class NetworkHandler(){
fun hasNetworkConnection():Boolean
}

------------------------------------

//iosMain or androidMain module
actual class NetworkHandler actual constructor() {
actual fun hasNetworkConnection():Boolean {
//implementation specific to each module
}
}

When I look at the NetworkHandler class implementation, I noticed that it depended on the Android application context. And in the iOS environment I didn’t even have a clue what dependencies it will need, so, this approach didn’t work. Hence, I had to consider an alternative solution.

Main approach — Creating NetworkHandler interface and providing an implementation in each platform:

//commonMain module
interface NetworkHandler {
fun hasNetworkConnection():Boolean
}

--------------------------------------

//androidMain module
class AndroidNetworkHandler(context:Context):NetworkHandler{
override fun hasNetworkConnection(): Boolean {
//implementation in android world
}
}

----------------------------------------

//iosMain module
class IosNetworkHandler():NetworkHandler{
override fun hasNetworkConnection(): Boolean {
// I still don't know how is implemenation in ios
// so I just return true here :D
return true
}
}

So far so good, but there was still one issue. I was using Koin as DI, and in core module I needed to provide the actual implementation of NetworkHandler class. Before the migration to a multiplatform, it was as follows:

val coreModule = module {
single<NetworkHandler> { NetworkHandler(androidContext()) }
}

Now in my multiplatform core module I needed to say to koin somehow “Hey btw Koin, here is my actual implementation of NetworkHandler for each platform” and this is when I discovered the powerful capability of Koin using the “include” and “constructor DSL” feature. The idea is to divide the Koin module into two parts: a common module and a platform-specific module. The platform-specific module is declared as an expected variable, and the actual implementation is provided in each platform. By using the “include” feature, we combine these modules into one single module.

//commonMain module
private val commonCoreModule = module {
//Add common core module implementations here
}

//https://github.com/InsertKoinIO/koin/issues/1341
// writing get() is important
val coreModule : Module get() = module {
includes(commonCoreModule + platformCoreModule)
}

internal expect val platformCoreModule:Module

-------------------------------

//androidMain module
internal actual val platformCoreModule: Module = module {
singleOf(::AndroidNetworkHandler) bind NetworkHandler::class
}

------------------------------------

//iosMain module
internal actual val platformCoreModule: Module = module {
singleOf(::IosNetworkHandler) bind NetworkHandler::class
}

By following this approach, I was able to successfully provide different implementations for each platform using Koin. This allowed me to customize the behavior of the NetworkHandler class based on the specific requirements of each platform.

--

--