Netzwerk- und HTTP-API-Programmierung in Kotlin: Eine ausführliche Anleitung
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты сидишь в уютном кафе и хочешь заказать пиццу. Ты не идёшь на кухню сам, не лезешь в холодильник и не включаешь духовку. Вместо этого ты подзываешь официанта, говоришь: «Принесите, пожалуйста, пиццу «Маргарита»», — и через некоторое время получаешь свой заказ.
Работа с сетью в программировании — это точно такой же процесс. Твоя программа (клиент) — это ты, сидящий за столиком. Сервер (удалённый компьютер) — это кухня ресторана. А HTTP-запрос — это официант, который передаёт твой заказ туда и обратно.
Когда ты открываешь приложение погоды на телефоне, оно «говорит» серверу: «Дай мне температуру в Москве». Сервер ищет данные в своей базе, упаковывает их в ответ и отправляет обратно. Всё это происходит за доли секунды.
💡 Важно: Без работы с сетью любое приложение — это остров в океане. Оно может делать что-то само, но не может общаться с внешним миром. HTTP API — это мосты между островами.
Зачем это нужно?
- Получать актуальные данные (курс валют, погоду, новости)
- Отправлять свои данные (регистрация, комментарии, загрузка фото)
- Работать с базами данных через интернет
- Интегрировать своё приложение с другими сервисами (Google Maps, Telegram, YouTube)
В этой статье мы научимся писать код на Kotlin, который будет «разговаривать» с серверами так же естественно, как ты заказываешь пиццу.
Теория: как это работает изнутри
Чтобы понять HTTP API, нужно разобраться с тремя ключевыми понятиями: протокол HTTP, методы запросов и форматы данных.
Что такое HTTP?
HTTP (HyperText Transfer Protocol) — это набор правил, по которым общаются клиент и сервер. Представь, что это язык, на котором говорит официант. Если ты скажешь «Принесите мне вчерашний суп», официант не поймёт — такого блюда нет в меню. Так и сервер: он понимает только определённые команды.
Методы HTTP (глаголы)
Каждый запрос к серверу имеет метод — это глагол, который говорит, что именно мы хотим сделать:
| Метод | Что делает | Аналогия в жизни |
|---|---|---|
GET |
Получить данные | «Покажи меню» |
POST |
Создать новые данные | «Добавь новое блюдо в меню» |
PUT |
Обновить данные целиком | «Замени пиццу на новую» |
PATCH |
Обновить часть данных | «Измени только сыр в пицце» |
DELETE |
Удалить данные | «Убери это блюдо из меню» |
URL — адрес ресурса
URL (Uniform Resource Locator) — это адрес, по которому находится нужный ресурс. Он состоит из нескольких частей:
- Протокол:
https:// - Домен:
api.weather.com - Путь:
/v2/current - Параметры:
?city=Moscow&units=metric
Пример полного URL: https://api.weather.com/v2/current?city=Moscow&units=metric
Заголовки (Headers)
Заголовки — это дополнительная информация о запросе. Как конверт письма: на нём написан адрес, обратный адрес и пометка «Срочно». В HTTP заголовки передают:
Content-Type— формат данных (JSON, XML, текст)Authorization— ключ доступа (пароль для сервера)User-Agent— кто делает запрос (браузер, приложение)
Тело запроса (Body)
Когда мы отправляем данные на сервер (например, при регистрации), мы кладём их в «тело» запроса. Чаще всего это JSON-строка:
{
"name": "Иван",
"email": "ivan@example.com",
"age": 25
}
Коды ответов
Сервер всегда отвечает числовым кодом. Это как оценка за заказ:
| Код | Значение | Аналогия |
|---|---|---|
| 200 | OK — всё хорошо | «Вот ваша пицца» |
| 201 | Created — создано | «Ваше блюдо добавлено в меню» |
| 400 | Bad Request — ошибка в запросе | «Вы заказали то, чего нет в меню» |
| 401 | Unauthorized — нет доступа | «Покажите пропуск в ресторан» |
| 404 | Not Found — не найдено | «Такого блюда нет в меню» |
| 500 | Internal Server Error — ошибка сервера | «На кухне пожар, подождите» |
Синтаксис в Kotlin: все формы и варианты
В Kotlin для работы с сетью есть несколько способов. Мы рассмотрим самый популярный и современный — библиотеку Ktor Client. Она написана на Kotlin, легковесна и поддерживает корутины (асинхронность).
Подключение библиотеки
Сначала нужно добавить зависимость в файл build.gradle.kts:
// Файл build.gradle.kts (модуль app)
dependencies {
// Основной клиент
implementation("io.ktor:ktor-client-core:2.3.0")
// Движок для работы с сетью (OkHttp)
implementation("io.ktor:ktor-client-okhttp:2.3.0")
// Для работы с JSON
implementation("io.ktor:ktor-client-content-negotiation:2.3.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0")
// Для логирования (чтобы видеть запросы)
implementation("io.ktor:ktor-client-logging:2.3.0")
}
Создание клиента
Ktor Client настраивается один раз и используется для всех запросов:
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
// Создаём клиента с настройками
val client = HttpClient(OkHttp) { // Используем OkHttp как движок
// Настраиваем автоматическую работу с JSON
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true // Игнорировать неизвестные поля
prettyPrint = true // Красивый вывод JSON
})
}
// Включаем логирование (видим запросы в консоли)
install(Logging) {
level = LogLevel.BODY // Показывать всё тело запроса/ответа
}
}
Базовые методы запросов
Ktor предоставляет функции-расширения для каждого HTTP-метода. Вот как они выглядят:
| Метод | Функция Ktor | Описание |
|---|---|---|
| GET | client.get(url) |
Получить данные |
| POST | client.post(url) { ... } |
Создать данные |
| PUT | client.put(url) { ... } |
Обновить данные |
| DELETE | client.delete(url) |
Удалить данные |
Обработка ответа
Каждый запрос возвращает объект HttpResponse, из которого можно извлечь:
response.status— код ответа (200, 404 и т.д.)response.body<T>()— тело ответа, преобразованное в нужный типresponse.headers— заголовки ответа
Примеры кода — от простого к сложному
Все примеры используют публичное API jsonplaceholder.typicode.com — это тестовый сервер, который всегда работает и возвращает фейковые данные.
Пример 1: Простой GET-запрос (получаем список)
Самый простой запрос — получить список пользователей:
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
suspend fun main() { // suspend — потому что работаем с корутинами
// Создаём клиента (упрощённая версия без настроек)
val client = HttpClient(OkHttp)
try {
// Делаем GET-запрос по адресу
val response: HttpResponse = client.get("https://jsonplaceholder.typicode.com/users")
// Получаем тело ответа как строку
val body: String = response.bodyAsText()
// Печатаем результат
println("Статус: ${response.status}")
println("Данные: $body")
} catch (e: Exception) {
println("Ошибка: ${e.message}")
} finally {
// Всегда закрываем клиента, чтобы освободить ресурсы
client.close()
}
}
Объяснение:
suspend fun— функция, которая может приостанавливаться (ждёт ответа от сервера)HttpClient(OkHttp)— создаём клиента на движке OkHttpclient.get(...)— отправляем GET-запросresponse.bodyAsText()— получаем ответ как обычный текстtry-catch-finally— обрабатываем ошибки и закрываем клиента
Пример 2: GET с параметрами запроса
Часто нужно передать параметры в URL. Например, получить посты конкретного пользователя:
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
suspend fun main() {
val client = HttpClient(OkHttp)
try {
// Параметры запроса: userId = 1
val response = client.get("https://jsonplaceholder.typicode.com/posts") {
// Добавляем параметры в URL
parameter("userId", 1)
parameter("_limit", 3) // Ограничиваем количество результатов
}
println("Статус: ${response.status}")
println("Тело ответа: ${response.bodyAsText()}")
} catch (e: Exception) {
println("Ошибка: ${e.message}")
} finally {
client.close()
}
}
Объяснение:
- Блок
{ ... }послеget(...)— это конфигурация запроса parameter("userId", 1)— добавляет?userId=1к URL- Можно добавить несколько параметров — они объединятся через
&
Пример 3: POST-запрос с телом (создание ресурса)
Теперь отправим данные на сервер. Создадим новый пост:
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
suspend fun main() {
val client = HttpClient(OkHttp)
try {
// Создаём JSON-строку вручную (пока без сериализации)
val newPost = """
{
"userId": 1,
"title": "Мой первый пост",
"body": "Это текст моего первого поста на Kotlin!"
}
""".trimIndent()
// Отправляем POST-запрос
val response = client.post("https://jsonplaceholder.typicode.com/posts") {
// Указываем тип содержимого — JSON
contentType(ContentType.Application.Json)
// Передаём тело запроса
setBody(newPost)
}
println("Статус: ${response.status}") // Должен быть 201 Created
println("Созданный объект: ${response.bodyAsText()}")
} catch (e: Exception) {
println("Ошибка: ${e.message}")
} finally {
client.close()
}
}
Объяснение:
contentType(ContentType.Application.Json)— говорим серверу, что отправляем JSONsetBody(newPost)— кладём JSON-строку в тело запроса- Сервер возвращает
201 Createdи сам созданный объект с новым ID
Пример 4: Работа с заголовками (авторизация)
Многие API требуют ключ доступа. Передаём его в заголовке:
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
suspend fun main() {
val client = HttpClient(OkHttp)
try {
val response = client.get("https://api.example.com/protected/data") {
// Добавляем заголовок авторизации
header("Authorization", "Bearer my-secret-token-12345")
// Можно добавить любой другой заголовок
header("User-Agent", "KotlinApp/1.0")
// Пользовательский заголовок
header("X-Custom-Header", "my-value")
}
println("Статус: ${response.status}")
println("Ответ: ${response.bodyAsText()}")
} catch (e: Exception) {
println("Ошибка: ${e.message}")
} finally {
client.close()
}
}
Объяснение:
header("Authorization", "Bearer ...")— стандартный способ передать токен- Заголовки — это пары «ключ: значение»
- Некоторые API требуют специфические заголовки — читай документацию
Пример 5: Автоматическая сериализация JSON (с data-классами)
Вручную писать JSON-строки неудобно. Используем kotlinx.serialization для автоматического преобразования:
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
// Определяем структуру данных (data-класс)
@Serializable
data class Post(
val userId: Int,
val id: Int = 0, // Значение по умолчанию — сервер сам назначит ID
val title: String,
val body: String
)
suspend fun main() {
// Создаём клиента с поддержкой JSON
val client = HttpClient(OkHttp) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
})
}
}
try {
// Создаём объект Post
val newPost = Post(
userId = 1,
title = "Пост с сериализацией",
body = "Kotlin сам преобразует объект в JSON!"
)
// Отправляем POST — Ktor сам сериализует объект
val response = client.post("https://jsonplaceholder.typicode.com/posts") {
contentType(ContentType.Application.Json)
setBody(newPost) // Передаём объект, не строку!
}
println("Статус: ${response.status}")
// Десериализуем ответ обратно в объект Post
val createdPost: Post = response.body()
println("Созданный пост: $createdPost")
println("ID нового поста: ${createdPost.id}")
} catch (e: Exception) {
println("Ошибка: ${e.message}")
} finally {
client.close()
}
}
Объяснение:
@Serializable— аннотация, которая говорит Kotlin, что этот класс можно превращать в JSONdata class Post(...)— data-класс автоматически генерируетtoString(),equals()и другие методыresponse.body<Post>()— Ktor сам преобразует JSON-ответ в объект Post- Не нужно писать JSON вручную — всё делает библиотека
Пример 6: Обработка ошибок и разных кодов ответа
Не все запросы успешны. Научимся обрабатывать разные ситуации:
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
suspend fun main() {
val client = HttpClient(OkHttp) {
// Настраиваем таймауты (чтобы не ждать вечно)
install(HttpTimeout) {
requestTimeoutMillis = 5000 // 5 секунд на весь запрос
connectTimeoutMillis = 3000 // 3 секунды на подключение
}
}
try {
// Пробуем получить несуществующий ресурс
val response = client.get("https://jsonplaceholder.typicode.com/posts/99999")
// Проверяем код ответа
when (response.status.value) {
in 200..299 -> {
println("✅ Успех! Статус: ${response.status}")
println("Данные: ${response.bodyAsText()}")
}
404 -> {
println("❌ Ресурс не найден (404)")
}
401, 403 -> {
println("🔒 Нет доступа (${response.status})")
}
in 500..599 -> {
println("💥 Ошибка сервера (${response.status})")
}
else -> {
println("⚠️ Неизвестный статус: ${response.status}")
}
}
} catch (e: HttpRequestTimeoutException) {
println("⏰ Таймаут запроса: ${e.message}")
} catch (e: Exception) {
println("❌ Общая ошибка: ${e.message}")
} finally {
client.close()
}
}
Объяснение:
HttpTimeout— плагин для настройки таймаутовwhen (response.status.value)— проверяем код ответа по диапазонамHttpRequestTimeoutException— специальное исключение для таймаутов- Всегда обрабатывай разные коды ответа — пользователь должен понимать, что пошло не так
Пример 7: Полноценный пример — получение и отображение погоды
Соберём всё вместе. Используем публичное API погоды (wttr.in — не требует ключа):
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
// Структура для ответа от wttr.in (упрощённая)
@Serializable
data class WeatherResponse(
val current_condition: List? = null
)
@Serializable
data class CurrentCondition(
val temp_C: String? = null,
val weatherDesc: List? = null,
val humidity: String? = null,
val windspeedKmph: String? = null
)
@Serializable
data class WeatherDesc(
val value: String? = null
)
suspend fun main() {
// Создаём клиента с JSON
val client = HttpClient(OkHttp) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
})
}
}
try {
println("🌤️ Запрашиваю погоду для Москвы...")
// GET-запрос к wttr.in с параметрами
val response = client.get("https://wttr.in/Moscow") {
// Параметры: формат JSON, без картинок
parameter("format", "j1")
parameter("lang", "ru")
}
if (response.status.value == 200) {
// Преобразуем ответ в объект WeatherResponse
val weather: WeatherResponse = response.body()
// Извлекаем данные
val current = weather.current_condition?.firstOrNull()
if (current != null) {
println("✅ Погода получена!")
println("🌡️ Температура: ${current.temp_C}°C")
println("☁️ Описание: ${current.weatherDesc?.firstOrNull()?.value ?: "Нет данных"}")
println("💧 Влажность: ${current.humidity}%")
println("💨 Ветер: ${current.windspeedKmph} км/ч")
} else {
println("❌ Не удалось получить данные о погоде")
}
} else {
println("❌ Ошибка сервера: ${response.status}")
}
} catch (e: Exception) {
println("❌ Ошибка: ${e.message}")
} finally {
client.close()
}
}
Объяснение:
- Создаём три data-класса, которые соответствуют структуре JSON от wttr.in
- Используем
? = nullдля полей, которых может не быть в ответе firstOrNull()— безопасно получаем первый элемент списка или null- Оператор
?:(Элвис) — если слева null, используем значение справа
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаком с Python, тебе будет проще. Давай сравним подходы:
| Аспект | Kotlin (Ktor) | Python (requests) |
|---|---|---|
| Библиотека | Ktor Client (встроенная поддержка корутин) | requests (сторонняя, но стандарт де-факто) |
| Асинхронность | По умолчанию асинхронный (suspend функции) | Синхронный по умолчанию (нужен asyncio + aiohttp) |
| Типизация | Строгая, data-классы для JSON | Динамическая, словари (dict) |
| Сериализация | Автоматическая через kotlinx.serialization | Ручная через json.dumps() / json.loads() |
| Обработка ошибок | try-catch с типами исключений | try-except с проверкой status_code |
| GET-запрос | client.get(url) |
requests.get(url) |
| POST с JSON | client.post(url) { setBody(obj) } |
requests.post(url, json=data) |
| Параметры URL | parameter("key", "value") |
params={"key": "value"} |
Пример на Python для сравнения:
import requests
# GET-запрос
response = requests.get("https://jsonplaceholder.typicode.com/posts/1")
print(response.status_code)
print(response.json()) # Автоматически парсит JSON в словарь
# POST-запрос
data = {"userId": 1, "title": "Привет", "body": "Мир"}
response = requests.post("https://jsonplaceholder.typicode.com/posts", json=data)
print(response.status_code)
print(response.json())
💡 Главное отличие: Kotlin требует больше кода для настройки, но даёт безопасность типов и производительность. Python быстрее писать, но легче допустить ошибку с типами данных.
Типичные ошибки и подводные камни
Даже опытные разработчики иногда попадают в эти ловушки. Вот самые частые проблемы:
1. Забыл закрыть HttpClient
Каждый созданный HttpClient использует системные ресурсы (сетевые соединения, потоки). Если не закрыть его, произойдёт утечка памяти.
// ❌ ПЛОХО: клиент не закрыт
fun badRequest() {
val client = HttpClient(OkHttp)
client.get("https://example.com")
// client.close() забыли!
}
// ✅ ХОРОШО: используем use() для автоматического закрытия
fun goodRequest() {
HttpClient(OkHttp).use { client ->
client.get("https://example.com")
} // client.close() вызывается автоматически
}
2. Игнорирование кодов ответа
Не все ответы с кодом 200 — успешны. Некоторые API возвращают 200 с ошибкой внутри JSON.
// ❌ ПЛОХО: проверяем только статус
val response = client.get("https://api.example.com/data")
if (response.status.value == 200) {
// Может быть ошибка внутри JSON!
val data = response.body<MyData>()
}
// ✅ ХОРОШО: проверяем и статус, и содержимое
val response = client.get("https://api.example.com/data")
if (response.status.value == 200) {
val body = response.bodyAsText()
if (body.contains("error")) {
println("API вернуло ошибку: $body")
} else {
val data = response.body<MyData>()
}
}
3. Неправильный Content-Type
Сервер может не понять, что ты отправляешь JSON, если не указать заголовок.
// ❌ ПЛОХО: не указан тип содержимого
client.post("https://api.example.com/data") {
setBody("""{"key": "value"}""")
}
// ✅ ХОРОШО: явно указываем Content-Type
client.post("https://api.example.com/data") {
contentType(ContentType.Application.Json)
setBody("""{"key": "value"}""")
}
4. Таймауты (запрос зависает навсегда)
Если сервер не отвечает, программа может зависнуть на неопределённое время. Всегда устанавливай таймауты.
// ❌ ПЛОХО: без таймаута
val client = HttpClient(OkHttp)
// ✅ ХОРОШО: с таймаутами
val client = HttpClient(OkHttp) {
install(HttpTimeout) {
requestTimeoutMillis = 10000 // 10 секунд
connectTimeoutMillis = 5000 // 5 секунд
}
}