Skip to content

cmp-network-monitor-compose

cmp-network-monitor-compose

Compose Multiplatform extensions for cmp-network-monitor — drop-in banners, @Composable collectors, CompositionLocal injection, and subtree scoping.

Maven Central Kotlin License

Core library docs (platform support, NetworkStatus model, validation strategies, captive portal, quality, bandwidth, extensions): see docs/network-monitor/README.md.

Platform Support

Platform Supported
Android, iOS (x64/arm64/simulator), macOS (x64/arm64), JVM, JS, WasmJS Yes
tvOS, watchOS, Linux, Windows, WasmWASI No (Compose Multiplatform limitation)

Installation

# libs.versions.toml
[versions]
cmp-network-monitor         = "3.3.0"

[libraries]
cmp-network-monitor         = { module = "io.github.mobilebytelabs:cmp-network-monitor",         version.ref = "cmp-network-monitor" }
cmp-network-monitor-compose = { module = "io.github.mobilebytelabs:cmp-network-monitor-compose", version.ref = "cmp-network-monitor" }
// build.gradle.kts
commonMain.dependencies {
    implementation(libs.cmp.network.monitor)
    implementation(libs.cmp.network.monitor.compose)
}

API Surface (Compose-only)

API Purpose
NetworkAwareContent(monitor, offlineContent, captivePortalContent, debounceMs, onlineContent) Auto switching between online / offline / captive portal subtrees.
ConnectivityBanner(modifier, monitor, message, backgroundColor, contentColor, debounceMs) Animated slide-in offline banner (Material 3).
rememberNetworkMonitor(config) Process-wide singleton; reset-aware via NetworkMonitorProvider.version.
rememberScopedNetworkMonitor(config) Per-composition lifecycle; auto-closes on dispose.
LocalNetworkMonitor CompositionLocal<NetworkMonitor>; throws if accessed without a provider.
ProvideNetworkMonitor(monitor, content) Wrapper for CompositionLocalProvider(LocalNetworkMonitor provides monitor) — also populates the internal nullable mirror used by rememberOrLocalNetworkMonitor.
rememberOrLocalNetworkMonitor(config) Returns the ProvideNetworkMonitor-injected monitor if present; falls back to rememberNetworkMonitor(config).
NetworkMonitor.collectIsOnlineAsState(debounceMs) State<Boolean>.
NetworkMonitor.collectNetworkStatusAsState(debounceMs) State<NetworkStatus>.
NetworkMonitor.collectNetworkQualityAsState(debounceMs) State<NetworkQuality> (debounce applied to upstream status before mapping).

Quick Start

// Singleton — most apps
@Composable
fun App() {
    val isOnline by rememberNetworkMonitor().collectIsOnlineAsState()
    if (!isOnline) OfflineHint()
}

// Drop-in banner
@Composable
fun Screen() {
    Column {
        ConnectivityBanner()                 // raw — flicker-honest
        ConnectivityBanner(debounceMs = 500L) // suppresses WiFi <-> Cellular handoff
        ScreenBody()
    }
}

// State-driven content switching
@Composable
fun DataScreen() {
    NetworkAwareContent(
        debounceMs = 300L,
        offlineContent = { OfflineEmptyState() },
        captivePortalContent = { portal -> CaptivePortalPrompt(portal.redirectUrl) },
        onlineContent = { available -> DataList(connectionType = available.info.type) },
    )
}

What's new in v3.3.0

Debounce on every collect / banner / content helper (M-005)

All five Compose collectors and both wrapper composables gained an optional debounceMs: Long = 0L parameter. Default 0L is identical to the pre-3.3.0 behaviour — no migration required. Pass a positive value to suppress WiFi <-> Cellular handoff flicker:

val online  by monitor.collectIsOnlineAsState(debounceMs = 300L)
val status  by monitor.collectNetworkStatusAsState(debounceMs = 300L)
val quality by monitor.collectNetworkQualityAsState(debounceMs = 300L)

ConnectivityBanner(debounceMs = 500L)
NetworkAwareContent(debounceMs = 500L) { /* onlineContent */ }

Internally these route through isOnlineDebouncedState / networkStatusDebouncedState (see core module) when debounceMs > 0L; the raw collectAsState() path is used when debounceMs <= 0L. collectNetworkQualityAsState(debounceMs > 0L) debounces the upstream NetworkStatus BEFORE mapping to quality, so quality transitions reflect the settled connection.

