补充篇 8.1:Ktor/KMP 断网处理:为什么请求前要先判断网络状态?

发布时间:2026/8/22 17:26:59
补充篇 8.1:Ktor/KMP 断网处理:为什么请求前要先判断网络状态? 上一篇我们已经建立了完整的网络异常链路Request ↓ Network / Timeout ↓ HTTP ↓ JSON ↓ Business Code ↓ AppError但在讨论AppError.Network时会遇到一个非常实际的问题如果当前已经明确断网为什么还要把 Request 交给 HttpClient然后等 Engine 请求失败以前 Android Retrofit OkHttp 项目中经常会在 Interceptor 中Request ↓ Interceptor ↓ 检查网络状态 ↓ 没有网络 ↓ 直接抛 NoNetworkException ↓ 不再 chain.proceed()也就是说已经明确知道当前不具备网络请求条件就不要继续进入真正的 HTTP 请求链路。到了 Ktor/KMP这个思想仍然成立。但 KMP 还多了一层值得理解的设计平台网络状态监听 ↓ 持续修改 NetworkConnectivityProvider 内部状态 ↓ NetworkClient 长期持有同一个 Provider ↓ 每次 Request 读取 Provider 当前最新状态所以这一篇真正要讲清楚三个东西Network Monitor NetworkConnectivityProvider NetworkClient分别负责什么。一、先说结论断网应该有两道防线整体可以先记成第一道防线 ↓ 请求前 Pre-check ↓ 明确断网 ↓ Fail Fast 第二道防线 ↓ 真正 HTTP 请求 ↓ 网络过程中仍然可能失败 ↓ ExceptionMapper也就是Pre-check 负责快速失败真实网络异常负责最终兜底。完整结构ApiService ↓ NetworkClient ↓ NetworkConnectivityProvider ↓ 请求前 Pre-check / \ 无网络 有网络 ↓ ↓ AppError.Network HttpClient ↓ Engine ↓ 真正网络请求 ↓ 网络仍然可能发生变化 ↓ Throwable ↓ ExceptionMapper ↓ AppError二、为什么请求前检查有价值假设手机已经明确没有互联网。用户点击刷新如果完全不检查ViewModel ↓ Repository ↓ ApiService ↓ NetworkClient ↓ HttpClient ↓ Engine ↓ DNS / Connect / Socket ↓ 失败 ↓ Throwable ↓ ExceptionMapper ↓ AppError.Network最后当然也能得到网络不可用但是问题是在进入 HttpClient 之前我们其实已经知道这次请求大概率没有执行意义。所以可以提前Request ↓ Connectivity Pre-check ↓ 明确没网 ↓ 直接结束三、Pre-check 本质上就是 Fail Fast所谓Fail Fast就是已经能够确定当前操作不具备执行条件就尽早失败而不是继续经过后面整套流程。于是Request ↓ Pre-check ↓ Unavailable ↓ AppError.Network而不用继续HttpClient ↓ Engine ↓ DNS ↓ Connect ↓ Socket ↓ Timeout所以请求前网络检查最核心的价值是快速失败 避免无意义的网络执行链路四、“节约网络资源”应该怎么理解这里可以更准确一些。设备已经完全断网时底层本来就未必真的能把 HTTP 数据发送到公网。所以所谓节约网络资源并不只是少用了多少流量更主要是避免不必要的 Engine 工作 DNS 尝试 连接建立尝试 Socket 工作 Timeout 等待 无意义 Retry 额外日志 额外异常转换最终价值Fail Fast ↓ 减少无效工作 ↓ 降低等待时间 ↓ 改善用户体验五、这和以前 OkHttp Interceptor 的思想其实一样以前可能这样class NetworkInterceptor( private val networkChecker: NetworkChecker, ) : Interceptor { override fun intercept( chain: Interceptor.Chain, ): Response { if (!networkChecker.isConnected()) { throw NoNetworkException() } return chain.proceed( chain.request() ) } }真正应该迁移的不是Interceptor 这个 API而是它背后的职责真正执行网络之前 ↓ 检查必要条件 ↓ 条件不满足 ↓ 不继续执行所以到了 Ktor以前 ConnectivityManager ↓ OkHttp Interceptor ↓ chain.proceed() 现在 NetworkConnectivityProvider ↓ NetworkClient / Client Plugin ↓ HttpClient思想是一致的。六、NetworkConnectivityProvider 到底是什么这里是这一版需要重点补充的地方。不要把NetworkConnectivityProvider理解成创建 HttpClient 时传进去一个 Boolean。例如不是创建 Client ↓ networkAvailable true ↓ 以后永远 true更准确的是NetworkConnectivityProvider 是一个长期存在的状态提供对象。NetworkClient 持有的是Provider 对象引用而不是Provider 当时那一刻的值结构创建 NetworkConnectivityProvider ↓ 创建 NetworkClient ↓ NetworkClient 持有 Provider ↓ ↓ 平台网络状态不断变化 ↓ 持续更新 Provider 内部状态 ↓ 每次 Request ↓ 读取 Provider 当前最新状态这和我们前面讲过的LanguageProvider TenantProvider RobotProvider是同一种 Provider 思想。七、Provider 对象不变变的是内部状态例如class DefaultNetworkConnectivityProvider : NetworkConnectivityProvider { private var available: Boolean false override fun isNetworkAvailable(): Boolean { return available } fun updateNetworkAvailable( available: Boolean, ) { this.available available } }创建一次NetworkConnectivityProvider ANetworkClientclass NetworkClient( private val client: HttpClient, private val connectivityProvider: NetworkConnectivityProvider, )持有的始终是Provider A但 Provider A 内部可以available true ↓ available false ↓ available true所以Provider 对象 ↓ 没有换 Provider 内部状态 ↓ 持续变化八、看一个完整状态变化过程一开始Provider ↓ available trueRequest ANetworkClient ↓ 读取 Provider ↓ true ↓ 允许请求后来网络断开Android Connectivity Callback ↓ Provider.update(false)现在还是同一个 Provider ↓ available falseRequest BNetworkClient ↓ 读取同一个 Provider ↓ false ↓ 直接 AppError.Network网络恢复Connectivity Callback ↓ Provider.update(true)Request CNetworkClient ↓ 读取 Provider ↓ true ↓ 继续 HttpClient完整过程Provider Available Request A ↓ 允许 网络断开 Provider Unavailable Request B ↓ 阻止 网络恢复 Provider Available Request C ↓ 允许九、所以 Provider 本质上是一个状态源可以把它理解成NetworkConnectivityProvider ↓ 保存“当前网络状态”NetworkClient 每次请求问它“现在网络状态是什么”就像LanguageProvider ↓ “当前默认语言是什么”以及RobotProvider ↓ “当前操作机器人是谁”所以Provider 的价值就在于 Client 不需要重新创建但它每次都可以读取最新状态。这点非常重要。十、谁负责修改 Provider虽然 Provider 内部状态可以修改但不应该理解成任何地方都可以随便改例如不建议connectivityProvider.available false到处出现。更合理的职责应该是平台 Network Monitor ↓ 负责感知网络变化 NetworkConnectivityProvider ↓ 负责保存当前状态 NetworkClient ↓ 只读取也就是Writer ↓ 平台网络监听组件 State Holder ↓ NetworkConnectivityProvider Reader ↓ NetworkClient这样数据流更清晰。十一、Provider 对外最好只读commonMain 可以只暴露interface NetworkConnectivityProvider { val isNetworkAvailable: Boolean }实现class DefaultNetworkConnectivityProvider : NetworkConnectivityProvider { private var _isNetworkAvailable false override val isNetworkAvailable: Boolean get() _isNetworkAvailable fun update( available: Boolean, ) { _isNetworkAvailable available } }这样NetworkClient ↓ 只能读 Network Monitor ↓ 负责 update避免整个项目随便修改共享状态。十二、正式一点甚至可以使用 StateFlow因为网络状态不仅 NetworkClient 需要。UI 也可能需要Offline Banner 网络恢复提示 重新加载按钮状态所以可以interface NetworkConnectivityProvider { val networkState: StateFlowNetworkState val isNetworkAvailable: Boolean }状态sealed interface NetworkState { data object Available : NetworkState data object Unavailable : NetworkState }实现概念class DefaultNetworkConnectivityProvider : NetworkConnectivityProvider { private val _networkState MutableStateFlowNetworkState( NetworkState.Unavailable ) override val networkState: StateFlowNetworkState get() _networkState override val isNetworkAvailable: Boolean get() _networkState.value NetworkState.Available fun update( state: NetworkState, ) { _networkState.value state } }于是平台 Network Monitor ↓ update ↓ NetworkConnectivityProvider │ ├── NetworkClient │ ↓ │ 请求前读取 │ └── UI ↓ collect 状态十三、Provider 不是每次请求都去联网测试这个非常重要。不要理解成Request ↓ isNetworkAvailable() ↓ Ping 一个网站 ↓ 再发送业务请求这反而会增加额外网络请求 增加延迟 增加功耗而且测试服务器能访问 ≠ 业务服务器一定能访问更合理的是平台网络监听 ↓ 持续维护状态 ↓ Provider 保存最新值 Request ↓ 只读取 Provider 当前值也就是Monitor 主动更新 Request 被动读取十四、KMP 为什么特别适合 Provider 抽象因为各个平台获取网络状态的方法不同。概念上commonMain NetworkConnectivityProvider ↑ ┌─────────┼─────────┐ ↓ ↓ ↓ androidMain iosMain webMainAndroidConnectivityManager NetworkCapabilitiesiOSNWPathMonitorWebnavigator.onLine online / offline events但 NetworkClient 不需要知道这些平台 API。它只知道connectivityProvider .isNetworkAvailable这就是平台能力 ↓ Provider 抽象 ↓ commonMain 使用十五、Android 端到底在监听什么Android 可以通过ConnectivityManager获得Network NetworkCapabilities但有 Wi-Fi不等于能访问互联网比如手机连接 Wi-Fi ↓ 路由器没有公网所以仅判断TRANSPORT_WIFI是不够的。十六、INTERNET 和 VALIDATED 要区分Android 中NET_CAPABILITY_INTERNET更接近这个网络被配置成具有访问互联网的能力。而NET_CAPABILITY_VALIDATED表示系统已经对公网连接进行了实际验证。所以对于普通App ↓ HTTPS ↓ Backend这种公网 APIVALIDATED更接近我们需要的状态。可以先记Wi-Fi Connected ≠ Internet Available而是Network ↓ INTERNET ↓ VALIDATED逐渐接近真正公网可用。十七、Android Provider 可以如何更新不要每次 Request 都connectivityManager.activeNetwork ↓ 重新查询一大堆状态更正式的做法可以是ConnectivityManager ↓ NetworkCallback ↓ 网络变化事件 ↓ 更新 Provider例如onAvailable ↓ 更新状态 onCapabilitiesChanged ↓ 重新判断 VALIDATED onLost ↓ 更新 Unavailable于是Android Network Monitor ↓ 持续 update ProviderNetworkClient只读取这正好符合Writer / State Holder / Reader的架构。十八、Android 结构可以这样理解ConnectivityManager ↓ NetworkCallback ↓ NetworkCapabilities ↓ 判断当前互联网状态 ↓ provider.update(...) ↓ NetworkConnectivityProvider ↓ NetworkClient而不是NetworkClient ↓ 直接操作 ConnectivityManager因为后者会把 commonMain 网络层和 Android 平台 API 耦合。十九、iOS 也是同一个思想iOSNWPathMonitor ↓ 网络状态变化 ↓ 更新 ProviderNetworkClientNetworkConnectivityProvider ↓ 读取当前状态所以Android ConnectivityManager iOS NWPathMonitor Web online/offline最终做的事情其实都是平台变化 ↓ 修改 Provider 当前状态二十、Web 也可以监听状态变化Webonline offline事件变化时Browser Event ↓ Provider.update(...)RequestNetworkClient ↓ 读取 Provider但 Web 的navigator.onLine只是一个比较弱的网络状态信号。所以依然要记住Provider Available并不等于API 一定请求成功二十一、所以 Provider 提供的到底是什么更准确的说法不是“服务器一定可访问。”而是根据当前平台维护的网络状态现在是否具备尝试网络请求的条件。也就是Unavailable ↓ 明确不值得尝试 Available ↓ 可以尝试 ↓ 但不保证成功因此NetworkConnectivityProvider本质上提供的是Current Network State而不是Future HTTP Result二十二、NetworkClient 如何使用 Provider例如class NetworkClient( private val client: HttpClient, private val connectivityProvider: NetworkConnectivityProvider, )请求private fun ensureNetworkAvailable() { if ( !connectivityProvider .isNetworkAvailable ) { throw NoNetworkException() } }然后Request ↓ ensureNetworkAvailable() ↓ 读取 Provider 当前状态注意Provider可能刚刚被平台监听组件更新。所以 NetworkClient 每次拿到的是当前值而不是创建 NetworkClient 时的旧值二十三、这和我们前面讲 Provider 动态值完全一致前面LanguageProvider ↓ 当前默认语言例如zh-CN ↓ 切换 ↓ en-USNetworkClient 每次请求重新读取当前 Language现在NetworkConnectivityProvider ↓ 当前网络状态例如Available ↓ 网络断开 ↓ Unavailable ↓ 恢复 ↓ AvailableNetworkClient 每次请求也重新读取当前 Network State所以 Provider 可以统一理解为一个长期存在、内部状态可以动态变化的数据来源。二十四、NetworkClient 不需要重新创建这点非常关键。假设NetworkClient A创建时Provider Available后来Provider Unavailable不需要销毁 NetworkClient A ↓ 重新创建 NetworkClient B因为 NetworkClient 持有的是Provider 引用下一次读取自然就是Unavailable所以同一个 NetworkClient 同一个 Provider 变化的只是 Provider 内部状态二十五、完整生命周期可以这样画App 启动 ↓ 创建 NetworkConnectivityProvider ↓ 创建平台 NetworkMonitor ↓ 创建 NetworkClient ↓ NetworkClient 持有 Provider ──────── 运行期间 ──────── 网络可用 ↓ Monitor ↓ Provider Available Request A ↓ 读取 Available ↓ 请求 网络断开 ↓ Monitor ↓ Provider Unavailable Request B ↓ 读取 Unavailable ↓ Fail Fast 网络恢复 ↓ Monitor ↓ Provider Available Request C ↓ 读取 Available ↓ 请求整个过程中NetworkClient ↓ 始终没有重建二十六、为什么 Pre-check 仍然不能代替异常处理因为 Provider 保存的本质上只是当前网络状态而网络是动态的。例如10:00:00.000 Provider AvailableRequest 读取Available然后10:00:00.050 Wi-Fi 突然断开这时候 Request 已经进入HttpClient ↓ EngineProvider 前面的判断已经完成了。所以还是可能Connection Error Socket Error DNS Error这就是典型的检查时正常 ≠ 执行时一定正常二十七、所以 Provider 和 ExceptionMapper 职责不同可以这样记NetworkConnectivityProvider ↓ 现在是否值得尝试 HttpClient / Engine ↓ 真正执行 ExceptionMapper ↓ 实际失败后属于什么错误三者不能互相替代。二十八、完整的两道防线Request ↓ NetworkConnectivityProvider ↓ 当前状态 / \ Unavailable Available ↓ ↓ Fail Fast HttpClient ↓ ↓ Network Error Engine ↓ 真正执行请求 ↓ 网络仍可能突然发生变化 ↓ Throwable ↓ ExceptionMapper ↓ AppError所以Provider 解决的是当前状态判断ExceptionMapper 解决的是真实执行失败。二十九、Pre-check 失败应该怎么进入 AppError例如if ( !connectivityProvider .isNetworkAvailable ) { throw NoNetworkException() }然后NoNetworkException ↓ ExceptionMapper ↓ AppError.Network这种方式很好理解。于是Pre-check 发现断网 和 Engine 网络失败最终都可以AppError.Network业务层不需要区分来源。三十、Timeout 和断网仍然要分开如果Provider Unavailable那么直接 Network Error甚至HttpClient 都没执行自然也不存在Connect Timeout Socket Timeout Request Timeout而如果Provider Available ↓ 请求进入 Engine ↓ 等待超过时间才会成为AppError.Timeout所以明确断网 ↓ Network 等待超时 ↓ Timeout不要混。三十一、Retry 与 Provider 的关系假设Request ↓ 失败 ↓ 准备 Retry如果此时Provider Unavailable那继续Retry 1 Retry 2 Retry 3通常没有意义。所以 Retry 策略可以考虑准备下一次 Retry ↓ 重新查看 Connectivity State但这里需要特别注意如果 Pre-check 只在 NetworkClient 最外层执行一次而 Retry 是 Ktor HttpRequestRetry 在 HttpClient 内部完成那么 Retry 不一定重新经过 NetworkClient 的 Pre-check。因此每次 Retry 前 是否重新检查 Provider属于后面HttpRequestRetry设计时要继续解决的问题。三十二、Pre-check 放 NetworkClient 还是 Client Plugin现在有两个方向。方案一NetworkClientApiService ↓ NetworkClient ↓ Provider ↓ HttpClient优点简单 清晰 容易和 AppError 体系结合当前阶段非常合适。方案二Ktor Client Plugin可以把 Connectivity 检查放到HttpClient Plugin某个请求阶段。概念上HttpClient ↓ Connectivity Plugin ↓ Provider ↓ Unavailable ↓ 阻止继续发送它更接近以前OkHttp Interceptor但 Ktor Plugin 的Hook / Pipeline和 OkHttpchain.proceed()并不是一模一样。所以现阶段不用急着把 Connectivity 做成 Plugin。三十三、当前阶段更推荐 NetworkClient因为我们现在已经有ApiService ↓ NetworkClient ↓ HttpClient那么executeRequest() ↓ Pre-check非常自然。先把职责 生命周期 状态来源搞清楚。以后真正学Custom Plugin Pipeline Hook再决定是否下沉。三十四、还有一个很重要的问题公网和局域网不是一回事假设Android 平板 ↓ Wi-Fi ↓ 机器人底盘这个 Wi-Fi没有公网Android 可能没有 VALIDATED但是192.168.1.100机器人完全可以访问。所以如果简单没有 Internet ↓ 所有 Request 都拦截就会出错。因此Internet Connectivity和Local Network Connectivity必须区分。三十五、Connectivity 也存在作用域例如apiClient ↓ 公网 Backend uploadClient ↓ 公网 Upload Server robotLocalClient ↓ 局域网 Robot那么apiClient ↓ Internet Pre-check而robotLocalClient ↓ 不能简单使用 Internet VALIDATED 判断这和前面我们反复讲的公共配置也有作用域完全一样。三十六、未来 NetworkConfig 甚至可以描述网络要求比如enum class ConnectivityRequirement { Internet, LocalNetwork, None, }然后data class NetworkConfig( val baseUrl: String, val connectivityRequirement: ConnectivityRequirement, )于是api ↓ Internet robotLocal ↓ LocalNetworkNetworkClient 根据不同 Client 的职责采取不同 Pre-check。不过普通 HTTPS 项目暂时不需要设计这么复杂。三十七、为什么接口不要叫 isWifiConnected()因为网络请求可能通过Wi-Fi Mobile Ethernet VPN Local Network所以真正关注的是当前请求所需要的网络能力而不是是不是连接 Wi-Fi所以NetworkConnectivityProvider比WifiChecker更加合理。三十八、网络恢复以后 Provider 会怎么变化例如Unavailable ↓ 网络恢复 ↓ 平台 Monitor 收到变化 ↓ Provider.update(Available)以后新的 Request读取 Available ↓ 允许执行这就是 Provider 持续动态更新的价值。但网络恢复不等于之前所有失败请求自动重发是否 Retry 仍然需要判断GET POST 幂等性 业务状态特别是支付 下单 机器人控制指令不能简单自动重放。三十九、所以 Network Monitor 和 NetworkClient 要彻底分开一个非常清晰的结构是平台 NetworkMonitor ↓ 感知网络状态变化 ↓ update ↓ NetworkConnectivityProvider ↑ ↑ │ │ NetworkClient UI 读取 collect也就是说Monitor ↓ 负责写 Provider ↓ 负责存 NetworkClient / UI ↓ 负责读这就是很典型的单向状态流。四十、NetworkConnectivityProvider 和 DefaultRequest Provider 的共同点现在其实可以把整个 Provider 思想串起来。LanguageProvider用户切换语言 ↓ Provider 内部状态改变 ↓ 后续 Request 读取新语言TenantProvider当前 Tenant 变化 ↓ Provider 状态改变 ↓ 后续 Request 读取新 TenantNetworkConnectivityProvider网络变化 ↓ Provider 状态改变 ↓ 后续 Request 读取新网络状态共同点Client 长期持有 Provider而 Provider 长期持有并更新当前状态。所以Provider不是初始化参数快照而是动态状态来源四十一、但是 Request 特殊值和 ConnectivityProvider 又不同前面的 LanguageProvider 默认 zh-CN某一次 Request临时 en-US可以 Request 级覆盖。但是 Connectivity当前明确 Offline通常不是这个 Request 写一个 Header 就能覆盖因为这是执行条件而不是请求参数所以 Provider 虽然思想相似但具体业务语义不同。四十二、完整请求生命周期最终可以画成App 启动 ↓ 创建 NetworkConnectivityProvider ↓ 启动 Platform NetworkMonitor ↓ 创建 NetworkClient ↓ NetworkClient 持有 Provider ↓ ────── 运行期间 ────── NetworkMonitor ↓ 不断更新 Provider ↓ Request ↓ NetworkClient ↓ 读取 Provider 当前状态 ↓ ┌─────────────┐ │ │ Offline Online ↓ ↓ Fail Fast HttpClient ↓ Engine ↓ 真正请求 ↓ Throwable / Response这里最重要的生命周期关系是NetworkClient ↓ 长期存在 Provider ↓ 长期存在 Provider State ↓ 持续变化 Request ↓ 一次性四十三、完整 KMP Connectivity 架构commonMain NetworkConnectivityProvider ↑ │ 当前网络状态 State ↑ ┌────────────────┼────────────────┐ │ │ │ androidMain iosMain webMain │ │ │ ConnectivityManager NWPathMonitor Browser Events NetworkCallback online/offline │ │ │ └────────────────┼────────────────┘ ↓ 更新 Provider State ↓ ┌────────────┴────────────┐ ↓ ↓ NetworkClient UI ↓ ↓ Pre-check 网络状态展示 ↓ HttpClient ↓ Engine ↓ HTTP这时候职责就非常清楚了。四十四、本篇总结断网处理不能只理解成Request ↓ HttpClient ↓ 请求失败 ↓ catch Exception更完整的设计是平台网络状态监听 ↓ 持续更新 NetworkConnectivityProvider ↓ NetworkClient 每次 Request 读取 Provider 当前最新状态如果Provider Unavailable则Fail Fast ↓ 不进入 HttpClient / Engine如果Provider Available只能说明当前值得尝试请求真正能否成功仍然由 HttpClient / Engine 实际执行决定所以NetworkConnectivityProvider ↓ 解决当前状态 HttpClient / Engine ↓ 解决实际执行 ExceptionMapper ↓ 解决失败分类三者职责不同。另外Provider 的生命周期也必须理解正确NetworkClient ↓ 长期持有同一个 Provider Provider ↓ 对象通常长期存在 Provider 内部 NetworkState ↓ 可以不断变化也就是说NetworkClient 持有的不是“创建时的网络状态值”而是一个可持续更新的 NetworkConnectivityProvider 对象平台网络监听负责修改 Provider 内部状态NetworkClient 每次请求读取当前最新状态。这也是 Provider 在整个 KMP 网络架构中的核心价值。进一步还要记住公网 Connectivity ≠ 局域网 Connectivity所以对于普通 HTTPS Backend可以使用Internet Pre-check但对于机器人局域网 IoT 设备 局域网服务必须根据 Client 实际网络职责设计 Connectivity 策略不能简单用Internet Unavailable一刀切阻止所有网络请求。回到主线下一篇《Ktor Plugin 实战Logging、HttpTimeout 与 HttpRequestRetry》下一篇继续把Logging HttpTimeout HttpRequestRetry三个正式项目常用能力连接起来。重点会讲HttpTimeout ↓ Request / Connect / Socket 到底分别控制哪一段 HttpRequestRetry ↓ 一次失败为什么可以重新发送 Provider ↓ 第一次请求时 Available Retry 前 ↓ 如果 Provider 已经变成 Unavailable 怎么办 GET ↓ 为什么更容易安全 Retry POST ↓ 为什么 Retry 必须考虑幂等性并把目前已经建立的Pre-check ↓ 真实网络异常 ↓ Timeout ↓ Retry正式串成一套完整的网络可靠性策略。