DEV Community

Cover image for A Tiny KMP Connectivity Monitor (Android + iOS) — No Pods Required
Qandil Tariq
Qandil Tariq

Posted on • Originally published at Medium

A Tiny KMP Connectivity Monitor (Android + iOS) — No Pods Required

I open-sourced a minimal Kotlin Multiplatform connectivity monitor that exposes a single

StateFlow<ConnectivityStatus> from shared code.

Android: ConnectivityManager callbacks • iOS: SCNetworkReachability (no CocoaPods).


Why I built this

Most apps need to react to connectivity. I wanted a production-style, zero-friction approach for Compose Multiplatform that keeps:

  • one API in commonMain
  • native implementations per platform
  • no CocoaPods requirement on iOS
  • easy to use from Compose

So I built KMP Connectivity Monitor.


The tiny API (commonMain)

// com.qandil.kmpconnectivity.connectivity.Connectivity.kt
package com.qandil.kmpconnectivity.connectivity

import kotlinx.coroutines.flow.StateFlow

enum class ConnectivityStatus { Online, Offline, Unavailable }

interface ConnectivityMonitor {
    val status: StateFlow<ConnectivityStatus>
    fun start()
    fun stop()
}

expect class ConnectivityMonitorFactory {
    fun create(): ConnectivityMonitor
}
Enter fullscreen mode Exit fullscreen mode

Android actual — ConnectivityManager

Listen to ConnectivityManager callbacks and update a MutableStateFlow.

// shared/src/androidMain/.../connectivity/Connectivity.android.kt (excerpt)
private fun updateNow() {
    val caps = cm.getNetworkCapabilities(cm.activeNetwork)
    val online = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true &&
                 caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
    scope.launch {
        _status.value = if (online) ConnectivityStatus.Online else ConnectivityStatus.Offline
    }
}

override fun start() {
    updateNow()
    val req = NetworkRequest.Builder()
        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        .build()
    cm.registerNetworkCallback(req, callback)
}
Enter fullscreen mode Exit fullscreen mode

Android manifest (app module):

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Enter fullscreen mode Exit fullscreen mode

Tip (for library lint): shared/src/androidMain/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
iOS actual  SystemConfiguration (no Pods)
Enter fullscreen mode Exit fullscreen mode

Use SCNetworkReachability to monitor the default route and update the flow.

// shared/src/iosMain/.../connectivity/Connectivity.ios.kt (excerpt)
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)

private fun update(flags: SCNetworkReachabilityFlags) {
    val reachable = flags.toInt() and kSCNetworkReachabilityFlagsReachable.toInt() != 0
    val needsConn = flags.toInt() and kSCNetworkReachabilityFlagsConnectionRequired.toInt() != 0
    val online = reachable && !needsConn
    scope.launch {
        _status.value = if (online) ConnectivityStatus.Online else ConnectivityStatus.Offline
    }
}
Enter fullscreen mode Exit fullscreen mode

Gradle link (SystemConfiguration):

// shared/build.gradle.kts
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

kotlin {
    targets.withType<KotlinNativeTarget>().configureEach {
        binaries.all { linkerOpts("-framework", "SystemConfiguration") }
    }
}
Enter fullscreen mode Exit fullscreen mode

Prefer NWPathMonitor? Switch the iOS impl to Network.framework and link that instead.

Using it in Compose

@Composable
fun Main(factory: ConnectivityMonitorFactory) {
    val monitor = remember(factory) { factory.create() }
    val status by monitor.status.collectAsState(ConnectivityStatus.Unavailable)

    LaunchedEffect(Unit) { monitor.start() }
    DisposableEffect(Unit) { onDispose { monitor.stop() } }

    // render your UI (e.g., a status card with Online / Offline / … Checking)
}
Enter fullscreen mode Exit fullscreen mode

Platform entry points:


// Android
setContent { Main(factory = com.qandil.kmpconnectivity.connectivity.ConnectivityMonitorFactory(this)) }

// iOS
Main(factory = com.qandil.kmpconnectivity.connectivity.ConnectivityMonitorFactory())
Enter fullscreen mode Exit fullscreen mode

How to run

Android: ./gradlew :androidApp:assembleDebug (or Run from Android Studio)

iOS: Run from Android Studio’s iOS config or your Xcode project (Simulator/Device)

Roadmap / help wanted

Flow.retryWhenOnline(monitor) helper

Expose network type (Wi-Fi/Cellular)

Tests with stubs

If this was useful, a ⭐ or PR would mean a lot:
Repo: https://github.com/Qandil11/KMP-Connectivity-Monitor

License

MIT (in the repo).

Top comments (0)