惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
博客园_首页
博客园 - 【当耐特】
V
Visual Studio Blog
博客园 - 叶小钗
月光博客
月光博客
美团技术团队
J
Java Code Geeks
小众软件
小众软件
Y
Y Combinator Blog
博客园 - Franky
Martin Fowler
Martin Fowler
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
Microsoft Security Blog
Microsoft Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
I needed simple KMP wrappers for two common APIs. So I bu...
ikhomutskikh · 2026-06-16 · via DEV Community

While working on my Kotlin Multiplatform project, I kept running into the same problem: I needed something, searched Maven Central, found either nothing maintained or nothing with the simple coroutine-friendly API I wanted. So I built them myself. Twice.

This is the story of KMP Network Monitor and KMP In-App Review — two small libraries, what gap each fills, why expect/actual is the right pattern for both, and what I learned publishing them to Maven Central.


Why two libraries?

Both solve the same category of problem: platform APIs that every mobile app needs, but that KMP doesn't abstract for you. In a KMP project, you either write them twice, find a library, or build one.

I tried the middle option first. What I found was either abandoned, overly complex for my needs, or missing one of the two platforms. And since I'd never built a KMP library before — I decided this was a good excuse to start. So I built both myself.


Library 1: KMP Network Monitor

The problem

Android uses ConnectivityManager with NetworkCallback. iOS uses NWPathMonitor. Different APIs, different threading models, different event semantics. I needed a single reactive stream I could consume from shared commonMain code.

The expect/actual approach

The natural instinct is a commonMain interface with two implementations. That works — but it's ordinary polymorphism, not KMP. With expect/actual, the compiler enforces that both sides exist. That's a compilation model guarantee you don't get from an interface.

// commonMain
expect class NetworkMonitor() : INetworkMonitor {
    override val connectionState: Flow<ConnectionState>
}

Three states, not two

sealed class ConnectionState {
    object Connected    : ConnectionState()
    object Disconnected : ConnectionState()
    object Unknown      : ConnectionState()
}

Unknown is the initial state before the platform resolves connectivity. Without it, you'd flash an "Offline" banner on every app launch.

The tricky part: validated connectivity

onAvailable fires when Android sees any network interface — including captive portals that have no real internet access. Trusting it alone means reporting "Connected" to a user who can't load anything.

The fix: wait for onCapabilitiesChanged with both NET_CAPABILITY_INTERNET and NET_CAPABILITY_VALIDATED:

override fun onAvailable(network: Network) {
    // Intentionally empty — wait for NET_CAPABILITY_VALIDATED
}

override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
    val valid = networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)
            && networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)
    if (valid) validNetworks.add(network) else validNetworks.remove(network)
    updateState()
}

override fun onLost(network: Network) {
    validNetworks.remove(network)
    updateState()
}

We track a Set<Network> because Android fires events per interface. With Wi-Fi and mobile data active, losing one shouldn't report Disconnected.

iOS: a striking contrast

// iosMain
actual class NetworkMonitor actual constructor() : INetworkMonitor {
    private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Unknown)
    actual override val connectionState: Flow<ConnectionState> = _connectionState

    init {
        val monitor = nw_path_monitor_create()
        nw_path_monitor_set_update_handler(monitor) { path ->
            _connectionState.value = if (nw_path_get_status(path) == nw_path_status_satisfied)
                ConnectionState.Connected else ConnectionState.Disconnected
        }
        nw_path_monitor_set_queue(monitor, dispatch_get_main_queue())
        nw_path_monitor_start(monitor)
    }
}

Android: ~50 lines, capability checks, set-based tracking. iOS: ~15 lines, nw_path_monitor handles everything. Same interface. Very different roads.

Setup and testability

Android: NetworkMonitorInitializer.initialize(this) — one line in onCreate.
iOS: zero setup.

For testing, inject INetworkMonitor and use FakeNetworkMonitor in tests:

val fake = FakeNetworkMonitor(initialState = ConnectionState.Connected)
fake.emit(ConnectionState.Disconnected) // simulate network loss


Library 2: KMP In-App Review

The problem

Existing KMP in-app review libraries felt heavier than I needed — multiple modules, delegate patterns, result codes for different stores. I wanted a single suspend fun requestReview() that works for Google Play and iOS App Store, nothing more.

Same pattern, more dramatic contrast

// commonMain
public expect class ReviewManager {
    public suspend fun requestReview()
}

Android — two suspendCancellableCoroutine calls: first to get a ReviewInfo token, then to launch the flow:

public actual class ReviewManager(private val activity: Activity) {
    public actual suspend fun requestReview() {
        val manager = ReviewManagerFactory.create(activity)
        val reviewInfo = suspendCancellableCoroutine { cont ->
            manager.requestReviewFlow()
                .addOnSuccessListener { cont.resume(it) }
                .addOnFailureListener { cont.resumeWithException(it) }
        }
        suspendCancellableCoroutine { cont ->
            manager.launchReviewFlow(activity, reviewInfo)
                .addOnCompleteListener { cont.resume(Unit) }
        }
    }
}

iOS:

public actual class ReviewManager {
    public actual suspend fun requestReview() {
        SKStoreReviewController.requestReview()
    }
}

Android takes responsibility for the full process and gives more control. iOS hands everything to the OS. expect/actual absorbs the difference so the caller never thinks about it.

The honest testing contract

Neither platform guarantees the dialog appears — not because the library is incomplete, but because both Google and Apple deliberately rate-limit review prompts to protect users from being spammed. The library contract is:

The coroutine completes without error. Not that the dialog was shown.

iOS Simulator always shows the dialog regardless of rate limits — useful during development.


The Gradle reality nobody writes about

Both libraries use vanniktech/gradle-maven-publish-plugin. Real problems I hit:

  • Configuration cache: add --no-configuration-cache to publish tasks
  • JVM target alignment: explicit targets required across all source sets — mismatch errors are cryptic
  • GPG signing: store key ID, password, and secret in local.properties, never in version control (it may be obvious, but still worth mentioning)
  • Publishing pipeline: GPG setup → Sonatype namespace verification → vanniktech plugin config → ./gradlew publishAllPublicationsToMavenCentralRepository --no-configuration-cache → manual release in Sonatype UI.

Conclusion

expect/actual is the story. Both libraries are small. What makes them genuinely KMP is the compilation model guarantee — the compiler enforces both sides exist. That's something you don't get from a plain interface.

Sometimes small and focused beats large and complete. A library that does one thing well and integrates in five minutes can be more useful than one that does everything but nobody reads the docs for.

The gap matters more than the size. I searched, found nothing that fit, and built the thing. That's reason enough to publish.


Links