🎯 Kotlin

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) — создаём клиента на движке OkHttp
  • client.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) — говорим серверу, что отправляем JSON
  • setBody(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, что этот класс можно превращать в JSON
  • data 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 секунд
    }
}

5. Неправильная сериализация (поля не совпадают)