Reset-aware rememberNetworkMonitor() (M-004)

rememberNetworkMonitor() now keys remember on NetworkMonitorProvider.version. If your app calls NetworkMonitorProvider.reset() (logout, account switch, test cleanup), downstream composables correctly recompose and re-acquire the fresh instance instead of holding the closed reference.

// Pre-3.3.0: this remembered a closed NetworkMonitor after reset(); collect() emitted nothing.
// 3.3.0+:   recomposes on version bump and re-installs.
val monitor = rememberNetworkMonitor()

reset() while no instance is installed is a no-opversion does not advance, so reactive consumers don't see a spurious recomposition.

NetworkMonitorProvider is NOT thread-safe. Serialize install() / reset() from a single thread (app startup / shutdown or test teardown). Reads via get() / getOrNull() / version / currentStatus are safe from any thread.

ProvideNetworkMonitor + rememberOrLocalNetworkMonitor (M-007 residual)

LocalNetworkMonitor already existed; 3.3.0 adds two ergonomic helpers around it for subtree scoping (per-screen isolation, multi-window, integration tests with a FakeNetworkMonitor).

// Wrap a subtree with a custom monitor
val custom = remember { createNetworkMonitor(myConfig) }
ProvideNetworkMonitor(custom) {
    MyScreenSubtree()
}

// Reusable consumer composable — accepts BOTH provider-injected and singleton-fallback callers
@Composable
fun OfflineHint(
    monitor: NetworkMonitor = rememberOrLocalNetworkMonitor(),
) {
    val online by monitor.collectIsOnlineAsState()
    if (!online) Text("Offline")
}

Direct CompositionLocalProvider(LocalNetworkMonitor provides custom) { ... } still works, but rememberOrLocalNetworkMonitor() reads an internal nullable mirror populated by ProvideNetworkMonitor — prefer ProvideNetworkMonitor to get the singleton-fallback wiring for free.

Testing

Use a FakeNetworkMonitor from the core artifact and inject it via ProvideNetworkMonitor:

@Test
fun bannerShowsWhenOffline() = runComposeUiTest {
    val fake = FakeNetworkMonitor(initialOnline = true)
    setContent {
        ProvideNetworkMonitor(fake) {
            Column {
                ConnectivityBanner()
                TestTagText("body")
            }
        }
    }

    onNodeWithText("No internet connection").assertDoesNotExist()
    fake.setOnline(false)
    onNodeWithText("No internet connection").assertIsDisplayed()
}

For unit tests of state-collecting composables, prefer rememberOrLocalNetworkMonitor() as the parameter default so the test can inject the fake without touching the global singleton.

Cross-references

License

Copyright 2026 MobileByteLabs

Licensed under the Apache License, Version 2.0

Module reference

Module Identity (auto-gen)

Artifact Package Current version Maven Since API tier
io.github.mobilebytelabs:cmp-network-monitor-compose com.mobilebytelabs.kmptoolkit.network.monitor.compose UNKNOWN Central 2026-05-30 experimental

Module purpose (one paragraph):


§2 Per-Platform Parity Matrix (auto-gen)

Target Source-set present Real impl UnsupportedPlatform stub .kt count Last reviewed Coverage Notes
(no src/{platform}Main/ directories found) 2026-06-01

Legend (Real impl): ✅ real impl, 🟡 partial / wontfix-OS / wontfix-infra / legacy stub, ⛔ not declared, — N/A. Legend (Coverage enum, since 2026-06-01): full (all public-API methods backed by OS primitive) · partial (most real; some typed UnsupportedPlatform fallbacks for contracts that don't apply) · wontfix-OS (OS lacks the primitive) · wontfix-infra (impl possible but CI/toolchain blocks it) · (legacy:full|stub) (auto-derived; pre-opt-in modules — add a // LD-2-coverage: {enum} comment to the platform's primary .kt file to graduate). See RULE-LIB-DEVELOPMENT-MD-001 LD-2 + ADRs for accepted wontfix cases.


API reference

Each release ships the module's full Dokka HTML site inside its -javadoc.jar artifact on Maven Central.

In IntelliJ / Android Studio the IDE mounts the jar and surfaces it automatically in hover popups, Quick Documentation, and Symbol search.