Ktor Client — HTTP-запросы
Ktor Client
Сервер Ktor принимает HTTP-запросы. Ktor Client — библиотека, которая эти запросы отправляет из приложения — Desktop, Android, backend-утилита. Оба используют корутины: вызовы get / post — suspend-функции.
Типичная цепочка: ViewModel → HttpClient → JSON → список на экране (Compose, Flow).
Словарь терминов
| Термин | Простыми словами |
|---|---|
| HTTP-клиент | Программа, которая формирует запрос (метод, URL, заголовки, тело) и читает ответ. |
| GET | "Дай данные" — обычно без тела. |
| POST | "Создай или отправь данные" — часто с JSON в теле. |
| HttpClient | Главный объект Ktor Client; создаётся один раз на приложение. |
| Engine (CIO, OkHttp) | Низкоуровневая реализация сокетов под платформу. |
| Content Negotiation | Автоматический JSON ↔ Kotlin-объект. |
| Suspend-вызов | client.get(...) можно вызывать только из корутины. |
Зависимости (JVM)
build.gradle.kts:
plugins {
kotlin("jvm") version "1.9.24"
kotlin("plugin.serialization") version "1.9.24"
application
}
dependencies {
implementation("io.ktor:ktor-client-core:2.3.12")
implementation("io.ktor:ktor-client-cio:2.3.12")
implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
implementation("io.ktor:ktor-client-logging:2.3.12")
}
Разбор:
kotlin("plugin.serialization")включает генерацию сериализации для@Serializableклассов.ktor-client-coreдаёт базовые API клиента:HttpClient, запросы и плагины.ktor-client-cioдобавляет сетевой движок для JVM-окружения.content-negotiationиkotlinx-jsonавтоматически переводят JSON в Kotlin-объекты и обратно.ktor-client-loggingупрощает отладку, показывая URL, статус и время ответа.
На Android вместо cio часто подключают ktor-client-okhttp.
Модель данных
Те же классы, что на сервере в Ktor — первая программа:
import kotlinx.serialization.Serializable
@Serializable
data class Note(val id: Int, val text: String)
@Serializable
data class NoteCreate(val text: String)
Разбор:
Noteописывает сущность, которую сервер возвращает при чтении списка заметок.NoteCreateхранит только входные данные для создания новой заметки.@Serializableдаётkotlinx.serializationвозможность кодировать и декодировать эти типы.- Разделение моделей делает API-контракт понятным и исключает отправку лишних полей.
Создание клиента
Код ITЗагрузка примера кода…
| Настройка | Зачем |
|---|---|
ContentNegotiation + json() | Тело ответа → Note, объект → JSON в POST |
ignoreUnknownKeys = true | Сервер добавил поле — клиент со старой моделью не падает |
Logging | В консоль: URL, статус, время (отладка) |
Важно: HttpClient дорогой в создании — один экземпляр на приложение (singleton, DI). При завершении: client.close().
GET и POST — разбор
Предположим, сервер из Ktor — первая программа слушает http://127.0.0.1:8080.
Код ITЗагрузка примера кода…
Построчно:
| Строка | Что происходит |
|---|---|
client.get(url) | HTTP GET, suspend до ответа |
.body<Map<...>>() | Десериализация JSON в тип |
post(url) { ... } | Блок настраивает запрос |
contentType(Application.Json) | Заголовок Content-Type: application/json |
setBody(NoteCreate(...)) | Сериализация объекта в JSON-тело |
runBlocking | Только для main; в Android — viewModelScope.launch |
Обработка ошибок
import io.ktor.client.plugins.*
suspend fun safeGet(client: HttpClient, url: String): String? =
try {
client.get(url).body()
} catch (e: ClientRequestException) {
println("HTTP ${e.response.status}: ${e.response.status.description}")
null
} catch (e: ServerResponseException) {
println("Ошибка сервера: ${e.response.status}")
null
}
| Исключение | Когда |
|---|---|
ClientRequestException | 4xx — ошибка запроса (404, 401) |
ServerResponseException | 5xx — ошибка сервера |
Таймаут в блоке клиента:
HttpClient(CIO) {
engine {
requestTimeout = 10_000
}
}
Разбор:
engineзадаёт параметры на уровне низкоуровневого сетевого слоя.requestTimeout = 10_000ограничивает ожидание ответа десятью секундами.- При превышении лимита запрос завершается исключением, которое обрабатывается в
try/catch. - Таймаут предотвращает зависание UI и фоновых операций на плохой сети.
Заголовки и токен
client.get("https://api.example.com/profile") {
header(HttpHeaders.Authorization, "Bearer $token")
parameter("page", 1)
}
Разбор:
header(HttpHeaders.Authorization, ...)добавляет токен в заголовок авторизации.- Формат
Bearer $tokenобычно используется для JWT и OAuth2 access token. parameter("page", 1)корректно добавляет query-параметр без ручной сборки URL.- Конфигурационный блок у
getпозволяет держать параметры запроса рядом с вызовом.
parameter добавляет ?page=1 к URL. Плагин Auth может подставлять токен автоматически для серии запросов.
Android и Kotlin Multiplatform
| Платформа | Движок |
|---|---|
| JVM / Desktop | CIO |
| Android | OkHttp |
| iOS | Darwin |
В KMP — expect fun createHttpClient() — HttpClient в commonMain, actual в androidMain / iosMain — сеть в общем коде, движок под платформу.
Репозиторий поверх клиента
Когда запросов становится больше двух-трёх, удобнее вынести HTTP-логику в отдельный класс:
class NotesApi(private val client: HttpClient, private val baseUrl: String) {
suspend fun fetchNotes(): List<Note> =
client.get("$baseUrl/notes").body()
suspend fun createNote(text: String): Note =
client.post("$baseUrl/notes") {
contentType(ContentType.Application.Json)
setBody(NoteCreate(text))
}.body()
}
Разбор:
NotesApiизолирует сетевые детали и даёт приложению понятные методы работы с заметками.fetchNotes()скрывает URL и десериализацию, возвращая сразуList<Note>.createNote(text)формирует JSON-тело черезNoteCreateи получает созданную запись.baseUrlпозволяет переиспользовать класс для разных окружений — dev, stage, prod.- Такой слой проще мокать в тестах, чем напрямую
HttpClient.
Дальше ViewModel работает с NotesApi, а детали URL и заголовков остаются в одном месте. Этот слой хорошо сочетается с Compose и Room.
Результат запроса как состояние UI
Вместо try/catch по всему коду удобно использовать единый тип результата:
sealed interface LoadState<out T> {
data object Loading : LoadState<Nothing>
data class Success<T>(val data: T) : LoadState<T>
data class Error(val message: String) : LoadState<Nothing>
}
suspend fun loadNotes(api: NotesApi): LoadState<List<Note>> =
try {
LoadState.Success(api.fetchNotes())
} catch (e: Exception) {
LoadState.Error(e.message ?: "Ошибка сети")
}
Такое представление делает экран предсказуемым — "загрузка", "данные", "ошибка" — в явном виде.
Ретраи и таймауты на уровне политики
Для нестабильной сети полезно задавать политику повторов:
Код ITЗагрузка примера кода…
Важно разделять бизнес-ошибки 4xx и сетевые сбои, чтобы не повторять заведомо некорректный запрос.
Дополнительный сниппет — PATCH и обработка ответа
suspend fun renameNote(client: HttpClient, baseUrl: String, id: Int, newText: String): Boolean {
val response = client.patch("$baseUrl/notes/$id") {
contentType(ContentType.Application.Json)
setBody(mapOf("text" to newText))
}
return response.status.value in 200..299
}
Разбор:
patch(...)отправляет частичное обновление ресурса, когда меняется только часть полей.contentType(Application.Json)фиксирует формат тела запроса для сервера.setBody(mapOf(...))сериализует словарь в JSON без отдельного DTO в коротком примере.- Переменная
responseдаёт доступ к статусу, заголовкам и при необходимости телу ответа. - Проверка
status.value in 200..299сводит исход операции к удобномуBooleanдля UI-слоя.
Частые ошибки
| Симптом | Причина |
|---|---|
Connection refused | Сервер не запущен |
| Serialization error | Нет @Serializable или разные поля JSON |
| Вызов из UI-потока без корутины | Нужен launch + suspend |
| Утечки | Клиент не close() при выходе из процесса |
Связанные материалы
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.