在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 networkval 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.GrayText(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? = nullfun 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 connectivityif (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: Booleanget() {val activeNetwork = connectivityManager.activeNetworkreturn 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)}
}