在Android中使用Flow获取网络连接信息

在Android中使用Flow获取网络连接信息_第1张图片

在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 共享,以避免由于应用程序泄漏回调而导致的性能问题。如果超出了限制,将抛出异常。重要的是取消注册这些回调以避免此问题,并节省资源。

让我们看看所要实现的效果

在Android中使用Flow获取网络连接信息_第2张图片
使用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_INTERNETNET_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.
        */
    }
}

在Android中使用Flow获取网络连接信息_第3张图片
然而,有一种情况下,当应用程序恢复(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 实例,这意味着每当将终端操作符应用于结果流时,块都会被调用一次。
在Android中使用Flow获取网络连接信息_第4张图片
现在我们已经在 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)
    }
}

你可能感兴趣的:(Kotlin进阶,android)