Building an app-wide network status indicator in Jetpack Compose
We've all been there. You tap a button in an app, expecting something magical to happen and then nothing. You wait. Maybe you tap again. Eventually, after what feels like an eternity (but is probably just a network timeout), you get a cryptic error message: "Could not connect" or worse, the app just hangs. Frustrating, right?
This often happens because the app only checks for an internet connection after you've initiated an action that requires one. This reactive approach leads to delays, uncertainty, and frankly, unhappy users. What if the app proactively told users when the internet connection is down, before they even try to perform an online action?
Proactively informing users about their connectivity status prevents failed attempts, manages expectations, and leads to a smoother, less frustrating interactions.
Global monitoring, central Display
The key is to monitor the network state continuously and display a persistent indicator whenever the connection drops.
We need two main components:
- A Connectivity Service: A reusable component responsible for observing the system's network status changes.
- UI Integration: Logic within our main Composable structure to react to status changes reported by the service and display/hide the indicator.
Step 1: Building the Connectivity Service
We'll create a service that leverages Android's ConnectivityManager
and its NetworkCallback
API. This approach provides real-time updates on network availability and capabilities. We'll wrap this callback-based API into a Kotlin Flow
for easy consumption with coroutines.
To use this network API, you need to add <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
in your AndroidManifest.xml
.
interface ConnectivityService {
fun isConnectionAvailable(): Flow<Boolean>
}
class RealConnectivityService(
private val context: Context,
) : ConnectivityService {
private val connectivityManager = context
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
override fun isConnectionAvailable(): Flow<Boolean> = callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
super.onCapabilitiesChanged(network, networkCapabilities)
val connected = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) // 1.
trySend(connected)
}
override fun onUnavailable() {
super.onUnavailable()
trySend(false)
}
override fun onLost(network: Network) {
super.onLost(network)
trySend(false)
}
override fun onAvailable(network: Network) {
super.onAvailable(network)
val capabilities = connectivityManager.getNetworkCapabilities(network)
trySend(capabilities?.hasCapability(NET_CAPABILITY_VALIDATED) == true)
}
}
connectivityManager.registerDefaultNetworkCallback(callback) // 2.
val initialNetwork = connectivityManager.activeNetwork
if (initialNetwork != null) {
val capabilities = connectivityManager.getNetworkCapabilities(initialNetwork)
trySend(capabilities?.hasCapability(NET_CAPABILITY_VALIDATED) == true)
} else {
trySend(false)
}
awaitClose { // 3.
connectivityManager.unregisterNetworkCallback(callback)
}
}.distinctUntilChanged() // 4.
}
ConnectivityManager.kt
NET_CAPABILITY_VALIDATED
: This is crucial! It checks if the device can actually reach the internet, not just if it's connected to a network (like a WiFi router with no internet uplink).registerDefaultNetworkCallback
: Monitors the default network used by the app.awaitClose
: Ensures we unregister the callback when the collector scope is cancelled (e.g., Activity is destroyed), preventing memory leaks.distinctUntilChanged
: Prevents unnecessary UI updates if the connectivity status flaps but remains the same (e.g., true -> true).
Step 2: Integrating into the UI
Now, let's use this service in our MainActivity
(or wherever your root Composable lives). We'll collect the Flow
provided by our ConnectivityService
and use a SnackbarHostState
to manage the display of our notification Snackbar
.
class MainActivity : ComponentActivity() { // 1.
[...]
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
[...]
setContent {
[...]
val connectivityStatusSnackbarHostState = SnackbarHostState()
lifecycleScope.launch { // 2.
connectivityService.isConnectionAvailable()
.collectLatest { isConnectionAvailable ->
if (!isConnectionAvailable) { // 3.
connectivityStatusSnackbarHostState.showSnackbar(
message = "No internet, please wait"
)
} else {
connectivityStatusSnackbarHostState
.currentSnackbarData?.dismiss()
}
}
}
[...]
SnackbarHost( // 4.
modifier = Modifier.safeDrawingPadding(),
hostState = connectivityStatusSnackbarHostState,
) { data ->
Surface( // 5.
shadowElevation = 6.dp,
shape = AppTheme.shapes.medium,
) {
Row(
modifier =
Modifier.padding(AppTheme.spacing.xl),
verticalAlignment =
Alignment.CenterVertically,
horizontalArrangement =
Arrangement.spacedBy(AppTheme.spacing.xl),
) {
Icon(
painter =
painterResource(
id = R.drawable.ic_info
),
contentDescription = null,
modifier = Modifier.size(32.dp),
)
Text(
text = data.visuals.message,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}
MainActivity.kt
MainActivity
Integration: Placing the observer here ensures it runs as long as the main activity is alive, covering the entire app lifecycle hosted within it.lifecycleScope.launch
/LaunchedEffect
: We launch a coroutine tied to the Activity's lifecycle (or composition) to collect theFlow
.collectLatest
is used to ensure that if the connection state flaps quickly, we only process the latest value.SnackbarHostState
: This state object controls theSnackbar
. We callshowSnackbar
when offline anddismiss
when online.SnackbarDuration.Indefinite
keeps it visible until explicitly dismissed.SnackbarHost
: This Composable listens to theSnackbarHostState
and displays the actualSnackbar
UI when requested.- Custom
Snackbar
: We provide a custom lambda to theSnackbarHost
to control its appearance (background color, icon, text style) for a non-intrusive but clear indication. Note the use ofsafeDrawingPadding()
to avoid overlapping with system bars when using edge-to-edge.
With this implementation, users will immediately see a clear "No internet, please wait" message appear (and stay) whenever their connection drops. It disappears automatically when the connection is restored. This simple, proactive feedback loop prevents confusion and manages expectations, leading to a significantly better user experience.
Happy coding!