How to observe Network connectivity status using Kotlin Flows
and show it inside Compose UI?
Monitoring Network connectivity status is a very common use-case, either we want to display current network status on the UI or to show a retry mechanism if the device is offline.
This story will show code examples about how to monitor and convert Network connectivity callbacks into callbackFlow
and observe it into the UI layer to show updates inside Compose UI and in the end it will provide helpful Takeaways
.
Prerequisites
This story requires basic understanding of following components
- Kotlin
Flows
andcallbackFlow
- Dependency Injection (
Dagger
orDagger Hilt
) - Basic understanding of Jetpack Compose
What do we want to achieve?
We want to create a Network Connectivity Service
to achieve followings
- Monitor current
Network Status
on device to know if device is connected to the Network or not. - Observe changes to the Network status in real time.
- Use Jetpack Compose API to show offline status inside the Compose UI.
Let’s get to Network Connectivity Service.
NetworkStatus
As we want to read Network Status from the service. In order to expose Network Status from the service we will use a sealed class holding that status information. See below the NetworkStatus
sealed class.
sealed class NetworkStatus { object Unknown: NetworkStatus() object Connected: NetworkStatus() object Disconnected: NetworkStatus() }
Network Connectivity Service
Let’s see the code first.
interface NetworkConnectivityService { | |
val networkStatus: Flow<NetworkStatus> | |
} | |
class NetworkConnectivityServiceImpl @Inject constructor ( | |
context: Context | |
): NetworkConnectivityService { | |
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | |
override val networkStatus: Flow<NetworkStatus> = callbackFlow { | |
val connectivityCallback = object : NetworkCallback() { | |
override fun onAvailable(network: Network) { | |
trySend(NetworkStatus.Connected) | |
} | |
override fun onUnavailable() { | |
trySend(NetworkStatus.Disconnected) | |
} | |
override fun onLost(network: Network) { | |
trySend(NetworkStatus.Disconnected) | |
} | |
} | |
val request = NetworkRequest.Builder() | |
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) | |
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) | |
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) | |
.build() | |
connectivityManager.registerNetworkCallback(request, connectivityCallback) | |
awaitClose { | |
connectivityManager.unregisterNetworkCallback(connectivityCallback) | |
} | |
} | |
.distinctUntilChanged() | |
.flowOn(Dispatchers.IO) | |
} |
NetworkConnectivityServiceImpl
is implementing an interface which is exposing networkStatus
as Flow<NetworkStatus>
.
The service is using callbackFlow
, callbackFlow
is an ideal choice here. Whenever we want to convert any API callbacks into Kotlin Flow callbackFlow
is the answer. callbackFlow
ensures that lambda remains active to be able to send data later-on during callbacks. It also provides awaitClose
block to unregister listeners when callbackFlow
closes.
In lambda connectivityManager
is registering to the network status callbacks. In each callback method it sends a NetworkStatus
using trySend
. As mentioned before callbackFlow
provides awaitClose
block to unregister for the network changes. awaitClose
will be called when callbackFlow
is closed so we need to unregister for listeners.
flowOn(Dispatchers.IO)
is to make sure that calling networkStatus
is main safe because we will be calling it from the UI layer inside ViewModel.
distinctUntilChanged()
ensures that it sends new value only when Network Status changes to avoid sending unnecessary updates. In real cases that will usually be the case when you turn off the Airplane mode then you will get the callback onAvailable
twice, one for wifi and the other for cellular.
What to know more about callbackFlow? I have written a detailed story on callbackFlow
, taking example of Firebase
RealtimeDatabase
callbacks and converting them into callbackFlow
. If interested you can read from the link below.
callbackFlow with Firebase — converting RealtimeDatabase callbacks into callbackFlow
Using Network Connectivity Service in ViewModel.
In order to provide current NetworkStatus
for UI, we will create a networkStatus
property in ViewModel
from where Compose UI will collect it.
Exposing a property in ViewModel
will look like below.
val networkStatus: StateFlow<NetworkStatus> = networkConnectivityService.networkStatus.stateIn( | |
initialValue = NetworkStatus.Unknown, | |
scope = viewModelScope, | |
started = WhileSubscribed(5000) | |
) |
Flow.StateIn operator is used to convert
callbackFlow
intoStateFlow
.NetworkConnectivityService
will be injected as dependency inside the ViewModel constructor, I am usingHilt
, It’s up to you whatever Dependency Injector you want to use.initialValue
insideFlow.StateIn is set
Unknown
because in the beginning when UI loads we will not knowNetworkStatus
and we don’t want to show any notification on UI setting default value to any ofConnected
orDisconnected.
- Providing
viewModelScope
will bind Flow to the life-cycle ofviewModelScope
. WhileSubscribed
is used to cancel the upstream automatically when there are no collectors collecting the flow.WhileSubscribed(5000)
will wait for 5 more minutes after the last collector before closing the upstream, It will avoid restarting the whole upstream flow unnecessary specially during configuration changes.
Collecting Network Status inside Compose UI
On the UI we will show notification when the device is offline
using the snackbar. LaunchedEffect
is the best choice here, I have written a very detailed blog post about LaunchedEffect
vs rememberCoroutineScope
exploring and explaining When and How to use both APIs. I recommend reading it, link is below.
Job Offers
Collecting networkStatus
inside Compose UI looks like below.
@OptIn(ExperimentalMaterial3Api::class) | |
@Composable | |
fun NetworkConnectivityScreen( | |
viewModel: NetworkConnectivityViewModel = viewModel(), | |
snackbarHostState: SnackbarHostState = SnackbarHostState() | |
) { | |
val networkStatus = viewModel.networkStatus.collectAsStateWithLifecycle() | |
if (networkStatus.value == NetworkStatus.Disconnected) { | |
LaunchedEffect(networkStatus) { | |
snackbarHostState.showSnackbar("you are offline") | |
} | |
} | |
Scaffold( | |
snackbarHost = { SnackbarHost(hostState = snackbarHostState) } | |
) { paddingValues -> | |
Box( | |
modifier = Modifier.fillMaxSize().padding(paddingValues), | |
contentAlignment = Alignment.Center | |
) { | |
Text(text = "Connectivity Service") | |
} | |
} | |
} |
collectAsStateWithLifecycle
is used to collect fornetworkStatus
in Composable, This is life-cycle-aware API and is a recommended way to collect Flow inside Compose.LaunchedEffect
is used to execute asuspend
functionsnackbarHostState.showSnackbar("message")
only whennetworkStatus
is inDisconnected
state.LaunchedEffect
launches coroutine within the scope of the composable where it is being used and cancels it when it leaves the composition so we don’t need to worry about the life-cycle of coroutine being automatically managed.
Takeaways
callbackFlow
is tailor built for converting any API callbacks into Kotlin Flow.Flow.StateIn
converts cold flow into hot flow, in this example it convertscallbackFlow
intoStateFlow
.collect
Flow
in Jetpack Compose in a life-cycle-aware manner via APIcollectAsStateWithLifecycle
WhileSubscribed(5000)
waits for 5 minutes before restarting upstream flow if flow does not have any observer, helpful in configuration changes or heavy upstream flows and also in UI related updates.LaunchedEffect
effect API is the recommended way to executesuspend
functions as an effect of anything happening out of the scope of composable and changes of Network connectivity is an example of those side-effects happening out of the scope of composable.
Sources
That’s it for now! Hope it was helpful… Looking forward to any questions/suggestions in the comments.
Remember to follow and 👏 if you liked it 🙂
— — — — — — — — —
This article was previously published on proandroiddev.com