Android — Network status listener + StateFlow

crocsandcoffee
6 min readDec 21, 2020

--

Create a network status bar using StateFlow with support for both pre and
post Android N.

Ex. offline state pulled from https://material.io/design/communication/offline-states.html

TL;DR — The bottom of this post shows the full implementation of the Repository, ViewModel and how the StateFlow is consumed in the view. There is also a video demo and link to the full source code on GitHub.

However, if you’d like to walk through all the steps, read on!

Alright…. my first post on Medium.. here it goes!

I’ll get straight to the point. Let’s say you want to show an offline banner, dialog or a different UI component to indicate to the user they are offline. Or maybe you want to trigger a network request or begin some process depending on the user’s internet connectivity status. The UI part is simple… but how can you get notified when the device’s internet connectivity changes?

Android N (24) added ConnectivityManager.NetworkCallbacks. Now if your minSdkVersion is ≥ 24 than your implementation will be straightforward with just using this new NetworkCallback API as you don’t need to worry about supporting legacy code. There’s some examples floating around on Medium and StackOverflow that exemplify at a high level how you can setup these callbacks, but I was interested in setting this logic up in a reusable way, following the separation of concerns principle, that would fit with my MVVM architecture and could be injected and used for any UI Controller that cared about network changes with little to no additional effort. Let’s check out the code!

NetworkStatusState

/**
* State hierarchy for different Network Status connections
*/
sealed class
NetworkStatusState {

/* Device has a valid internet connection */
object
NetworkStatusConnected : NetworkStatusState()

/* Device has no internet connection */
object
NetworkStatusDisconnected : NetworkStatusState()
}

Since I wanted to emit changes in network connectivity via StateFlow, I created a sealed class representing the different possible states I cared about. This is where you can get more creative for states like, “connecting…” or network error, but I only cared about two finite use cases.

Although I used StateFlow, you could use whatever Observable pattern you prefer, such as LiveData or RxJava etc…

NetworkStatusRepository

/**
* This Repository manages listening to changes in the network state * of the device and emits the network change via a
[StateFlow].
*/
@Singleton
class
NetworkStatusRepository @Inject constructor(
private val appContext: Context,
private val mainDispatcher: CoroutineDispatcher,
private val appScope: CoroutineScope
) {
.....}

For this particular project, I was using Dagger for DI, where an app wide CoroutineScope was created. This isn’t required and other DI should work the same if setup correctly. The important part is to make sure this repository is a singleton, with the reasons explained further below.

// Just for additional clarity
// ex. application scope defined in my Dagger App module
@Provides
@Singleton
fun provideAppScope(): CoroutineScope {
return CoroutineScope
(SupervisorJob() + Dispatchers.Main)
}

The NetworkStatusRepository will encapsulate the appropriate calls for listening for network changes on different flavors of Android and just emitting them as a NetworkStatusState .

// NetworkStatusRepository.ktinit {
_state
.subscriptionCount
.map { count -> count > 0 } // map count into active/inactive flag
.distinctUntilChanged() // only react to true <--> false changes
.onEach { isActive ->
/** Only subscribe to network callbacks if we have an active subscriber */
if
(isActive) subscribe()
else unsubscribe()
}
.launchIn(appScope)
}

The init block here is special.. I wanted to implement this in a way where it was self managing, and handled subscribing/unsubscribing itself for listening to network status changes, without external calls propagated from the view layer. Basically, if there are no active subscribers of the StateFlow state, then this repository would unregister itself as a listener appropriately. Once a new subscriber appears, we would start listening for network changes and emit the current network state right away.

Our complete Repository:

