第十二篇:完整 KMP + Ktor 网络架构:从 ApiService 到 NetworkClient、Provider、Plugin、Engine
2026/8/24 23:11:40 网站建设 项目流程

前面十一篇,我们已经陆续把 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 ↓ HttpClient

NetworkClient负责项目自己的统一规则,例如:

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.com

Provider:

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 ↓ 谁都可以随便改 NetworkState

Provider 是:

状态来源。

不是:

全局可写变量。


十四、Plugin:把横切能力挂进 HttpClient 生命周期

前面第十篇已经讲过:

Ktor ≠ 一个大号 OkHttp Interceptor

Ktor 更核心的模型是:

HttpClient Lifecycle + Plugin + Hook

官方 Plugin:

ContentNegotiation DefaultRequest Logging HttpTimeout HttpRequestRetry Auth

Custom 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 ↓ HTTP

Response:

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.Network

Timeout

HttpClient ↓ HttpTimeout ↓ Timeout Exception ↓ ExceptionMapper ↓ AppError.Timeout

HTTP 500

500 ↓ Retry Policy ↓ 最终仍失败 ↓ ExceptionMapper ↓ AppError.Server

JSON 解析失败

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 ↓ NetworkClient

2. HTTP 能力线

NetworkClient ↓ HttpClient ↓ Plugin ↓ Engine ↓ HTTP

3. 动态状态线

App / Platform State ↓ Provider ↓ NetworkClient / Plugin / HttpClient

4. 错误返回线

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 差异 网络测试

这些内容已经不是“把基础网络层搭起来”,而是在现有架构上继续增加真正面向复杂项目的工程能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询