前面十一篇,我们已经陆续把 Ktor Client 网络层中的主要知识点拆开讲了一遍:
HttpClient Engine ContentNegotiation DefaultRequest Provider NetworkClient ApiResponse<T> 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(): List<Order> { 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 → ApiResponse<T> 业务 code 判断 data 解包 Throwable → AppError可以把它理解成:
Ktor HttpClient 提供通用 HTTP 能力,NetworkClient 定义我们这个项目怎么使用这些能力。
四、一个基础 NetworkClient
假设后端统一返回:
{ "code": 0, "msg": "", "data": {} }对应:
@Serializable data class ApiResponse<T>( 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.body<ApiResponse<T>>() 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而refreshClient:
Refresh 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, )多个 Client:
data class NetworkConfigs( val api: NetworkConfig, val refresh: NetworkConfig, )Config 适合存:
BaseUrl Timeout 固定 Header Client 类型相关配置因为这些东西回答的是:
这个 Client 本身是什么?
十一、Provider:描述“现在是什么”
前面我们已经出现了很多 Provider:
TokenProvider 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.comProvider:
Language = zh-CN某一次特殊 Request:
Language = 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官方 Plugin:
ContentNegotiation DefaultRequest Logging HttpTimeout HttpRequestRetry AuthCustom Plugin:
Trace 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 ↓ HTTPResponse:
HTTP ↓ Engine ↓ HttpResponse ↓ Auth / Retry 等生命周期处理 ↓ Logging ↓ ContentNegotiation ↓ ApiResponse<User> ↓ 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.NetworkTimeout
HttpClient ↓ HttpTimeout ↓ Timeout Exception ↓ ExceptionMapper ↓ AppError.TimeoutHTTP 500
500 ↓ Retry Policy ↓ 最终仍失败 ↓ ExceptionMapper ↓ AppError.ServerJSON 解析失败
Response ↓ ContentNegotiation ↓ SerializationException ↓ ExceptionMapper ↓ AppError.Parse业务错误
HTTP 200 ↓ ApiResponse<T> ↓ 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,可以:
single<TokenProvider> { DefaultTokenProvider(...) } single< NetworkConnectivityProvider > { DefaultNetworkConnectivityProvider(...) } single { NetworkClients( configs = get(), tokenProvider = get(), commonHeadersProvider = get(), connectivityProvider = get(), exceptionMapper = get(), ) } single { UserApiService( networkClient = get<NetworkClients>().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
普通 HTTP:
Request ↓ Response而 WebSocket:
Connect ↓ 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 ↓ ApiResponse<T> ↓ 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 请求层封装 ↓ 第七篇 ApiResponse<T> 与业务 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 网络层的基础架构已经完整闭环。
接下来不再继续补基础框架,而是进入真正的工程进阶阶段。
后续计划:
进阶第一篇 AppResult<T>: 网络层到底应该 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 差异 网络测试这些内容已经不是“把基础网络层搭起来”,而是在现有架构上继续增加真正面向复杂项目的工程能力。