ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第十二篇:完整 KMP + Ktor 网络架构:从 ApiService 到 NetworkClient、Provider、Plugin、Engine

第十二篇:完整 KMP + Ktor 网络架构:从 ApiService 到 NetworkClient、Provider、Plugin、Engine 前面十一篇我们已经陆续把 Ktor Client 网络层中的主要知识点拆开讲了一遍HttpClient Engine ContentNegotiation DefaultRequest Provider NetworkClient ApiResponseT AppError Connectivity Logging HttpTimeout HttpRequestRetry Custom Client Plugin Auth单独理解这些概念并不算特别困难。真正进入项目之后难的是另外一个问题这些东西最后到底应该怎么组合比如ApiService 和 NetworkClient 谁负责什么 Provider 为什么不能直接写成普通变量 Plugin 和 NetworkClient 有什么区别 为什么 HttpClient 要长期复用 什么时候需要多个 HttpClient Token、Language、NetworkState 应该放在哪 Android、iOS、Web 最后又是怎么落到不同 Engine 上的所以这一篇不再继续增加新的 Ktor API。我们把前面所有内容重新收回来最终形成一套完整的KMP Ktor 网络层架构。一、先看最终架构先不要急着看代码。把整个网络层压缩成一张图UI ↓ ViewModel ↓ Repository ↓ ApiService ↓ NetworkClient ↓ NetworkClients / \ ↓ ↓ apiClient refreshClient │ │ └──────┬───────┘ ↓ HttpClient │ ┌──────────────┼──────────────┐ ↓ ↓ ↓ Config Provider Plugin │ │ │ ↓ ↓ ↓ BaseUrl Token ContentNegotiation Timeout Language DefaultRequest ... Network Logging ... HttpTimeout Retry Auth Custom Plugin │ ↓ Engine ┌──────────┼──────────┐ ↓ ↓ ↓ Android iOS Web OkHttp Darwin JS/Wasm └──────────┼──────────┘ ↓ HTTP看起来组件很多但实际上只需要回答六个问题ApiService ↓ 我要调用哪个业务接口 NetworkClient ↓ 项目里的 HTTP 请求应该怎么统一执行 HttpClient ↓ 这一条网络通道拥有哪些能力 Provider ↓ 当前运行期状态是什么 Plugin ↓ HTTP 生命周期中需要增加什么横切能力 Engine ↓ 这个平台最终怎么真正发送 HTTP这六个问题就是整套架构的主线。二、ApiService描述“我要调用哪个接口”先从最靠近业务的一层开始。例如class UserApiService( private val networkClient: NetworkClient, ) { suspend fun getUser( userId: Long, ): User { return networkClient.get( path /users/$userId, ) } }订单模块class OrderApiService( private val networkClient: NetworkClient, ) { suspend fun getOrders(): ListOrder { return networkClient.get( path /orders, ) } suspend fun createOrder( request: CreateOrderRequest, ): Order { return networkClient.post( path /orders, body request, ) } }ApiService最核心的职责就是描述业务接口。它应该关心接口 Path Query 参数 Request DTO Response DTO 接口属于哪个业务模块例如UserApiService OrderApiService RobotApiService FileApiService而不应该关心Token 怎么刷新 Timeout 是多少 JSON 怎么解析 有没有网络 IOException 怎么映射 Android 用什么 Engine这些都不是 ApiService 的职责。三、NetworkClient项目自己的统一请求边界NetworkClient不是 Ktor 官方类。它是项目为了统一网络规则而增加的一层。比如业务代码如果直接大量使用client.get(...) client.post(...)很快就会出现UserApiService 自己判断网络 OrderApiService 自己解析 ApiResponse ProductApiService 自己 catch Timeout RobotApiService 自己处理业务 code最终每个 ApiService 都有一套网络逻辑。所以我们增加ApiService ↓ NetworkClient ↓ HttpClientNetworkClient负责项目自己的统一规则例如Connectivity Pre-check get / post / put / delete 统一封装 HttpResponse → ApiResponseT 业务 code 判断 data 解包 Throwable → AppError可以把它理解成Ktor HttpClient 提供通用 HTTP 能力NetworkClient 定义我们这个项目怎么使用这些能力。四、一个基础 NetworkClient假设后端统一返回{ code: 0, msg: , data: {} }对应Serializable data class ApiResponseT( val code: Int, val msg: String, val data: T? null, )NetworkClient 可以形成class NetworkClient( private val client: HttpClient, private val connectivityProvider: NetworkConnectivityProvider, private val exceptionMapper: ExceptionMapper, ) { suspend inline fun reified T get( path: String, noinline block: HttpRequestBuilder.() - Unit {}, ): T { return execute { client.get(path) { block() } } } suspend inline fun reified T, reified B post( path: String, body: B, noinline block: HttpRequestBuilder.() - Unit {}, ): T { return execute { client.post(path) { setBody(body) block() } } } suspend inline fun reified T execute( request: () - HttpResponse, ): T { if (!connectivityProvider.isNetworkAvailable) { throw AppException( AppError.Network ) } return try { val response request() val apiResponse response.bodyApiResponseT() if (apiResponse.code ! 0) { throw ApiException( code apiResponse.code, message apiResponse.msg, ) } apiResponse.data ?: throw ApiException( code -1, message Response data is null, ) } catch ( cancellation: CancellationException ) { throw cancellation } catch ( throwable: Throwable ) { throw AppException( exceptionMapper.map( throwable ) ) } } }这里最值得注意的是NetworkClient解决的是项目语义而不是所有网络能力。五、不要把 NetworkClient 写成 God Class随着项目增长很容易开始往里面塞Token Refresh Logging Timeout Retry JSON Header Cookie Signing Encryption Cache Connectivity 异常处理 上传 下载最后形成NetworkClient ├── everything └── everything else这就失去了分层意义。应该保持NetworkClient ↓ 项目级 Request 执行规则 Plugin ↓ HTTP 生命周期中的横切能力 Provider ↓ 运行期动态状态 HttpClient ↓ 组合这些能力一句话NetworkClient 管项目规则Plugin 管 HTTP 生命周期。六、HttpClient一条长期复用的“网络能力通道”Ktor 真正的核心对象还是HttpClient它不是简单的HTTP 请求工具更准确地说它代表一条配置完成、长期复用的网络能力通道。例如一个apiClient可能拥有BaseUrl 公共 Header JSON Logging Timeout Retry Bearer Auth Custom Plugin这些能力组合在一起构成apiClient所以不要每一次请求val client HttpClient() client.get(...) client.close()HttpClient 背后还有Engine 连接复用 Plugin 状态 Auth Token Cache Cookie Coroutine / 平台资源正常设计应该Application 创建 ↓ 创建 HttpClient ↓ 长期复用 ↓ Application 生命周期结束 ↓ close()七、长期复用不等于整个 App 只能有一个 Client这一点非常重要。我们一直强调HttpClient 要复用但并不意味着整个 App 必须只有一个 HttpClient更准确的规则是一个明确的网络配置域对应一个长期复用的 HttpClient。例如apiClient ↓ 普通业务 API refreshClient ↓ Token Refresh thirdPartyClient ↓ 第三方服务 uploadClient ↓ 特殊上传通道但是不要UserApi 一个 Client OrderApi 一个 Client ProductApi 一个 Client这通常属于过度拆分。八、为什么 apiClient 和 refreshClient 很适合拆开apiClient业务 API Bearer Auth AccessToken 401 Refresh 普通 Retry Policy而refreshClientRefresh API 不安装 Bearer Auth 不携带普通 AccessToken 不会再次触发自身 Refresh于是apiClient ↓ 401 ↓ refreshClient ↓ 获取新 Token ↓ apiClient 重发原 Request职责非常清楚。需要强调双 Client 是一种架构选择不是 Ktor Auth 的强制要求。Ktor 本身也支持单 Client 配合 Refresh Request 标记完成 Token 刷新。我们拆 Client是因为网络责任不同而不是因为Ktor 要求这么做九、NetworkClients统一管理不同网络通道如果项目存在api refresh甚至以后还有upload download thirdParty可以增加一层class NetworkClients( private val configs: NetworkConfigs, private val tokenProvider: TokenProvider, private val commonHeadersProvider: CommonHeadersProvider, private val connectivityProvider: NetworkConnectivityProvider, private val exceptionMapper: ExceptionMapper, ) { val refresh: NetworkClient by lazy { createRefreshNetworkClient() } val api: NetworkClient by lazy { createApiNetworkClient( refreshClient refresh, ) } }它的职责不是执行请求而是组织整个项目有哪些网络责任域。十、Config描述“这个 Client 是什么”例如data class NetworkConfig( val baseUrl: String, val requestTimeoutMillis: Long 15_000, val connectTimeoutMillis: Long 10_000, val socketTimeoutMillis: Long 15_000, )多个 Clientdata class NetworkConfigs( val api: NetworkConfig, val refresh: NetworkConfig, )Config 适合存BaseUrl Timeout 固定 Header Client 类型相关配置因为这些东西回答的是这个 Client 本身是什么十一、Provider描述“现在是什么”前面我们已经出现了很多 ProviderTokenProvider NetworkConnectivityProvider CommonHeadersProvider LanguageProvider TenantProvider它们本质上属于同一类设计一个长期存在、提供当前运行期状态的数据源。例如interface LanguageProvider { val currentLanguage: String }重点不是Provider 里面有一个变量而是HttpClient ↓ 长期持有 Provider 引用 Provider ↓ 状态可以继续变化 下一次 Request ↓ 重新读取最新状态所以Client 持有的是 Provider 引用不是创建 Client 那一刻的值。十二、Config、Provider、Request 三种作用域这也是前面整个系列非常重要的一套模型。可以记成Config ↓ 这个 Client 是什么 Provider ↓ 现在默认是什么 Request ↓ 这一笔请求我要什么例如Config BaseUrl https://api.example.comProviderLanguage zh-CN某一次特殊 RequestLanguage en-US于是Client Config ↓ 长期稳定 Provider ↓ 运行期变化 Request Override ↓ 一次性变化三个层级不会冲突。十三、Provider 不等于“谁都可以修改的全局变量”例如NetworkConnectivityProvider更合理的结构Platform NetworkMonitor ↓ Writer NetworkConnectivityProvider ↓ State Holder NetworkClient / Retry ↓ Reader对网络层只暴露interface NetworkConnectivityProvider { val isNetworkAvailable: Boolean }而真正的update(...)留在具体实现或网络监控模块内部。这样就不会变成ViewModel Repository ApiService ↓ 谁都可以随便改 NetworkStateProvider 是状态来源。不是全局可写变量。十四、Plugin把横切能力挂进 HttpClient 生命周期前面第十篇已经讲过Ktor ≠ 一个大号 OkHttp InterceptorKtor 更核心的模型是HttpClient Lifecycle Plugin Hook官方 PluginContentNegotiation DefaultRequest Logging HttpTimeout HttpRequestRetry AuthCustom PluginTrace Signing Encryption 特殊协议Plugin 适合解决跨多个 Request、并且与 HTTP 生命周期有关的能力。十五、Plugin 和 NetworkClient 怎么判断放哪这是项目里特别容易纠结的问题。可以使用一个简单判断如果逻辑是这个项目的 Response 必须先检查 code 再解包 data放NetworkClient因为这是项目业务协议如果逻辑是每个 Request 都要增加 TraceId放Plugin / DefaultRequest因为这是HTTP 横切能力如果逻辑是401 ↓ Refresh Token ↓ 重新发送放Auth Plugin因为它与Request / Response 生命周期直接相关。所以可以记NetworkClient 负责项目语义Plugin 负责生命周期横切能力。十六、HttpClient 最终就是这些能力的组合器例如fun createApiHttpClient( config: NetworkConfig, tokenProvider: TokenProvider, commonHeadersProvider: CommonHeadersProvider, connectivityProvider: NetworkConnectivityProvider, refreshClient: HttpClient, ): HttpClient { return createPlatformHttpClient { expectSuccess true install( ContentNegotiation ) { json( createJson() ) } install( DefaultRequest ) { url( config.baseUrl ) headers .appendIfNameAbsent( Language, commonHeadersProvider .language, ) headers .appendIfNameAbsent( App-Version, commonHeadersProvider .appVersion, ) } install( HttpRequestRetry ) { maxRetries 2 retryIf { request, response, - request.method HttpMethod.Get response.status.value in 500..599 connectivityProvider .isNetworkAvailable } exponentialDelay() } install( HttpTimeout ) { requestTimeoutMillis config .requestTimeoutMillis connectTimeoutMillis config .connectTimeoutMillis socketTimeoutMillis config .socketTimeoutMillis } install( Logging ) { logger createHttpLogger() level LogLevel.ALL sanitizeHeader { it HttpHeaders.Authorization } bodyFilter createSafeBodyFilter() } install(Auth) { bearer { loadTokens { tokenProvider .getBearerTokens() } refreshTokens { refreshAccessToken( oldTokens oldTokens, refreshClient refreshClient, tokenProvider tokenProvider, ) } } } } }这里最值得观察的已经不是某一个 API。而是HttpClient ↓ 把各种能力组合起来所以可以把HttpClient理解成网络能力容器。十七、refreshClient 配置明显更简单例如fun createRefreshHttpClient( config: NetworkConfig, ): HttpClient { return createPlatformHttpClient { expectSuccess true install( ContentNegotiation ) { json( createJson() ) } install( HttpTimeout ) { requestTimeoutMillis config .requestTimeoutMillis } install( Logging ) { logger createHttpLogger() bodyFilter createSafeBodyFilter() } // 不安装 Bearer Auth } }于是apiClient ↓ 业务认证通道 refreshClient ↓ 认证刷新通道两个 Client 的差异来自职责而不是为了拆而拆十八、哪些公共配置可以提取如果两个 Client 都需要ContentNegotiation Logging 基础 Timeout可以fun HttpClientConfig*. installCommonPlugins() { install( ContentNegotiation ) { json( createJson() ) } install( Logging ) { logger createHttpLogger() } }然后createPlatformHttpClient { installCommonPlugins() install(Auth) { ... } }但是不要为了“不要出现重复代码”把Auth Retry Policy BaseUrl 特殊 Header全部强行揉进一个万能 Factory。还是那句话抽公共但不要封死变化。十九、Engine真正执行 HTTP 的平台层前面的ApiService NetworkClient Provider Plugin HttpClient基本都可以放在commonMain真正来到平台差异的是Engine例如Android ↓ OkHttp Engine iOS ↓ Darwin Engine Web ↓ JS / Wasm 对应 Engine可以把它理解成Ktor 公共 HttpClient API ↓ Engine ↓ 平台底层网络实现所以Plugin 决定请求过程中做什么Engine 决定这个平台最终怎么把请求发出去。这是两个完全不同的层级。二十、KMP 到底应该共享什么比较理想的是commonMain ApiService NetworkClient NetworkConfig Provider Interface ApiResponse AppError ExceptionMapper JSON Config Auth Logic Retry Policy Plugin Config平台层androidMain / iosMain / wasmJsMain Engine Connectivity Monitor Secure Storage 平台日志实现 证书 / TLS 平台差异也就是业务网络规则共享真正的平台差异下沉。而不是Android 网络写一套 iOS 网络写一套 Web 网络再写一套二十一、expect / actual 应该用在哪里如果只是选择一个可以工作的 Engine不一定非要为了“用了 KMP”就写expect / actual但是如果不同平台确实有Android OkHttp 特殊配置 iOS Darwin 特殊配置 Web 网络限制 TLS / Certificate Proxy 平台网络监控那么expect / actual就非常合理。例如// commonMain expect fun createPlatformHttpClient( block: HttpClientConfig*.() - Unit, ): HttpClient然后androidMain ↓ OkHttp iosMain ↓ Darwin wasmJsMain ↓ 对应 Web Engine原则不是为了 expect/actual 而 expect/actual而是只把真正的平台差异放进去。二十二、Repository 又处在哪一层例如class UserRepository( private val userApiService: UserApiService, private val userLocalDataSource: UserLocalDataSource, ) { suspend fun getUser(): User { return userApiService .getUser() } }Repository 关心的是数据来自 Remote 来自 Local 是否需要 Cache 什么时候同步它不应该关心Authorization Header HttpTimeout Ktor Plugin OkHttp Engine Darwin Engine所以ViewModel ↓ Repository ↓ ApiService ↓ NetworkClient ↓ HttpClient每一层的问题是不一样的。二十三、一次正常请求到底怎么走例如userApiService .getUser( userId 1001 )流程UserApiService ↓ NetworkClient.get() ↓ Connectivity Pre-check ↓ apiClient ↓ DefaultRequest ↓ 读取 Provider 当前状态 ↓ Auth ↓ Logging ↓ HttpTimeout ↓ HttpRequestRetry ↓ Custom Plugin ↓ Engine ↓ HTTPResponseHTTP ↓ Engine ↓ HttpResponse ↓ Auth / Retry 等生命周期处理 ↓ Logging ↓ ContentNegotiation ↓ ApiResponseUser ↓ NetworkClient ↓ 检查 code ↓ 解包 data ↓ User ↓ ApiService ↓ Repository ↓ ViewModel这才是一次完整的 Ktor 网络调用。二十四、如果发生 401 呢网络生命周期会产生分支GET /user ↓ Authorization: Bearer A1 ↓ HTTP ↓ 401 ↓ Auth ↓ RefreshToken ↓ refreshClient ↓ A2 ↓ 保存新 Token ↓ 重新发送原 Request ↓ Authorization: Bearer A2 ↓ 200所以一次业务请求不一定只对应一次真实 HTTP Request。这也是理解 KtorSend生命周期以后非常重要的一点。二十五、如果发生 503 呢又会走另一条恢复链GET /products ↓ 503 ↓ HttpRequestRetry ↓ 判断 错误是否值得 Retry AND 当前网络是否 Available AND Request 是否可以安全重放 ↓ Backoff ↓ Send #2 ↓ 200注意Auth Refresh和HttpRequestRetry都会重新发送 Request。但两者原因完全不同401 ↓ 认证状态恢复 503 / Network Error ↓ 瞬时故障恢复不要混在一起。二十六、完整错误链路现在异常体系也可以重新串起来。断网NetworkClient ↓ Connectivity Pre-check ↓ Unavailable ↓ AppError.NetworkTimeoutHttpClient ↓ HttpTimeout ↓ Timeout Exception ↓ ExceptionMapper ↓ AppError.TimeoutHTTP 500500 ↓ Retry Policy ↓ 最终仍失败 ↓ ExceptionMapper ↓ AppError.ServerJSON 解析失败Response ↓ ContentNegotiation ↓ SerializationException ↓ ExceptionMapper ↓ AppError.Parse业务错误HTTP 200 ↓ ApiResponseT ↓ code ! 0 ↓ ApiException ↓ AppError.Business登录失效401 ↓ Auth Refresh ↓ 失败 ↓ 最终 Unauthorized ↓ AppError.Unauthorized于是 ViewModel 最终面对的是User / Order / Product 或者 AppError而不是底层 Ktor 异常。二十七、推荐的目录结构实际项目可以参考shared/ └── src/ ├── commonMain/ │ │ └── network/ │ │ │ ├── client/ │ │ ├── NetworkClient.kt │ │ ├── NetworkClients.kt │ │ └── HttpClientFactory.kt │ │ │ ├── config/ │ │ ├── NetworkConfig.kt │ │ └── NetworkConfigs.kt │ │ │ ├── provider/ │ │ ├── TokenProvider.kt │ │ ├── CommonHeadersProvider.kt │ │ └── NetworkConnectivityProvider.kt │ │ │ ├── model/ │ │ └── ApiResponse.kt │ │ │ ├── error/ │ │ ├── AppError.kt │ │ └── ExceptionMapper.kt │ │ │ ├── plugin/ │ │ ├── HttpLoggingConfig.kt │ │ ├── RetryPolicy.kt │ │ └── CustomPlugin.kt │ │ │ └── api/ │ ├── UserApiService.kt │ ├── OrderApiService.kt │ └── AuthApiService.kt │ ├── androidMain/ │ └── network/ │ ├── PlatformHttpClient.kt │ ├── AndroidConnectivityMonitor.kt │ └── AndroidSecureStore.kt │ ├── iosMain/ │ └── network/ │ ├── PlatformHttpClient.kt │ ├── IosConnectivityMonitor.kt │ └── IosSecureStore.kt │ └── wasmJsMain/ └── network/ ├── PlatformHttpClient.kt └── WebConnectivityMonitor.kt目录名称不是标准答案。真正重要的是commonMain ↓ 共享网络规则 platformMain ↓ 平台实现差异二十八、DI谁负责对象生命周期如果使用 Koin可以singleTokenProvider { DefaultTokenProvider(...) } single NetworkConnectivityProvider { DefaultNetworkConnectivityProvider(...) } single { NetworkClients( configs get(), tokenProvider get(), commonHeadersProvider get(), connectivityProvider get(), exceptionMapper get(), ) } single { UserApiService( networkClient getNetworkClients().api ) }这里一定要把两件事分开Factory ↓ 这个对象怎么创建 DI / Koin ↓ 这个对象创建几个 活多久 谁依赖它所以Factory 管怎么造DI 管造几个和生命周期。或者更简单Ktor 负责网络Koin 负责对象。二十九、什么时候应该创建新的 HttpClient以后项目增加业务时不要第一反应新业务 ↓ 新 Client先问BaseUrl 是否明显不同 Auth 是否完全不同 Timeout / Retry Policy 是否明显不同 Header / Cookie / TLS 是否属于独立配置域 Client 生命周期是否应该隔离如果大部分答案都是是才考虑新的 HttpClient否则优先复用现有 Client Request Override三十、例如上传一定要独立 Client 吗不一定。如果只是BaseUrl 一样 Auth 一样 Header 一样 只是 Timeout 更长完全可以apiClient.post( /upload ) { timeout { requestTimeoutMillis 120_000 } }没必要因为Timeout 不一样就创建一个新 Client。如果上传已经变成独立域名 独立认证 特殊并发策略 完全不同 Retry Binary Logging 策略 上传进度 Plugin那uploadClient就开始成为一个真正独立的网络责任域这才值得拆。三十一、WebSocket 不应该硬塞进普通 NetworkClient普通 HTTPRequest ↓ Response而 WebSocketConnect ↓ Session ↓ 持续 Message ↓ Heartbeat ↓ Reconnect ↓ Disconnect生命周期完全不同。所以可以共享Config Provider Logger Auth 信息但最好形成WebSocketClient自己的抽象。不要为了“网络相关都放 NetworkClient”把两个完全不同的通信模型强行揉在一起。三十二、网络架构不要过度抽象架构设计还有一个常见误区BaseClient AbstractClient ClientManager RequestExecutor RequestProcessor ResponseProcessor NetworkGateway NetworkCoordinator最后一个 GET穿十几层这不是好的架构。真正的原则应该是每增加一层都必须解决一个明确问题。例如ApiService ↓ 解决业务接口描述 NetworkClient ↓ 解决项目统一网络规则 HttpClient ↓ 解决 HTTP 能力组合 Engine ↓ 解决平台网络实现这几层已经足够明确。三十三、整套网络架构其实只有四条主线如果觉得前面内容太多可以最后压缩成四条线。1. 业务调用线ViewModel ↓ Repository ↓ ApiService ↓ NetworkClient2. HTTP 能力线NetworkClient ↓ HttpClient ↓ Plugin ↓ Engine ↓ HTTP3. 动态状态线App / Platform State ↓ Provider ↓ NetworkClient / Plugin / HttpClient4. 错误返回线Network / HTTP / JSON / Business ↓ Throwable ↓ ExceptionMapper ↓ AppError ↓ Repository / ViewModel四条线组合起来就是整个 KMP 网络层。三十四、最后再看一遍完整架构UI ↓ ViewModel ↓ Repository ↓ ApiService ↓ NetworkClient │ Connectivity Pre-check ↓ NetworkClients ┌─────────┴─────────┐ ↓ ↓ apiClient refreshClient │ │ └─────────┬─────────┘ ↓ HttpClient │ ┌──────────────────┼──────────────────┐ ↓ ↓ ↓ Config Provider Plugin │ │ │ ↓ ↓ ↓ BaseUrl Token ContentNegotiation Timeout Language DefaultRequest ... Network Logging ... HttpTimeout Retry Auth Custom Plugin │ ↓ Engine ┌──────────┼───────────┐ ↓ ↓ ↓ Android iOS JS/Wasm OkHttp Darwin ... └──────────┼───────────┘ ↓ HTTP ↓ Response / Throwable ↓ ContentNegotiation ↓ ApiResponseT ↓ NetworkClient ↓ ┌────────────┴────────────┐ ↓ ↓ code 0 Throwable ↓ ↓ data ExceptionMapper ↓ ↓ T AppError如果能自己解释这张图中的每一层为什么存在 负责什么 为什么不能放到旁边那一层那么这套 Ktor 网络架构就已经真正理解了。三十五、最终记住六句话整个系列其实可以压缩成下面六句话。1. ApiService描述“我要调用哪个业务接口”。2. NetworkClient规定“项目里的请求应该怎么统一执行”。3. HttpClient代表“一条长期复用、已经配置完成的网络能力通道”。4. Provider提供“当前运行期动态状态”。5. Plugin把横切能力挂到 Request / Response 生命周期。6. Engine负责“这个平台最终怎么真正发出 HTTP 请求”。串起来就是ApiService ↓ NetworkClient ↓ HttpClient ↓ Plugin Provider ↓ Engine ↓ HTTP这就是整个 KMP Ktor 网络架构的核心。三十六、写在最后刚开始学习 Ktor 时很容易把它理解成Retrofit 的替代品然后关注GET 怎么写 POST 怎么写 JSON 怎么解析但学到这里以后会发现 Ktor Client 真正值得理解的是HttpClient Plugin Lifecycle Engine它提供的是一套完整的网络能力组合模型。而项目自己的架构则继续在它上面增加ApiService NetworkClient Provider AppError ExceptionMapper NetworkClients最后形成业务层 ↓ 项目网络抽象 ↓ Ktor Client ↓ 平台 Engine ↓ 真实 HTTP所以真正成熟的 KMP 网络层追求的并不是代码最少 封装层数最多 所有东西都做成 Plugin而是业务层足够简单 每一层职责清楚 动态状态可以安全变化 HttpClient 可以长期复用 错误语义稳定 平台差异被隔离 特殊请求仍然保留扩展能力 未来新增网络能力时知道应该放在哪一层当你能够看到一个新的需求例如“我要做请求签名”第一反应已经不是Ktor 有没有哪个 API而是这个需求属于什么职责 它应该发生在哪个生命周期 应该放 NetworkClient Provider 还是 Custom Plugin那么你就已经从会使用 Ktor走到了能够设计 Ktor 网络架构Ktor 网络层系列主线到这里形成闭环第一篇 Ktor Client 到底是什么 ↓ 第二篇 GET、POST、参数与请求体 ↓ 第三篇 ContentNegotiation ↓ 第四篇 kotlinx.serialization ↓ 第五篇 DefaultRequest ↓ 补充篇 5.1 静态配置、Provider 与 Request 三种作用域 ↓ 第六篇 NetworkClient 请求层封装 ↓ 第七篇 ApiResponseT 与业务 code ↓ 第八篇 统一异常体系 AppError ↓ 补充篇 8.1 KMP 断网处理与 ConnectivityProvider ↓ 第九篇 Logging、HttpTimeout 与 HttpRequestRetry ↓ 补充篇 9.1 Logging 深入 Header、Body 脱敏与自定义 Logger ↓ 补充篇 9.2 Ktor DSL 深入 为什么 install、get、headers 可以这样写 ↓ 第十篇 Custom Client Plugin 从 OkHttp Interceptor 理解 Ktor 生命周期 ↓ 第十一篇 Bearer Token、Refresh Token 与 401 自动刷新 ↓ 第十二篇 完整 KMP Ktor 网络架构到这里Ktor Client 的网络主线已经可以告一段落。后续进入 Ktor 网络层进阶系列到第十二篇为止KMP Ktor 网络层的基础架构已经完整闭环。接下来不再继续补基础框架而是进入真正的工程进阶阶段。后续计划进阶第一篇 AppResultT 网络层到底应该 throw还是返回统一 Result ↓ 进阶第二篇 Ktor Multipart 文件上传 ↓ 进阶第三篇 文件下载与 Progress ↓ 进阶第四篇 Ktor WebSocket 长连接 ↓ 进阶第五篇 Cookie 与 Session ↓ 进阶第六篇 请求签名 Timestamp、Nonce、Body Hash 与 Signature ↓ 进阶第七篇 Request Body 加密与 Response 解密 ↓ 进阶第八篇 TLS、证书校验与 Certificate Pinning ↓ 进阶第九篇 Ktor Engine 深入 OkHttp、Darwin、CIO、JS/Wasm ↓ 进阶第十篇 NetworkClient 与 Plugin 单元测试前十二篇解决的是一套完整的 KMP Ktor 网络层应该怎么设计。后面的进阶系列则继续解决上传下载 长连接 认证与 Session 请求安全 数据加密 TLS 平台 Engine 差异 网络测试这些内容已经不是“把基础网络层搭起来”而是在现有架构上继续增加真正面向复杂项目的工程能力。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进