Перейти к основному содержимому

Ktor Client — HTTP-запросы

Разработчику Архитектору

Ktor Client

Сервер Ktor принимает HTTP-запросы. Ktor Client — библиотека, которая эти запросы отправляет из приложения — Desktop, Android, backend-утилита. Оба используют корутины: вызовы get / postsuspend-функции.

Типичная цепочка: 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
}
ИсключениеКогда
ClientRequestException4xx — ошибка запроса (404, 401)
ServerResponseException5xx — ошибка сервера

Таймаут в блоке клиента:

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 / DesktopCIO
AndroidOkHttp
iOSDarwin

В 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 как основа веб-интеграций.