Jetpack Compose中判断和监听网络连接状态

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose中判断和监听网络连接状态相关的知识,希望对你有一定的参考价值。

要判断和监听网络连接状态,要使用的系统服务是 ConnectivityManager 对象,可以通过 context.getSystemService(Context.CONNECTIVITY_SERVICE) 来获得。所以首先可以定义一个Context的扩展属性来方便的获取该对象:

private val Context.connectivityManager: ConnectivityManager
    get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

判断网络瞬时连接状态

也就是判断当前那一刻的连接状态,一般用于请求后台接口前的检查准备工作。

  • 如果是在android 6.0 以下(API < 23)系统版本,可以使用 ConnectivityManagergetActiveNetworkInfo()方法先拿到返回的NetworkInfo对象,然后调用NetworkInfoisConnected()方法来判断。
  • 如果是在Android 6.0 +API >= 23)系统版本,可以先调用 ConnectivityManagergetActiveNetwork()方法返回一个Network对象,然后调用 ConnectivityManagergetNetworkCapabilities(Network),会返回一个 NetworkCapabilities 对象,再调用NetworkCapabilitieshasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)判断即可。

我们可以为 ConnectivityManager 定义一个扩展属性来实现上面的判断逻辑:

private val ConnectivityManager.isNetWorkConnected
    get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 
                val networkCapabilities = getNetworkCapabilities(activeNetwork)
                networkCapabilities?.hasCapability(NET_CAPABILITY_INTERNET) ?: false
             else 
                activeNetworkInfo?.isConnected ?: false
            

监听网络连接状态的变化

一种古老的办法是注册一个action为"android.net.conn.CONNECTIVITY_CHANGE"的动态广播来监听(Android7.0以上已经不允许在静态广播中监听该action了),在接受到该广播的时候去判断。但是这种办法已经不是一个较好的解决方案了,尤其在较高的版本中,Android官方已经不推荐这种做法了,甚至会屏蔽应用接受该类型的广播。目前官方推荐的方案是使用ConnectivityManager 对象进行注册一个Callback回调来监听,你可以使用ConnectivityManager.registerDefaultNetworkCallback(NetworkCallback)ConnectivityManager.registerNetworkCallback(NetworkCallback) 来实现。这两个注册方法可以结合使用。

但它们的用途各有不同。

所有 Android 应用都有一个默认网络。系统决定了哪个网络应是默认网络。系统通常首选不按流量计费的网络而非按流量计费的网络,首选网速较快的网络而非网速较慢的网络。当应用发出网络请求时,系统会使用默认网络满足该请求。应用也可以通过其他网络发送流量。

在应用的整个生命周期内,设置为默认网络的网络可能随时发生变化。典型的例子是设备处于一个已知活跃、不按流量计费、速度快于移动网络的 Wi-Fi 接入点的覆盖范围内。设备将连接到此接入点,并将所有应用的默认网络切换至新的 Wi-Fi 网络。

当新网络成为默认网络时,应用打开的任何新连接都会使用此网络。一段时间后,上一个默认网络上的所有剩余连接都将被强制终止。如果知道默认网络发生变化的时间对应用很重要,它应按如下方式注册默认网络回调:

connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() 
    override fun onAvailable(network : Network) 
        Log.e(TAG, "The default network is now: " + network)
    

    override fun onLost(network : Network) 
        Log.e(TAG, "The application no longer has a default network. The last default network was " + network)
    

    override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) 
        Log.e(TAG, "The default network changed capabilities: " + networkCapabilities)
    

    override fun onLinkPropertiesChanged(network : Network, linkProperties : LinkProperties) 
        Log.e(TAG, "The default network changed link properties: " + linkProperties)
    
)

注意:请勿在回调中调用同步方法来查找新可用网络的属性,因为这会受到竞态条件的影响。相反,应该等待对该网络的 onCapabilitiesChanged() 和 onLinkPropertiesChanged() 的调用,在搭载 Android 8.0(API 级别 26)及更高版本的设备上,在调用 onAvailable() 后会立即调用它们。

对于通过 registerDefaultNetworkCallback() 注册的回调,onLost() 表示网络失去成为默认网络的资格。网络不一定会断开。

默认情况下,回调方法会在应用的连接线程上被调用,这是 ConnectivityManager 使用的一个单独线程。如果回调的实现需要执行时间更长的工作,请使用变体 ConnectivityManager.registerDefaultNetworkCallback(NetworkCallback, Handler) 在单独的工作器线程上调用它们。

当不再需要使用回调时,请通过调用 ConnectivityManager.unregisterNetworkCallback(NetworkCallback) 来取消注册。

