在Android中使用Flow获取网络连接信息
如果你是一名Android开发者,你可能会对这个主题感到有趣。考虑到几乎每个应用程序都需要数据交换,例如刷新动态或上传/下载内容。而互联网连接对此至关重要。但是,当用户的设备离线时,数据如何进行交换呢?我们如何确定设备重新连接到互联网,以便我们可以提供他们请求的数据?本文将指导您了解如何读取和监听用户的网络状态。
让我们来深入研究一下!
首先,我们要使用ConnectivityManager
类来确定用户设备的网络连接状态。
class MyConnectivityManager(context: Context) {
private val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
fun isConnected(): Boolean {
// Network class represents one of the networks that the device is connected to.
val activeNetwork = connectivityManager.activeNetwork
return if (activeNetwork == null) {
false // if there is no active network, then simply no internet connection.
} else {
// NetworkCapabilities object contains information about properties of a network
val netCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
(netCapabilities != null
// indicates that the network is set up to access the internet
&& netCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
// indicates that the network provides actual access to the public internet when it is probed
&& netCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
}
}
}
然而,连接状态可能随时发生变化,因此如果我们希望监听这些变化,我们可以利用ConnectivityManager.NetworkCallback
。
class MyConnectivityManager(context: Context) {
private val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network : Network) {
// indicates that the device is connected to a new network that satisfies the capabilities
// and transport type requirements specified in the NetworkRequest
}
override fun onLost(network : Network) {
// indicates that the device has lost connection to the network.
}
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
// indicates that the capabilities of the network have changed.
}
})
fun subscribe() {
connectivityManager.registerDefaultNetworkCallback(networkCallback)
/*
or:
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
*/
}
fun unsubscribe() {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
我想分享一些关于以下内容的知识:
ConnectivityManager.registerDefaultNetworkCallback(NetworkCallback)
和 ConnectivityManager.registerNetworkCallback(NetworkRequest, NetworkCallback)
何时使用哪个?这取决于你的需求。
registerDefaultNetworkCallback(NetworkCallback)
用于接收关于我们应用程序默认网络的变化通知。所有应用程序都有一个默认网络,由系统确定。系统通常倾向于选择非计量网络而不是计量网络,并且更喜欢速度更快的网络而不是速度较慢的网络。
而 registerNetworkCallback(NetworkRequest, NetworkCallback)
则用于只接收特定网络的通知。这就是为什么有 NetworkRequest
来指定需求。
例如,下面的代码用于创建一个请求,该请求连接到互联网并使用Wi-Fi或蜂窝连接作为传输类型。
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
或者像这样:
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
NetworkCapabilities.NET_CAPABILITY_NOT_METERED
能力用于不向用户计费的网络。我们鼓励您参考此文档以确定适当的使用方式,并在可能的情况下限制对计量网络的使用,包括将大型下载推迟到连接到非计量网络的时候。你可以了解更多关于 NetworkCapabilities
常量以调整你的需求。
总结一下,使用 registerDefaultNetworkCallback(NetworkCallback)
对默认网络连接进行一般性的监控。然而,这个方法是在API 24(或26,具体取决于是否与Handler一起使用)中添加的。
如果你的应用程序支持最低SDK版本为21,那么使用 registerNetworkCallback(NetworkRequest, NetworkCallback)
更可取。
还有一个在API级别21引入的requestNetwork(NetworkRequest, NetworkCallback)
方法,用于查找与指定NetworkRequest
匹配的最佳网络。区别在于,register...()
用于监听网络连接的变化,而 request...()
更类似于请求特定网络。
系统将限制每个应用程序(由应用程序UID标识)的未完成网络请求数量为100个,这些请求与
registerNetworkCallback(NetworkRequest, PendingIntent)
及其变体、requestNetwork(NetworkRequest, PendingIntent)
以及ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback
共享,以避免由于应用程序泄漏回调而导致的性能问题。如果超出了限制,将抛出异常。重要的是取消注册这些回调以避免此问题,并节省资源。
让我们看看所要实现的效果
使用Compose实现代码如下:
@Composable
fun ConnectivityUiView(isOnline: Boolean) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopCenter,
) {
val msgStr = stringResource(
id = if (isOnline) {
R.string.internet_back_online_msg
} else {
R.string.you_are_offline_msg
}
)
val bgColor = if (isOnline) JunglesGreen else Color.Gray
Text(
text = msgStr,
modifier = Modifier
.fillMaxWidth()
.background(bgColor)
.padding(4.dp),
style = TextStyle(color = Color.White),
textAlign = TextAlign.Center
)
}
}
接下来是在之前创建的 MyConnectivityManager
类中创建另一个回调函数。
class MyConnectivityManager(context: Context) {
...
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network : Network) {
mCallback?.onLost()
}
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
&& networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
mCallback?.onConnected()
}
}
})
private var mCallback: Callback? = null
fun setCallback(callback: Callback) {
mCallback = callback
}
...
interface Callback {
fun onConnected()
fun onLost()
}
}
如果你注意到,当具备 NET_CAPABILITY_INTERNET
和 NET_CAPABILITY_VALIDATED
这两种能力时,会调用 mCallback?.onConnected()
回调函数。
为什么不在 onAvailable()
方法中调用呢?答案是,在使用 registerDefaultNetworkCallback()
时,onAvailable()
方法将立即紧随一个对 onCapabilitiesChanged()
的调用。另外,建议不要在这个回调函数中调用 ConnectivityManager.getNetworkCapabilities(android.net.Network)
方法,因为它容易出现竞态条件。
现在我们可以注册 MyConnectivityManager.Callback
来接收通知,无论是 onConnected()
还是 onLost()
。
class MainActivity : ComponentActivity() {
private val myConnectivityManager by lazy { MyConnectivityManager(this) }
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
var isOnline by remember { mutableStateOf(myConnectivityManager.isConnected()) }
val myConnectivityManagerCallback = object : MyConnectivityManager.Callback {
override fun onConnected() {
isOnline = true
}
override fun onLost() {
isOnline = false
}
}
myConnectivityManager.setCallback(myConnectivityManagerCallback)
ConnectivityUiView(isOnline)
}
}
override fun onResume() {
...
myConnectivityManager.subscribe()
}
override fun onStop() {
...
myConnectivityManager.unsubscribe()
/*
Important! This was only for demo purposes.
It's better to unsubscribe inside the onPause() method instead of onStop()
to receive the callback only when the UI is visible.
*/
}
}
然而,有一种情况下,当应用程序恢复(onResume()
)时,界面没有根据最后的网络状态进行更新,这是因为我们刚刚订阅了 NetworkCallback
,并且还没有最新的回调来更新界面。
为了解决这个问题,我们可以通过添加一些“调整”来处理:
fun subscribe() {
connectivityManager.registerDefaultNetworkCallback(networkCallback)
// everytime an activity or fragment subscribe, we send them the latest state of connectivity
if (isConnected()) {
mCallback?.onConnected()
} else {
mCallback?.onLost()
}
}
如今的 Android 开发有很多工具使代码更可读、易维护等等。正因为如此,我们将使用 Flow(开心)。
首先,我们将创建一个 Flow,在网络条件下发出 true 或 false。这将取代 mCallback?.onConnected()
和 mCallback?.onLost()
的调用,因为:
每天一个 Flow,远离“回调地狱”。
创建 Flow 有几种方式,但我们将使用 callbackFlow {}
并将其命名为 _connectionFlow
。
private val _connectionFlow = callbackFlow {
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network) {
trySend(false)
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
&& networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
trySend(true)
}
}
}
subscribe(networkCallback)
awaitClose {
unsubscribe(networkCallback)
}
}
正如你所看到的,我们使用 trySend()
,因为只有在收集器准备好时才需要发送值。另一个选项是在想要发送值并愿意挂起直到可以传递时使用 send()
。
还要注意的是,我们现在在 Flow 块内部订阅了 networkCallback
,并在使用 awaitClose {}
时取消订阅 networkCallback
。这是重要的,以确保在不需要订阅时不浪费资源。
上面定义的是一个冷 Flow 实例,这意味着每当将终端操作符应用于结果流时,块都会被调用一次。
现在我们已经在 flow {...}
块中调用了 subscribe()
和 unsubscribe()
,所以在 onResume()
和 onPause()
中不再需要调用它们。太好了!保持 DRY,以备将来使用 ✨
接下来,我们将使用 stateIn
将这个冷 Flow 转换成热 Flow(即 StateFlow
)。
class MyConnectivityManager(context: Context, private val externalScope: CoroutineScope) {
val connectionStateFlow: StateFlow<Boolean>
get() = _connectionStateFlow
.stateIn(
scope = externalScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = isConnected
)
...
}
实际上,还有另一种选择可以使用 shareIn()
。然而,我认为也许在将来我们可能需要访问最后的网络状态,在这种情况下,使用 StateFlow
更加方便,因为我们可以通过 connectionStateFlow.value
轻松访问它。
将其转换为热 Flow 的原因是我们可以将值从冷上游流多播到多个收集器中。冷流只能有一个订阅者,任何新的订阅者都会创建一个新的flow {..}
执行。通过使用热流,我们可以提高性能,因为它们始终处于活动状态,无论是否有观察者,都可以发出数据。
还有一个重要的注意点是热流共享开始的时机。在这种情况下,我们使用了 SharingStarted.WhileSubscribed(5000)
。这意味着共享在订阅期间开始,并在最后一个收集器取消订阅后活动状态保持 5 秒钟。
最后,我们如何在我们的 Compose UI 中实现它呢?
class MainActivity : ComponentActivity() {
private val myConnectivityManager by lazy { MyConnectivityManager(this, lifecycleScope) }
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
val connectionState by myConnectivityManager.connectionStateFlow
.collectAsStateWithLifecycle()
ConnectivityUiView(connectionState)
}
}
}
最终实现的MyConnectivityManager
版本如下:
//MyConnectivityManager.kt
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.stateIn
/**
* Created by meyta.taliti on 23/09/23.
*/
class MyConnectivityManager(context: Context, private val externalScope: CoroutineScope) {
private val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
val connectionAsStateFlow: StateFlow<Boolean>
get() = _connectionFlow
.stateIn(
scope = externalScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = isConnected
)
private val _connectionFlow = callbackFlow {
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network : Network) {
trySend(false)
}
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
&& networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
trySend(true)
}
}
}
subscribe(networkCallback)
awaitClose {
unsubscribe(networkCallback)
}
}
private val isConnected: Boolean
get() {
val activeNetwork = connectivityManager.activeNetwork
return if (activeNetwork == null) {
false
} else {
val netCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
(netCapabilities != null
&& netCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
&& netCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
}
}
private fun subscribe(networkCallback: ConnectivityManager.NetworkCallback) {
connectivityManager.registerDefaultNetworkCallback(networkCallback)
}
private fun unsubscribe(networkCallback: ConnectivityManager.NetworkCallback) {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}