class NetworkStatusRepository constructor(
private val context: Context,
private val mainDispatcher: CoroutineDispatcher,
private val appScope: CoroutineScope
) {

private val cm: ConnectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

private var callback: ConnectivityManager.NetworkCallback? = null
private var
receiver: ConnectivityReceiver? = null

private val
_state = MutableStateFlow(getCurrentNetwork())
val state: StateFlow<NetworkStatusState> = _state

init {
_state
.subscriptionCount
.map { count -> count > 0 } // map count into active/inactive flag
.distinctUntilChanged() // only react to true<->false changes
.onEach { isActive ->
/** Only subscribe to network callbacks if we have an active subscriber */
if
(isActive) subscribe()
else unsubscribe()
}
.launchIn(appScope)
}

/* Simple getter for fetching network connection status synchronously */
fun
hasNetworkConnection() = getCurrentNetwork() == NetworkStatusConnected

private fun getCurrentNetwork(): NetworkStatusState {
return try {
cm.getNetworkCapabilities(cm.activeNetwork)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.let { connected ->
if (connected == true) NetworkStatusConnected
else NetworkStatusDisconnected
}
} catch (e: RemoteException) {
NetworkStatusDisconnected
}
}

private fun subscribe() {

// just in case
if
(callback != null || receiver != null) return

if
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
callback = NetworkCallbackImpl().also { cm.registerDefaultNetworkCallback(it) }
} else {
receiver = ConnectivityReceiver().also {
context.registerReceiver(it, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
}
}

/* emit our initial state */
emitNetworkState(getCurrentNetwork())
}

private fun unsubscribe() {

if (callback == null && receiver == null) return

if
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
callback?.run { cm.unregisterNetworkCallback(this) }
callback = null
} else {
receiver?.run { context.unregisterReceiver(this) }
receiver = null
}
}

private fun emitNetworkState(newState: NetworkStatusState) {
appScope.launch(mainDispatcher) {
_state.emit(newState)
}
}

private inner class ConnectivityReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {

/** emit the new network state */
intent
.getParcelableExtra<NetworkInfo>(ConnectivityManager.EXTRA_NETWORK_INFO)
?.isConnectedOrConnecting
.let { connected ->
if (connected == true) emitNetworkState(NetworkStatusConnected)
else emitNetworkState(getCurrentNetwork())
}
}
}

private inner class NetworkCallbackImpl : ConnectivityManager.NetworkCallback() {

override fun onAvailable(network: Network) = emitNetworkState(NetworkStatusConnected)

override fun onLost(network: Network) = emitNetworkState(NetworkStatusDisconnected)
}
}

A couple things to note here:

  1. For devices running Android N or greater, an instance of ConnectivityManager.NetworkCallback will be used for notifications about network changes.
  2. For older devices (less than N), a context registered BroadcastReceiver will be used with a IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) See note below
  3. I wouldn’t recommend suppressing the deprecated warnings that will surface here as you can miss or forgot to remove unnecessary OS runtime checks when bumping up your minSdkVersion later on…
  4. A ConnectivityManager.NetworkCallback should be registered at most once at any time. This is why we unregister this callback when we have no active subscribers and only ever manage a single instance.
  5. The ConnectivityManager.NetworkCallback will continue to be called until either the application exists or unregisterNetworkCallback is called.

Note: Apps targeting Android 7.0 (24) and higher do not receive this broadcast if they declare the broadcast receive in their manifest. Apps will still receive broadcasts if they register their BroadcastReceiver with Context.registerReceiver().

Connecting our Repository to our ViewModel

@MainThread
class
NetworkStatusViewModel(
private val repo: NetworkStatusRepository
) : ViewModel() {

/** [StateFlow] emitting a [NetworkStatusState] every time it changes */
val
networkState: StateFlow<NetworkStatusState> = repo.state

/* Simple helper/getter for fetching network connection status synchronously */
fun
isDeviceOnline() : Boolean = repo.hasNetworkConnection()
}

Connecting our ViewModel to our View/UI

viewModel.networkState.asLiveData().observe(this) { state ->
binding.offlineBar.offlineBarRoot.visibility = when (state) {
NetworkStatusState.NetworkStatusConnected -> View.GONE
NetworkStatusState.NetworkStatusDisconnected -> View.VISIBLE
}
}

Voila! We can now include the NetworkStatusViewModel in any Activity/Fragment where we care about network connectivity changes and simply observe on the StateFlow networkState for updates. Here’s a video of everything working together:

The full source code for this demo app and the examples can be found here.

Please feel free to share your questions, thoughts, concerns, any feedback would be much appreciated.

Happy Coding!
- crocsandcoffee 🐊 ☕️

--

--

crocsandcoffee
crocsandcoffee

Written by crocsandcoffee

Staff Android Engineer @ Block/Square | Android Tech Lead @ Flave | Android Lead @ RadioJavan | Gamer | Foodie | CodeBlooded

Responses (1)