Skip to content

网络请求

源:Ktor Client | OkHttp

网络请求是几乎所有应用的核心功能。KMP 生态中,Ktor Client 是跨平台网络库的首选方案,本文将展示如何使用 Ktor 实现统一的网络请求。

平台差异对比

平台推荐引擎底层实现特性支持
AndroidOkHttpOkHttp3HTTP/2、连接池、拦截器
iOSDarwinNSURLSessionTLS、Cookie、缓存
Desktop (JVM)CIO / OkHttpCoroutine IO / OkHttp3全功能支持
JavaScriptJSfetch API基础 HTTP

Ktor Client 优势

Ktor Client 提供统一的 API,各平台自动选择最优引擎,开发者无需关心底层差异。

Ktor Client 基础使用

API 核心签名说明

  • val client = HttpClient(engine)
  • suspend fun HttpClient.get(url: String): HttpResponse
  • suspend inline fun <reified T> HttpClient.get(url: String): T
  • suspend fun HttpClient.post(url: String) { body = ... }
  • fun HttpClient.close()

标准代码块

kotlin
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class User(
    val id: Int,
    val name: String,
    val email: String
)

@Serializable
data class ApiResponse<T>(
    val code: Int,
    val data: T,
    val message: String
)

class ApiClient {
    
    private val client = HttpClient {
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
                isLenient = true
            })
        }
    }
    
    suspend fun getUser(userId: Int): User {
        return client.get("https://api.example.com/users/$userId").body()
    }
    
    suspend fun createUser(name: String, email: String): User {
        return client.post("https://api.example.com/users") {
            setBody(mapOf("name" to name, "email" to email))
        }.body()
    }
    
    fun close() {
        client.close()
    }
}

// 业务层使用
class UserRepository(private val apiClient: ApiClient) {
    
    suspend fun fetchUserProfile(userId: Int): Result<User> {
        return runCatching {
            apiClient.getUser(userId)
        }
    }
}
kotlin
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*

// Android 使用 OkHttp 引擎
actual fun createHttpClient(): HttpClient {
    return HttpClient(OkHttp) {
        engine {
            config {
                followRedirects(true)
                connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
            }
        }
    }
}
kotlin
import io.ktor.client.*
import io.ktor.client.engine.darwin.*

// iOS 使用 Darwin (NSURLSession) 引擎
actual fun createHttpClient(): HttpClient {
    return HttpClient(Darwin) {
        engine {
            configureRequest {
                setAllowsCellularAccess(true)
            }
        }
    }
}
kotlin
import io.ktor.client.*
import io.ktor.client.engine.cio.*

// Desktop 使用 CIO 引擎(纯 Kotlin 协程实现)
actual fun createHttpClient(): HttpClient {
    return HttpClient(CIO) {
        engine {
            maxConnectionsCount = 1000
            endpoint {
                connectTimeout = 30000
                socketTimeout = 30000
            }
        }
    }
}

代码封装示例

以下是带统一错误处理和 Token 刷新的完整封装:

kotlin
// commonMain
sealed class NetworkResult<out T> {
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Error(val code: Int, val message: String) : NetworkResult<Nothing>()
    data class Exception(val throwable: Throwable) : NetworkResult<Nothing>()
}

class ApiService(private val baseUrl: String, private val tokenProvider: () -> String?) {
    
