第十二篇:完整 KMP + Ktor 网络架构:从 ApiService 到 NetworkClient、Provider、Plugin、Engine
前面十一篇,我们已经陆续把 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。
这也是理解 Ktor Send 生命周期以后非常重要的一点。
二十五、如果发生 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 差异
网络测试
这些内容已经不是“把基础网络层搭起来”,而是在现有架构上继续增加真正面向复杂项目的工程能力。
更多推荐




所有评论(0)