Observing your network connection with Flow

Barry Irvine
3 min readFeb 3, 2022

--

One of the biggest challenges of the modern Android development is migrating all the Context-based callbacks that used to live in an Activity or Fragment into a component that we can easily use in our ViewModel. Even better, if we can change the callback into a Kotlin Flow, we can easily consume all the changes as and when they happen.

Recently I wanted to display a banner in a Composable that only shows when there is no network connection. Basically I wanted to consume a boolean Flow that returned true if there was a network connection and false if there wasn’t.

First off, I needed to add the relevant permission to the manifest:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

I then also needed to know the current state of the network. Unfortunately I needed to support all the way back to Lollipop and there are different commands for each Android version so I created a compatibility object hides the different implementations.

object ConnectedCompat {

private val IMPL: ConnectedCompatImpl

init {
IMPL = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
MarshMallowImpl
} else {
BaseImpl
}
}

fun isConnected(connectivityManager: ConnectivityManager) =
IMPL.isConnected(connectivityManager)

internal interface ConnectedCompatImpl {
fun isConnected(connectivityManager: ConnectivityManager): Boolean
}

object BaseImpl : ConnectedCompatImpl {
@Suppress("DEPRECATION")
override fun isConnected(connectivityManager: ConnectivityManager): Boolean =
connectivityManager.activeNetworkInfo?.isConnected ?: false

}

object MarshMallowImpl : ConnectedCompatImpl {
@RequiresApi(Build.VERSION_CODES.M)
override fun isConnected(connectivityManager: ConnectivityManager): Boolean =
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
?.hasCapability(NET_CAPABILITY_INTERNET) == true
}
}

So using the ConnectivityManager I can now work out whether I’m currently connected, but what about actually getting changes in the network state updating the flow? For that I need to utilise callbackFlow.

To begin with, I added a provider for the ConnectivityManager to my Hilt configuration. Something like this:

@Module
@InstallIn(SingletonComponent::class)
class ApplicationModule {
@Provides
fun provideConnectivityManager(@ApplicationContext context: Context): ConnectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}

Now I can just create a new component which I’m going to call NetworkMonitor which I can inject into a ViewModel or an Activity or Fragment for that matter.

class NetworkMonitor @Inject constructor(
private val connectivityManager: ConnectivityManager
) {

val isConnected: Flow<Boolean> = callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
trySend(true)
}

override fun onLost(network: Network) {
trySend(false)
super.onLost(network)
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
}
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
trySend(ConnectedCompat.isConnected(connectivityManager))
connectivityManager.registerNetworkCallback(request, callback)

awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
}
}

There’s a lot to unpack there but basically this allows me to wrap the old callback so that it produces a cold Flow of Boolean. Each time a WiFi or cellular data network is available it emits true and when it is lost it emits false. When the NetworkMonitor goes out of scope — for example the ViewModel that was referencing it is no longer in use — the callback is automatically unregistered (this is the awaitClose section). Furthermore, when we first access the NetworkMonitor we also want the current state of the network — for which we use the compatibility method we just created :

trySend(ConnectedCompat.isConnected(connectivityManager))

There is one final Marshmallow-specific bit of code in there — in Marshmallow you can also check if a network really has Internet capabilities, so I also added this clause for that scenario:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}

A really basic example of using this would be:

@HiltViewModel
class MyViewModel @Inject constructor(
val networkMonitor: NetworkMonitor
) : ViewModel()
@Composable
fun MyScreen(
model: MyViewModel = hiltViewModel()
) {
val hasNetwork by model.networkMonitor.isConnected.collectAsState(true)
if (!hasNetwork) {
Text("You don't have a network ")
}
}

As usual, if you enjoyed this post, I’d appreciate a recommend, follow, share or tweet and if you have any questions please don’t hesitate to post them below.

--

--

Barry Irvine

Writing elegant Android code is my passion — but with 20+ years experience in roles from programme delivery to working at the coal face, I’ve seen it all.