网络状态监听:ConnectivityManager使用、封装为Flow、在网络变化时自动重试请求

说实话,网络状态监听这个事,很多新手容易忽略。

你想想看,用户在地铁上刷App,信号断断续续的。如果我们的应用不做任何处理,请求失败就直接抛异常,体验能好吗?

我在项目中遇到过好几次这样的场景:用户反馈「为什么我明明有网,你们App却显示加载失败?」。排查下来,其实是网络刚恢复的那一瞬间,请求已经发出去了,但连接还没完全建立起来。

嗯,今天我们就来彻底搞定这个问题。

ConnectivityManager 的基本用法

Android 里监听网络状态,核心就是 ConnectivityManager。这个系统服务负责管理所有的网络连接。

获取它的方式很简单:

val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

但要注意,从 Android 7.0 开始,系统废弃了 NetworkInfo,推荐使用 NetworkCapabilities 来判断网络状态。

我个人的习惯是,写一个扩展函数来封装这个判断逻辑:

fun ConnectivityManager.isNetworkAvailable(): Boolean {
    val network = activeNetwork ?: return false
    val capabilities = getNetworkCapabilities(network) ?: return false
    return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}

这里有个坑,我曾经踩过:activeNetwork 可能为 null,即使 Wi-Fi 或移动数据是打开的。为什么?因为系统需要一点时间来建立连接。所以一定要做空安全判断。

注册网络状态回调

监听网络变化,传统做法是注册 BroadcastReceiver。但 Android 官方更推荐使用 NetworkCallback,因为它更高效、更精准。

val callback = object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) {
        // 网络可用
    }

    override fun onLost(network: Network) {
        // 网络断开
    }

    override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
        // 网络能力变化,比如从Wi-Fi切到移动数据
    }
}

connectivityManager.registerNetworkCallback(
    NetworkRequest.Builder().build(),
    callback
)

记得在不需要的时候取消注册:

connectivityManager.unregisterNetworkCallback(callback)

封装为 Flow:让网络状态变成响应式数据流

直接使用回调,在 MVVM 架构里有点别扭。ViewModel 需要持有 callback 引用,还要处理生命周期。

更好的方式,是把网络状态封装成一个 Flow。这样任何地方都可以 collect,而且天然支持协程取消。

来看看我的封装思路:

class NetworkMonitor(private val context: Context) {

    val networkState: Flow<NetworkStatus> = callbackFlow {
        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                trySend(NetworkStatus.Available)
            }

            override fun onLost(network: Network) {
                trySend(NetworkStatus.Unavailable)
            }

            override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
                val hasInternet = capabilities.hasCapability(
                    NetworkCapabilities.NET_CAPABILITY_INTERNET
                )
                trySend(if (hasInternet) NetworkStatus.Available else NetworkStatus.Unavailable)
            }
        }

        connectivityManager.registerNetworkCallback(
            NetworkRequest.Builder().build(),
            callback
        )

        // 发送初始状态
        val initialStatus = if (connectivityManager.isNetworkAvailable()) {
            NetworkStatus.Available
        } else {
            NetworkStatus.Unavailable
        }
        trySend(initialStatus)

        // 协程取消时自动取消注册
        awaitClose {
            connectivityManager.unregisterNetworkCallback(callback)
        }
    }
}

sealed class NetworkStatus {
    object Available : NetworkStatus()
    object Unavailable : NetworkStatus()
}

这里用了 callbackFlow,它是专门用来把回调式 API 转为 Flow 的。注意 trySend 而不是 send,因为回调可能发生在非协程上下文中。

小技巧: 如果你需要区分 Wi-Fi 和移动数据,可以在 NetworkStatus 里加一个枚举字段。我在做视频类App时,就根据网络类型决定是否自动播放。

网络变化时自动重试请求

好了,现在网络状态已经变成 Flow 了。怎么用它来自动重试呢?

核心思路是:当请求失败时,监听网络状态 Flow,等到网络恢复后自动重试。

我封装了一个 retryOnNetworkChange 操作符:

fun <T> Flow<T>.retryOnNetworkChange(
    networkMonitor: NetworkMonitor,
    maxRetries: Int = Int.MAX_VALUE
): Flow<T> = this.retryWhen { cause, attempt ->
    if (cause is IOException && attempt < maxRetries) {
        // 等待网络恢复
        networkMonitor.networkState
            .filter { it == NetworkStatus.Available }
            .first()
        true // 继续重试
    } else {
        false // 不重试
    }
}

使用起来非常简洁:

viewModelScope.launch {
    apiService.fetchData()
        .retryOnNetworkChange(networkMonitor, maxRetries = 3)
        .catch { e ->
            _uiState.value = UiState.Error("加载失败,请检查网络")
        }
        .collect { data ->
            _uiState.value = UiState.Success(data)
        }
}

这里有个细节:filter { it == NetworkStatus.Available }.first() 会挂起当前协程,直到网络恢复。不会占用任何 CPU 资源,非常优雅。

注意: 不要无限重试。我见过一个线上事故,某个接口因为服务器临时故障,客户端不断重试,结果把数据库打挂了。一定要设置 maxRetries,并且加上指数退避。

完整的架构图

下面这张图展示了整个数据流的走向:

网络状态监听与自动重试架构 ConnectivityManager 系统网络服务 NetworkMonitor 封装为 Flow Flow 网络状态流 API 请求 Retrofit + Coroutine 请求成功? (IOException) 返回数据 collect 接收 等待网络恢复

实际项目中的最佳实践

我在多个项目里用过这套方案,总结出几个要点:

  • 不要在主线程 collect:网络状态 Flow 虽然不耗时,但 collect 操作本身会挂起协程。记得用 viewModelScopelifecycleScope
  • 考虑冷启动场景:App 启动时,ConnectivityManager 可能还没准备好。我习惯在 Application 的 onCreate 里初始化 NetworkMonitor,并延迟 500ms 再发送初始状态。
  • 区分网络类型:如果用户用的是移动数据,下载大文件前应该弹个提示。可以在 NetworkStatus 里加上 isMetered 字段。
  • 测试要覆盖:用模拟器切换网络模式,或者用 NetworkCallback 的 mock 来测试重试逻辑。我曾经因为没测试「飞行模式切换」场景,上线后出了 bug。

核心要点:

  1. 使用 ConnectivityManager.NetworkCallback 替代 BroadcastReceiver
  2. 用 callbackFlow 将回调转为 Flow,自动管理生命周期
  3. retryWhen + filter + first 实现优雅的网络恢复重试
  4. 设置重试上限,避免无限重试导致服务器压力

好了,这套方案我用了两年多,线上表现很稳定。你可以在自己的项目里试试看,有问题随时调整。


公众号:蓝海资料掘金营,微信deep3321