Android — Network status listener + StateFlow
Create a network status bar using StateFlow with support for both pre and
post Android N.
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:
- For devices running Android N or greater, an instance of ConnectivityManager.NetworkCallback will be used for notifications about network changes.
- For older devices (less than N), a context registered BroadcastReceiver will be used with a
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
See note below - 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…
- 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. - 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 🐊 ☕️