而调用 ConnectivityManager.registerNetworkCallback(NetworkRequest, NetworkCallback) 方法的流程与监听默认网络类似。主要区别在于,虽然在任何给定时间只能有一个默认网络应用于某个应用,但此方法可让您的应用同时看到所有可用网络,因此,它的 onLost(Network) 表示网络已永久断开连接,而非表示它不再是默认网络。

应用需要构建 NetworkRequest 以告知 ConnectivityManager 它想要监听的网络类型。例如,如果您的应用只希望使用不按流量计费的互联网连接:

val request = NetworkRequest.Builder()
  .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
  .addCapability(NET_CAPABILITY_INTERNET)
  .build()

connectivityManager.registerNetworkCallback(request, myNetworkCallback)

对于默认网络回调,可以调用接受 HandlerregisterNetworkCallback(NetworkRequest, NetworkCallback, Handler),这样就不会加载应用的 Connectivity 线程。如果回调不再使用,请调用 ConnectivityManager.unregisterNetworkCallback(NetworkCallback)。一个应用可以同时注册多个网络回调。

由于 ConnectivityManager.registerDefaultNetworkCallback(NetworkCallback) 只能在 Android 7.0+ (API >= 24) 以上才能使用,因此我们在 Android 7.0 以下只能使用另外一个registerNetworkCallback方法。当然,在Android 7.0+ 以上你可以选择同时进行两个注册方法的调用,它们可以同时存在,但是这样感觉判断情况会有点点复杂,因为当同时注册二者时,它们的回调之间的触发顺序有点微妙,可以参考官网读取网络状态的示例部分

为了简单方便起见,可以定义一个监听网络状态的接口:

interface ConnectivityObserver 
    fun observe(): Flow<Status>
    enum class Status  Available, Unavailable, Losing, Lost 

这里希望以流的方式返回网络枚举状态值进行监听。

下面定义一个 NetWorkManager 类实现该接口:

class NetWorkManager(private val context: Context): ConnectivityObserver 

    private val manager = context.applicationContext.connectivityManager

    fun isNetWorkConnected() = manager.isNetWorkConnected // 瞬时连接状态查询

    override fun observe(): Flow<ConnectivityObserver.Status>  
        return callbackFlow 
            val callback = object : ConnectivityManager.NetworkCallback() 
                override fun onAvailable(network: Network) 
                    launch  send(ConnectivityObserver.Status.Available) 
                    showToast("onAvailable")
                

                override fun onLosing(network: Network, maxMsToLive: Int) 
                    launch  send(ConnectivityObserver.Status.Losing) 
                    showToast("onLosing")
                

                override fun onLost(network: Network) 
                    launch  send(ConnectivityObserver.Status.Lost) 
                    showToast("onLost")
                

                override fun onUnavailable() 
                    launch  send(ConnectivityObserver.Status.Unavailable) 
                    showToast("onUnavailable")
                 
            
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) 
                manager.registerDefaultNetworkCallback(callback)
             else 
                val request = NetworkRequest.Builder()
                    .addCapability(NET_CAPABILITY_INTERNET)
                    .build()
                manager.registerNetworkCallback(request, callback)
            
            awaitClose  manager.unregisterNetworkCallback(callback) 
        .distinctUntilChanged()
    

    fun showToast(str: String) 
        context.showToast(str)
    

上面代码中在 7.0 以上使用 registerDefaultNetworkCallback注册监听,7.0 以下使用registerNetworkCallback注册监听,注意到registerNetworkCallback需要传递一个NetworkRequest对象,在构造该对象时设置了一个NET_CAPABILITY_INTERNET能力,表示该网络可以连接访问互联网,可以自行查阅NetworkCapabilities类获取其他感兴趣的能力进行添加。并且在 awaitClose 中进行了反注册,这样当流所在的协程被取消时会自动执行反注册。

这里只监听了4种方法 onAvailableonLosingonLostonUnavailable,如果感兴趣可以覆写其他方法进行监听。

我们希望 NetWorkManager 全局只有一个实例,并且它使用 applicationContext 来获取 connectivityManager对象,因此可以在Application类中定义一个成员属性,并使用 lazy 语法来设置延迟加载:

class MyApp: Application() 
    val netWorkManager by lazy  NetWorkManager(this) 
    
	override fun onCreate() 
        super.onCreate() 
    
 

然后就可以在ActivityComposable 进行观察网络状态:

