在 Android 开发中,我们通常使用:
Retrofit + OkHttp
来完成网络请求。
但进入 KMP(Kotlin Multiplatform)之后,会发现一个新的网络框架:
Ktor Client
第一次接触 Ktor 时,很容易产生几个疑问:
Ktor 是不是相当于 Retrofit?
HttpClient 是不是相当于 OkHttpClient?
Engine 又是什么?
为什么 Android 用 OkHttp,
iOS 却可以使用 Darwin?
ContentNegotiation、Logging、Auth
这些 install(...) 又是什么?
实际上,Ktor 和 Retrofit 的设计思路并不完全一样。
理解 Ktor 最关键的一步,不是先记住 get()、post() 怎么写,而是先理解它的整体结构:
HttpClient
+
Plugin
+
Engine
只要这一层理解清楚,后面的 JSON 解析、公共 Header、Token、日志、超时、重试等功能都会很好理解。
一、先从熟悉的 Retrofit 说起
传统 Android 项目中,我们可能会这样定义接口:
interface UserApi {
@GET("user/{id}")
suspend fun getUser(
@Path("id") id: Long
): User
}
然后创建 Retrofit:
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(...)
.build()
这里实际上存在几个不同角色。
Retrofit
│
├── 定义接口
├── 拼装请求
├── Converter
│
↓
OkHttp
│
↓
真正执行 HTTP 请求
可以简单理解为:
Retrofit 更偏向于"HTTP 接口描述和封装",OkHttp 才是底层真正执行网络请求的客户端。
例如:
@GET("user/{id}")
Retrofit 根据注解帮我们生成真正的 HTTP 请求。
所以 Android 开发者长期使用 Retrofit 后,很容易形成一种思维:
定义 Interface
↓
写 @GET / @POST
↓
Retrofit 自动生成实现
而 Ktor 并不是这种设计。
二、Ktor 没有 Retrofit 那种 Interface + 注解模式
Ktor Client 最直接的网络请求是这样的:
val response = client.get(
"https://example.com/user/1001"
)
POST 也是类似:
client.post(
"https://example.com/user"
) {
setBody(request)
}
你会发现:
没有 @GET
没有 @POST
没有 Retrofit Interface
而是直接:
client.get()
client.post()
client.put()
client.delete()
这是一种 DSL 风格的网络请求方式。
因此,从 Retrofit 迁移到 Ktor 时,第一件需要改变的事情就是:
Retrofit:
Interface
↓
Annotation
↓
生成请求
Ktor:
HttpClient
↓
DSL
↓
直接构造请求
这并不意味着正式项目里所有地方都直接写:
client.get(...)
后面我们仍然可以自己封装:
ApiService
↓
HttpRequest
↓
HttpClient
只是 Ktor 本身并不强制你使用 Retrofit 那种接口声明式结构。
三、Ktor 的核心:HttpClient
Ktor Client 最核心的对象就是:
HttpClient
例如:
val client = HttpClient()
或者指定一个 Engine:
val client = HttpClient(OkHttp)
官方文档也明确指出,HttpClient 创建时可以显式传入 Engine;如果不传,则会根据项目中引入的 Engine 依赖选择合适的实现。
可以先粗略理解为:
HttpClient 是整个 Ktor 网络系统的入口。
以后所有:
GET
POST
PUT
DELETE
JSON
Header
Token
Logging
Timeout
Retry
最终都会围绕这个 HttpClient 工作。
例如:
val client = HttpClient(OkHttp) {
expectSuccess = true
install(ContentNegotiation) {
json()
}
install(Logging)
}
这时候我们已经开始看到 Ktor 的整体设计了:
HttpClient
│
├── 基础配置
│
├── Plugin
│
└── Engine
四、Engine 是什么?
这是刚接触 Ktor 时最容易困惑的概念。
假设我们写:
HttpClient(OkHttp)
这里:
HttpClient
和:
OkHttp
不是一回事。
其中:
HttpClient
负责提供统一的 Ktor 网络 API。
而:
OkHttp
是一个 Engine。
Engine 可以理解成:
真正负责和操作系统网络能力交互、执行 HTTP 请求的底层实现。
Ktor Client 本身是多平台的,目前可以运行在 JVM、Android、JavaScript/Wasm 和 Kotlin/Native 等目标上,而不同平台可以使用不同 Engine。官方文档例如列出了 Android/JVM 可使用 OkHttp,Apple 平台可使用 Darwin,JavaScript 可使用 Js,同时 CIO 也覆盖多个平台。
于是整个结构变成:
你的业务代码
↓
Ktor HttpClient
↓
Engine
↓
系统网络能力
五、为什么 KMP 需要 Engine?
这其实正是 Ktor 非常适合 KMP 的原因。
假设现在有一个 KMP 项目:
commonMain
androidMain
iosMain
我们希望大量网络代码都放在:
commonMain
例如:
suspend fun getUser(): User {
return client
.get("/user")
.body()
}
问题来了。
Android 和 iOS 底层网络环境完全不同。
Android 可以使用:
OkHttp
iOS 则可以使用:
Darwin Engine
因此 Ktor 在中间做了一层抽象:
commonMain
HttpClient
│
client.get(...)
│
┌────────┴────────┐
↓ ↓
Android iOS
↓ ↓
OkHttp Darwin
↓ ↓
Android 网络 Apple 网络
官方也直接给出了类似的 KMP 用法:Android source set 可以添加 Android/OkHttp 等 Engine,iOS source set 使用 Darwin,运行时由对应平台的 Engine 执行请求。
这就是 Ktor 在 KMP 中非常重要的一层设计:
上层 API 跨平台,底层网络实现平台化。
六、这其实很像 KMP 的 expect / actual 思想
虽然 Ktor Engine 并不等于 expect / actual,但是从架构思想上非常像。
KMP 经常解决这样的问题:
commonMain
定义统一能力
↓
androidMain
Android 实现
iosMain
iOS 实现
Engine 也是类似:
HttpClient
↓
统一网络 API
↓
┌─────────────┴────────────┐
↓ ↓
OkHttp Darwin
↓ ↓
Android iOS
因此业务层根本不需要关心:
Android 到底怎么发请求?
iOS 到底怎么发请求?
它只需要:
client.get(...)
这就是抽象层的意义。
七、那么 Plugin 又是什么?
理解 Engine 后,再看第二个重要概念:
Plugin
假设我们要处理 JSON。
Ktor 可以:
install(ContentNegotiation) {
json()
}
想打印网络日志:
install(Logging)
想处理认证:
Auth
想配置超时:
HttpTimeout
想自动重试:
HttpRequestRetry
这些横切能力在 Ktor Client 中主要通过 Plugin 提供。官方将序列化、日志、认证等通用能力都归入 Client Plugins,并通过 install(...) 安装到 HttpClient 中。
所以:
val client = HttpClient(OkHttp) {
install(ContentNegotiation) {
json()
}
install(Logging)
install(HttpTimeout)
install(Auth)
install(HttpRequestRetry)
}
可以理解为:
HttpClient
│
├── ContentNegotiation
│ ↓
│ JSON
│
├── Logging
│ ↓
│ 日志
│
├── Auth
│ ↓
│ Token
│
├── HttpTimeout
│ ↓
│ 超时
│
└── HttpRequestRetry
↓
重试
因此:
Plugin 就是在 HttpClient 上安装各种网络能力。
八、这和 OkHttp Interceptor 有点像,但不能完全画等号
Android 开发中我们非常熟悉:
OkHttpClient.Builder()
.addInterceptor(...)
比如:
TokenInterceptor
LoggingInterceptor
HeaderInterceptor
RetryInterceptor
所以看到 Ktor:
install(Logging)
install(Auth)
install(DefaultRequest)
会觉得:
这不就是 Interceptor 吗?
从"解决横切逻辑"的角度来看确实很像:
请求
↓
公共 Header
↓
Token
↓
Logging
↓
真正请求
但是概念上不能直接说:
Plugin = Interceptor
因为 Ktor Plugin 的能力范围更广。
它不仅可以处理请求,还可以参与:
请求生命周期
响应生命周期
序列化
认证
异常处理
日志
重试
官方还允许创建自定义 Client Plugin,通过请求和响应阶段实现可复用能力。
因此更准确的理解应该是:
Interceptor
↓
可以帮助理解 Plugin
但:
Plugin > 单纯请求拦截
九、把 Retrofit + OkHttp 映射到 Ktor
现在可以做一个非常重要的对照。
Android 传统方案
Retrofit
│
├── @GET
├── @POST
├── Converter
│
↓
OkHttpClient
│
├── Interceptor
├── Timeout
├── Connection
│
↓
HTTP
而到了 Ktor:
Ktor HttpClient
│
├── get / post / put / delete
│
├── ContentNegotiation
├── DefaultRequest
├── Logging
├── Auth
├── HttpTimeout
├── Retry
│
↓
Engine
│
├── OkHttp
├── Darwin
├── CIO
└── Js
│
↓
HTTP
因此千万不要简单地理解为:
Ktor = Retrofit
更合适的理解是:
Ktor Client 自己提供了比较完整的 HTTP Client 抽象,并通过 Engine 对接各个平台。
在 Android 上:
Ktor HttpClient
↓
OkHttp Engine
↓
OkHttp
也就是说:
Ktor 并没有要求 Android 放弃 OkHttp。
反而可以把 OkHttp 作为 Android 平台下的执行 Engine。
十、为什么这一套特别适合 KMP?
现在来看问题就很清楚了。
如果直接使用 Retrofit:
commonMain
↓
?
Retrofit 本身主要面向 JVM/Android 生态,并不能直接作为完整的 KMP 跨平台网络抽象。
而 Ktor 的设计天然就是:
commonMain
HttpClient
↓
公共网络逻辑
↓
┌──────────┼──────────┐
↓ ↓ ↓
Android iOS Web
↓ ↓ ↓
OkHttp Darwin Js/CIO
因此我们可以把大量逻辑写在 commonMain:
ApiService
Request
Response
DTO
JSON
异常处理
Token 逻辑
Repository
只有真正和平台网络实现有关的部分留给各个平台。
这也是 KMP 的核心思想:
能共享的逻辑尽量共享,需要平台能力时再下沉到平台层。
十一、一个最简单的 Ktor 网络结构
先不用考虑复杂架构。
最简单可以只有:
val client = HttpClient()
然后:
suspend fun getUser() {
val response = client.get(
"https://example.com/user/1001"
)
}
此时结构只有:
业务代码
↓
HttpClient
↓
Engine
↓
HTTP
但正式项目肯定不会停留在这里。
我们还需要:
JSON
公共 BaseUrl
公共 Header
Token
Logging
Timeout
Retry
异常转换
统一 Response
于是最终会慢慢演化成:
ApiService
↓
HttpRequest
↓
HttpClient
│
┌───────────┼───────────┐
↓ ↓ ↓
DefaultRequest Logging Auth
↓ ↓ ↓
ContentNegotiation Timeout / Retry
│
↓
Engine
│
┌─────────┴─────────┐
↓ ↓
Android iOS
↓ ↓
OkHttp Darwin
这就是后面整个 Ktor 网络系列真正要搭起来的东西。
十二、HttpClient 应该每次请求都创建吗?
不要。
例如下面这种方式并不好:
suspend fun getUser() {
val client = HttpClient()
client.get(...)
}
然后另一个接口:
suspend fun getOrder() {
val client = HttpClient()
client.get(...)
}
HttpClient 本身持有连接、线程以及协程相关资源,官方也明确指出创建 HttpClient 并不是一个廉价操作,存在多次请求时应该复用实例,并在整个 Client 不再使用时再调用 close() 释放资源。
因此正式项目一般会:
创建一个 HttpClient
↓
统一配置
↓
整个网络层复用
例如:
class HttpClientFactory {
fun create(): HttpClient {
return HttpClient {
// 网络配置
}
}
}
然后通过依赖注入等方式复用。
这和 Android 项目通常只维护一个:
OkHttpClient
Retrofit
的思想是一样的。
十三、现在重新理解 Ktor
学到这里,可以先忘掉具体 API。
只记住三个核心词:
HttpClient
Plugin
Engine
它们之间的关系:
HttpClient
│
统一的网络请求入口
│
┌─────────────┴─────────────┐
↓ ↓
Plugin Engine
↓ ↓
扩展网络能力 真正执行网络请求
↓ ↓
JSON OkHttp
Logging Darwin
Auth CIO
Timeout Js
Retry
...
简单概括:
HttpClient
负责:
整个 Ktor Client 的入口
Plugin
负责:
给 HttpClient 增加能力
例如:
JSON
Logging
Auth
Timeout
Retry
DefaultRequest
Engine
负责:
真正执行网络请求
例如:
Android → OkHttp
iOS → Darwin
Web → Js / CIO 等
十四、从 Retrofit 思维迁移到 Ktor
如果之前长期使用:
Retrofit + OkHttp
那么学习 Ktor 最容易卡住的地方,其实不是 Kotlin 代码。
而是思维方式发生了变化。
以前:
定义 Retrofit Interface
↓
通过注解描述 HTTP
↓
Retrofit 创建实现
↓
OkHttp 请求
现在:
HttpClient
↓
DSL 构造请求
↓
Plugin 处理公共能力
↓
Engine 执行请求
最终可以记成一句话:
Ktor Client 提供跨平台统一网络 API,Plugin 提供网络层公共能力,Engine 负责适配不同平台并真正执行 HTTP 请求。
理解了这一句话,Ktor 的整体框架基本就立住了。
十五、本篇总结
这篇没有急着学习:
GET 怎么写
POST 怎么写
JSON 怎么解析
而是先回答了一个更重要的问题:
Ktor Client 到底是什么?
整个 Ktor 网络体系可以先记成:
业务层
↓
ApiService
↓
HttpClient
│
├── Plugin
│ ├── JSON
│ ├── Logging
│ ├── Auth
│ ├── Timeout
│ └── Retry
│
↓
Engine
│
├── Android → OkHttp
├── iOS → Darwin
└── Web → 对应 Engine
↓
HTTP
如果从 Retrofit 迁移过来,可以暂时这样理解:
Retrofit + OkHttp 的很多网络层职责
↓
在 KMP 中重新组织为
↓
Ktor HttpClient
+
Plugin
+
Engine
这也是后面学习所有 Ktor Client 能力的基础。
下一篇
《Ktor 网络请求基础:GET、POST、参数与请求体》
下一篇开始真正写请求,依次讲清楚:
GET
POST
PUT
DELETE
Path 参数
Query 参数
Header
Request Body
HttpResponse
并开始建立从:
client.get(...)
到正式项目网络层封装的第一步。