Freelancing for Pale Blue

Looking for flexible work opportunities that fit your schedule?


Building an app-wide network status indicator in Jetpack Compose

Android Apr 25, 2025

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:

  1. A Connectivity Service: A reusable component responsible for observing the system's network status changes.
  2. 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

  1. 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).
  2. registerDefaultNetworkCallback: Monitors the default network used by the app.
  3. awaitClose: Ensures we unregister the callback when the collector scope is cancelled (e.g., Activity is destroyed), preventing memory leaks.
  4. 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

  1. 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.
  2. lifecycleScope.launch / LaunchedEffect: We launch a coroutine tied to the Activity's lifecycle (or composition) to collect the Flow. collectLatest is used to ensure that if the connection state flaps quickly, we only process the latest value.
  3. SnackbarHostState: This state object controls the Snackbar. We call showSnackbar when offline and dismiss when online. SnackbarDuration.Indefinite keeps it visible until explicitly dismissed.
  4. SnackbarHost: This Composable listens to the SnackbarHostState and displays the actual Snackbar UI when requested.
  5. Custom Snackbar: We provide a custom lambda to the SnackbarHost to control its appearance (background color, icon, text style) for a non-intrusive but clear indication. Note the use of safeDrawingPadding() 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!

Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.