Kotlin Multiplatform (KMP) simplifies implementing shared features in cross-platform projects, allowing you to set up platform-specific dependencies without having to completely overhaul your codebase. Its flexibility and versatility make KMP an exceptional and thoughtfully designed development framework.
In the first article in this series, you put all the foundational elements in place to start using Kotlin Multiplatform. Now you’re ready to implement platform-specific dependencies from their respective modules. A practical example to illustrate this is creating a simple network listener.
To help guide you, I have created a video to showcase the result of your work.
Start Using Kotlin Multiplatform
If you’ve developed apps before, you’ve probably encountered scenarios where you needed to inform users about their internet connectivity, be it an offline status or a disabled connection. Android developers typically employ the ConnectivityManager
, while iOS developers might use NWPathMonitor
.
Accessing ConnectivityManager
in the Android module is straightforward with the androidContext()
function provided by the Koin dependency injection (DI) framework for KMP. However, in December 2023, I observed that NWPathMonitor
isn’t included in the prebuilt libraries for Kotlin Native, so I used an interface to integrate it using Swift code. This approach involves leveraging the IosApplicationComponent
to bridge the NWPathMonitor
class from the iosApp
module to the composeApp
module shared by Android and iOS.
First I’ll conceptualize the interface. At its core, the network listener must perform two fundamental actions: initiating the listening process and ceasing it when necessary. To do so, define two functions: registerListener
to start the listening and unregisterListener
to stop it; then encapsulate these functionalities within an interface named NetworkHelper
. This interface is also designed to notify you about network status changes, providing callbacks to indicate when a network connection is established or lost.
// NetworkHelper.kt (In commonMain)
package org.example.project.platform
interface NetworkHelper {
fun registerListener(onNetworkAvailable: () -> Unit, onNetworkLost: () -> Unit)
fun unregisterListener()
}
Implementing this interface on Android is pretty straightforward. Go to the exact location in your androidMain
module and create a new class called AndroidNetworkHelper,
which implements the NetworkHelper
interface. This is a pretty standard implementation.
// AndroidNetworkHelper.kt
package org.example.project.platform
import android.content.Context
import android.content.Context.CONNECTIVITY_SERVICE
import android.net.ConnectivityManager
import android.net.Network
class AndroidNetworkHelper(context: Context) : NetworkHelper {
private var networkCallback: ConnectivityManager.NetworkCallback? = null
private val connectivityManager = context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
override fun registerListener(onNetworkAvailable: () -> Unit, onNetworkLost: () -> Unit) {
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
onNetworkAvailable()
}
override fun onUnavailable() {
onNetworkLost()
}
override fun onLost(network: Network) {
onNetworkLost()
}
}
networkCallback?.let { connectivityManager.registerDefaultNetworkCallback(it) }
}
override fun unregisterListener() {
networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
}
}
You still want to make this class available in the dependency injection (DI) graph described in the previous article.
// PlatformModule.android.kt
actual val platformModule = module {
single<NetworkHelper> { AndroidNetworkHelper(androidContext()) }
}
Now for the fun part: In the iosApp
module, go into the iosApp
folder and create a new Swift class file called IosNetworkHelper
. You should do this using JetBrains Fleet or Apple’s XCode code editor:
// IosNetworkHelper.swift
import Foundation
import Network
import composeApp
class IosNetworkHelper : NetworkHelper {
private let monitor: NWPathMonitor = NWPathMonitor()
func registerListener(onNetworkAvailable: @escaping () -> Void, onNetworkLost: @escaping () -> Void) {
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
onNetworkAvailable()
} else {
onNetworkLost()
}
}
monitor.start(queue: DispatchQueue.global(qos: .background))
}
func unregisterListener() {
monitor.cancel()
}
}
Notice that the Unit
return type is now represented as a Void
type. That’s due to the Kotlin Native Obj-C interoperability mappings.
Now that you have an iOS implementation leveraging the Network
package native to the iOS ecosystem, you can integrate it into the IosApplicationComponent
. To do this effectively, first define it within the component:
// IosApplicationComponent.kt (In iosMain)
package org.example.project.platform
class IosApplicationComponent(val networkHelper: NetworkHelper)
// We've removed the old 'application()' function
// iOSApp.swift
import SwiftUI
import composeApp
@main
struct iOSApp: App {
init() {
KoinInit_iosKt.doInitKoinIos(
appComponent: IosApplicationComponent(
networkHelper: IosNetworkHelper() // Note this
)
)
}
var body: some Scene {...}
}
Lastly, to piece the puzzle together, make it available to Koin:
// PlatformModule.ios.kt
actual val platformModule = module {
single<NetworkHelper> { get<IosApplicationComponent>().networkHelper }
}
Complete the Implementations
You now have everything you need to complete your NetworkListener
implementation. Return to the commonMain
source set and create a class called NetworkListener
:
// NetworkListener.kt
class NetworkListener(private val helper: NetworkHelper) {
val networkStatus: Flow<NetworkStatus> = callbackFlow {
helper.registerListener(
onNetworkAvailable = {
trySend(NetworkStatus.Connected)
},
onNetworkLost = {
trySend(NetworkStatus.Disconnected)
}
)
awaitClose {
helper.unregisterListener()
}
}.distinctUntilChanged().flowOn(Dispatchers.IO)
}
sealed class NetworkStatus {
data object Connected : NetworkStatus()
data object Disconnected : NetworkStatus()
}
This class utilizes Kotlin’s flows to tap into the capabilities of the NetworkHelper
methods you established previously. This NetworkListener
class can be seamlessly injected into the DI graph within the commonMain
source set, further streamlining its integration and use.
// CommonModule.kt (In commonMain source set)
package org.example.project.di
import org.example.project.platform.NetworkListener
import org.koin.dsl.module
val commonModule = module {
single { NetworkListener(get()) }
}
To wrap it all up, go to the App.kt
composable and test the network connectivity:
// App.kt
@Composable
fun App(
networkListener: NetworkListener = koinInject(),
) {
KoinContext {
MaterialTheme {
val networkStatus by networkListener.networkStatus.collectAsState(NetworkStatus.Connected)
Text(text = networkStatus.toString())
}
}
}
Conclusion
This two-part article provided a step-by-step guide to calling platform-specific dependencies in Kotlin Multiplatform. While I’ve applied some creative approaches, the majority of the patterns discussed are derived from various sources, including blogs, official Kotlin and JetBrains articles, documentation and KMP samples, including:
- The Basics of Kotlin Multiplatform Project Structure
- Kotlin Multiplatform Samples
- Using Platform-Specific APIs
- Expected and Actual Declarations
- Interoperability with Swift/Objective-C
The shift toward remote and borderless hiring enables companies to look beyond geographical constraints to access global talent. Explore strategies to build high-performing, globally distributed teams by downloading The Future of Hiring Is Borderless.
About the author
Mofe Ejegi
Mofe Ejegi is a software engineer with a primary focus on mobile development. He is based in Delta state, Nigeria, and is a member of the Andela Talent Network, a private global marketplace for digital talent. He currently works as a Senior Android engineer at Banque Misr where he demonstrates his strong background in FinTech. On the side, he actively participates in the official Kotlin community on Slack, mostly researching the latest developments in the Kotlin and Android ecosystem, particularly on Kotlin Multiplatform.