class NetWorkTestActivity: ComponentActivity() 

    private val netWorkManager by lazy  (application as MyApp).netWorkManager 

    @OptIn(ExperimentalLifecycleComposeApi::class)
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        if (netWorkManager.isNetWorkConnected()) 
            showToast("isNetWorkConnected()")
        
        setContent 
            MyComposeApplicationTheme 
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) 
                    val status by netWorkManager.observe()
                        .collectAsStateWithLifecycle(ConnectivityObserver.Status.Unavailable)
                    Column(horizontalAlignment = Alignment.CenterHorizontally) 
                        Text("isNetWorkConnected: $netWorkManager.isNetWorkConnected()")
                        Text("status: $status")
                    
                
            
         
    

由于 Activity 中非 Composable 的其他部分也可能使用网络状态,因此 netWorkManager 设置为了 Activity 的属性。如果是在低级别的 Composable函数中使用,可以通过 LocalContext.current 来获取:

val netWorkManager = (LocalContext.current.applicationContext as MyApp).netWorkManager

在上面 Activity 中的 Composable函数里,调用了netWorkManager.observe()方法,然后对其返回的StateFlow调用collectAsStateWithLifecycle()转换为Composable可以观察的状态。该方法使得Composable在安全的声明周期内调用StateFlow收集数据。

同时上面还调用了 netWorkManager.isNetWorkConnected() 查询瞬时连接状态,因为瞬时状态查询后值是固定的,不会自动变化,但是这里当监听网络状态的status值改变时会触发重组作用域进行重组(Column组件是inline的),因此重组时会再次调用到netWorkManager.isNetWorkConnected() ,所以当网络开关切换时,界面上的两个状态都会发生变化。

监听网络连接是否是使用数据流量

要判断当前网络是否为计费流量的网络连接,可以使用 ConnectivityManagerisActiveNetworkMetered() 方法,这个也是一次性的判断,可以在 NetWorkManager 类中定义一个方法:

class NetWorkManager(private val context: Context): ConnectivityObserver 

    private val manager = context.applicationContext.connectivityManager 

    private val isMeteredFlow: MutableStateFlow<Boolean> = MutableStateFlow(manager.isActiveNetworkMetered)
    fun isMetered(): StateFlow<Boolean> = isMeteredFlow.asStateFlow()
 	......
 

当网络切换时,我们希望外部调用者能持续观察当前新的网络是否为计费流量的网络连接。因此这里使用 MutableStateFlow 来存储状态值,并将ConnectivityManager.isActiveNetworkMetered的结果作为其初始值。

然后在 ConnectivityManager.NetworkCallback 中覆写 onCapabilitiesChanged() 方法,当网络连接的能力发生变化时,会回调该方法,只需要在该方法中判断返回的 networkCapabilities是否具备NetworkCapabilities.NET_CAPABILITY_NOT_METERED这个属性即可(该值表示是否为非流量计费网络),然后将新的值设置给保存结果的 MutableStateFlow 对象,这样外部观察者就能接收到变化。

class NetWorkManager(private val context: Context): ConnectivityObserver 
	......
    private val isMeteredFlow: MutableStateFlow<Boolean> = MutableStateFlow(manager.isActiveNetworkMetered)
    fun isMetered(): StateFlow<Boolean> = isMeteredFlow.asStateFlow()

    override fun observe(): Flow<ConnectivityObserver.Status> 

        return callbackFlow 
            val callback = object : ConnectivityManager.NetworkCallback() 
                ......
                override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) 
                    val notMetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
                    isMeteredFlow.value = !notMetered
                    println("notMetered: ==========$notMetered")
                
            
           ......
        .distinctUntilChanged()
    

    fun showToast(str: String) 
        context.showToast(str)
    

最后,在 Composable 中使用:

 val status by netWorkManager.observe()
             .collectAsStateWithLifecycle(ConnectivityObserver.Status.Unavailable)
             
val isMetered by netWorkManager.isMetered().collectAsStateWithLifecycle()

Column(horizontalAlignment = Alignment.CenterHorizontally) 
     Text("isNetWorkConnected: $netWorkManager.isNetWorkConnected()")
     Text("status: $status")
     Text("是否正在使用移动流量:$isMetered") 
 

这里依然是使用 collectAsStateWithLifecycle() 将 StateFlow 安全地转换成 Composable 的可观察状态。

最后,别忘了在AndroidManifest.xml清单文件中添加查询网络状态相关的权限:

    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

以上是关于Jetpack Compose中判断和监听网络连接状态的主要内容,如果未能解决你的问题,请参考以下文章

Android jetpack compose中的按钮长按监听器

如何判断 jetpack-compose 分页是不是有效?

Jetpack Compose中的手势操作

Jetpack Compose 网络请求

在 Android 中输入/加载基于 Jetpack Compose 的屏幕时从网络或数据库获取数据

随便嵌套?Jetpack Compose 到底优秀在哪里