    private val client = HttpClient {
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
                isLenient = true
            })
        }
        
        install(HttpTimeout) {
            requestTimeoutMillis = 30000
            connectTimeoutMillis = 30000
            socketTimeoutMillis = 30000
        }
        
        install(HttpRequestRetry) {
            retryOnServerErrors(maxRetries = 3)
            exponentialDelay()
        }
        
        defaultRequest {
            url(baseUrl)
            
            // 自动添加 Token
            tokenProvider()?.let { token ->
                header("Authorization", "Bearer $token")
            }
        }
    }
    
    suspend inline fun <reified T> get(
        path: String,
        params: Map<String, String> = emptyMap()
    ): NetworkResult<T> {
        return safeApiCall {
            client.get(path) {
                params.forEach { (key, value) ->
                    parameter(key, value)
                }
            }
        }
    }
    
    suspend inline fun <reified T, reified R> post(
        path: String,
        body: T
    ): NetworkResult<R> {
        return safeApiCall {
            client.post(path) {
                contentType(ContentType.Application.Json)
                setBody(body)
            }
        }
    }
    
    suspend inline fun <reified T> safeApiCall(
        crossinline apiCall: suspend () -> HttpResponse
    ): NetworkResult<T> {
        return try {
            val response = apiCall()
            
            when (response.status.value) {
                in 200..299 -> {
                    val data = response.body<T>()
                    NetworkResult.Success(data)
                }
                401 -> {
                    // Token 过期,需要刷新
                    NetworkResult.Error(401, "Unauthorized")
                }
                else -> {
                    NetworkResult.Error(
                        code = response.status.value,
                        message = response.bodyAsText()
                    )
                }
            }
        } catch (e: Exception) {
            NetworkResult.Exception(e)
        }
    }
    
    fun close() {
        client.close()
    }
}

依赖补充

Ktor Client 核心

kotlin
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("io.ktor:ktor-client-core:2.3.7")
            implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
            implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
        }
        
        androidMain.dependencies {
            implementation("io.ktor:ktor-client-okhttp:2.3.7")
        }
        
        iosMain.dependencies {
            implementation("io.ktor:ktor-client-darwin:2.3.7")
        }
        
        jvmMain.dependencies {
            implementation("io.ktor:ktor-client-cio:2.3.7")
        }
    }
}
groovy
kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation "io.ktor:ktor-client-core:2.3.7"
                implementation "io.ktor:ktor-client-content-negotiation:2.3.7"
                implementation "io.ktor:ktor-serialization-kotlinx-json:2.3.7"
            }
        }
        androidMain {
            dependencies {
                implementation "io.ktor:ktor-client-okhttp:2.3.7"
            }
        }
        iosMain {
            dependencies {
                implementation "io.ktor:ktor-client-darwin:2.3.7"
            }
        }
    }
}
toml
[versions]
ktor = "2.3.7"

[libraries]
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }

最新版本查看链接:https://ktor.io/docs/client-releases.html

kotlinx.serialization

kotlin
plugins {
    kotlin("plugin.serialization") version "1.9.21"
}

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
        }
    }
}

实战坑点

iOS Darwin 引擎 SSL 证书

SSL Pinning

iOS Darwin 引擎不支持自定义 SSL 证书验证,需使用系统信任的证书。

解决方案:
如需自定义证书,可切换到 CIO 引擎或在 iOS 原生层配置。

Android OkHttp 版本冲突

::: caution 依赖冲突 项目中若已使用 OkHttp,可能与 Ktor 的 OkHttp 引擎版本冲突。 :::

解决方案:

kotlin
androidMain.dependencies {
    implementation("io.ktor:ktor-client-okhttp:2.3.7") {
        exclude(group = "com.squareup.okhttp3")
    }
    implementation("com.squareup.okhttp3:okhttp:4.12.0") // 统一版本
}

序列化异常处理

SerializationException

服务端返回非预期字段或格式错误会导致反序列化失败。

解决方案:

kotlin
Json {
    ignoreUnknownKeys = true  // 忽略未知字段
    coerceInputValues = true  // 强制转换非法值为默认值
}

客户端未关闭

资源泄漏

HttpClient 是重量级对象,应复用全局单例,应用退出时关闭。

最佳实践:

kotlin
object NetworkModule {
    val client = HttpClient { /* ... */ }
    
    fun close() {
        client.close()
    }
}

// 应用退出时调用
NetworkModule.close()

CORS 跨域问题

Web 平台限制

JavaScript 引擎受浏览器 CORS 限制,需服务端配置 Access-Control-Allow-Origin。