Модульний монстр, або Альтернатива Retrofit — Ktor Client

Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!

Привіт, у цій статті я розповім про гнучку альтернативу улюбленому серед Android-розробників Retrofit, а саме — про Ktor Client. Буде цікава історія та багато практики.

Але спочатку представлюсь. Звуть мене Богданом, вайтішник, півроку досвіду, бос всіх павуків. В основному займаюсь Backend-розробкою з використанням Ktor (так, він є як клієнт та як бекенд, Ktor Client та Ktor Backend відповідно)

Моє знайомство з Ktor почалося, коли я захотів бодай спробувати написати застосунок із використанням Kotlin Multiplatform Mobile (спойлер — не вдалось, бо хотів таким чином почати знайомство зі SwiftUI, але потрібен макбук), але, як виявилось, Retrofit не підтримує KMM, тож мені довелось шукати альтернативи. Єдиною такою виявився Ktor, і з’ясувалося, недарма мої бажання змусили мене використовувати саме його.

Перше, що я зробив — це відкрив документацію без надії на неї, але мені досі нічого окрім неї не знадобилось для роботи з Ktor-клієнтом (так, це фреймворк з нормальною документацією, що часто оновлюється, щось рідке десь здохло, шкода не путін).

Перший пункт там — це підтримувані платформи, а саме JavaScript, JVM, Android, iOS, watchOS, Wear OS (через Android), tvOS, macOS, Linux та Windows. Нічого незвичайного, просто бібліотека, що працює на 10 платформах, 6 з яких у корені різні, не зважайте.

Ну, не буду томити, перейдемо до практики. Використовувати будемо Cat Facts API. Щоб почати нам варто створити новий Kotlin-проєкт у IDE, яку ви бажаєте використовувати (я користуюсь IntelliJ IDEA Ultimate) та трохи підрихтувати Gradle.

Додаємо Ktor у проєкт

Без прописаних залежностей у Gradle нічого не буде, як би нудно не було, але треба. Ktor складається з трьох компонентів — ядра (core), двигуна (engine) та плагінів (plugins).

Залежність для ядра:

// build.gradle.kts
implementation("io.ktor:ktor-client-core:$ktor_version")


// build.gradle
implementation "io.ktor:ktor-client-core:$ktor_version"


// Apache Maven
<dependency> 
    <groupId>io.ktor</groupId>
    <artifactId>ktor-client-cio-jvm</artifactId>
    <version>${ktor_version}</version>
</dependency>

І для двигуна (у нашому випадку — CIO, Coroutine I/O):

// build.gradle.kts
implementation("io.ktor:ktor-client-cio:$ktor_version")


// build.gradle
implementation "io.ktor:ktor-client-cio:$ktor_version"


// Apache Maven
<dependency>
    <groupId>io.ktor</groupId>
    <artifactId>ktor-client-cio-jvm</artifactId>
    <version>${ktor_version}</version>
</dependency>

Все! Ctrl+Shift+O, щоб засинкувати — і ви зірка.

Створюємо клієнт

І тут Ktor проявляє гнучкість сильнішу, ніж у балерини, — те, за що я так його люблю. Нескінченні комбінації плагінів та клієнтів — все у ваших руках! (Можна і OkHttp, дідусі, нікуди ваші інтерцептори не дінуться, всі посилання на ресурси внизу).

Для початку створимо дещо дуже просте:

fun main() {
    val client = HttpClient(CIO)
}

Тут ми передаємо двигун у конструктор клієнта (це необов’язково робити, якщо ми маємо у залежностях Gradle тільки один двигун, тупо HttpClient() і все працюватиме).

Привіт, Інтернете!

Клієнт створили, настав час його використати. Підберімо один з урлів у Cat Facts API, наприклад /fact

suspend fun main() {  
    val client = HttpClient(CIO)  
    val response = client.get("https://catfact.ninja/fact")  // get-запит до API
    println(response.bodyAsText())  // bodyAsText() перетворює тіло відповіді у текст, або падає якщо там не текст
}

Помітили, що main() став suspend-функцією? Ktor використовує корутини «під капотом» для асинхронщини. Не знаю, як вам, а от мені не терпиться нарешті запустити та дізнатись цікавий факт про котиків.

{"fact":"A cat lover is called an Ailurophilia (Greek: cat+lover).","length":57}

Отже, я айлюрофіл.

Тітко Галю, у нас безлад в stdout

Як ми використаємо отримані дані? Треба ж якось JSON парсити, чи що? Для цього добрі люди створили Content Negotiation. І тут ми приходимо до першого плагіну.

Кожен плагін завжди треба підключати у Gradle окремо. Цей плагін особливий — з ним нам треба підключити і бібліотеки для серіалізації. Ktor офіційно підтримує Serialization API, Gson та Jackson.

Ось його залежності:

// build.gradle.kts
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")


// build.gradle
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"


// Apache Maven
<dependency>
    <groupId>io.ktor</groupId>
    <artifactId>ktor-client-content-negotiation-jvm</artifactId
    <version>${ktor_version}</version>
</dependency>

Та залежності Serialization API (Gson та Jackson покриваються тут):

// build.gradle.kts
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")

// не забудьте про плагін компілятора
plugins {
    kotlin("jvm") version "1.8.21"
    kotlin("plugin.serialization") version "1.8.21"
}


// build.gradle
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"

// не забудьте про плагін компілятора
plugins { 
    id 'org.jetbrains.kotlin.jvm' version '1.8.21'
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21'
}


// Apache Maven
<dependency>
    <groupId>io.ktor</groupId>
    <artifactId>ktor-serialization-kotlinx-json-jvm</artifactId
    <version>${ktor_version}</version>
</dependency>
// а як у Maven плагіни компілятора підключити документація не каже

До речі, Serialization API підтримує CBOR, Protobuf та XML, тому я рекомендую саме його.

Щоб встановити плагін нам треба відредагувати HttpClient() якось так:

val client = HttpClient(CIO) {
    install(ContentNegotiation)
}

І час натравити його на JSON. Тузік, тримай грілку:

install(ContentNegotiation) {
    json()
}

Для Gson і Jackson просто замінюємо json() на gson() та jackson() відповідно. Всередину також можна передати кастомні серіалізатори Json (Serialization API), Gson (Gson) та ObjectMapper (Jackson), в залежності від того, що ви використовуєте.

От, наприклад, Микола з бекенду любить додавати неочікувані поля, а вам треба зберегти зворотну сумісність, щоб користувачам старих версій не довелось купувати новий смартфон тупо через цього Миколу. Можна просто ігнорити неочікувані поля, якось так:

// Serialization API
json(
    Json {
        ignoreUnknownKeys = true
    }
)

// Jackson
jackson {  
    configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)  
}

Gson буде ігнорувати невідомі поля за дефолтом, принаймні так стверджує документація, тому просто передаємо дефолтний Gson:

// Gson
gson {
    
}

Все, ми налаштували наш серіалізатор, тепер час створити моделі та отримувати вже чисті дані, щоб ніяких тіток Галь не кликати. Можна усе городити в один файл, але я рекомендую написати модель в окремому файлі.

У нашого JSON є поле «fact», воно те, що треба:

@Serializable // приберіть анотацію якщо використовуєте щось інше окрім Serialization API
data class Fact(val fact: String)

Окей, факти ми отримуємо, JSON серіалізуємо, але що, як ми хочемо відправити параметр? Наприклад, Cat Facts API іноді повертає задовгі факти, а у нас поганий інтернет і, о, щастя, там є query-параметр, що обмежує довжину факту. Модифікуємо реквест:

val response = client.get("https://catfact.ninja/fact") { // get-запит до API  
    url {  
        parameters.append("max_length", "60")  // додаємо параметри
    }  
}

Все, тепер навіть у лихі часи з поганим інтернетом ми дізнаємося, що від тигра у воді не сховаєшся...

Tigers are excellent swimmers and do not avoid water.

Погралися, і годі

Як зливати дані гуглу, або Як користуватись cookies у Ktor

Ну, а тепер до серйозніших речей. Багато API мають XSRF-токен, цей API віддає його як cookie, що спрощує нам дуже багато речей. Тому у клієнта можна спокійно встановити плагін для кукі і ніяк його навіть не конфігурувати.

Навіть у Gradle лізти не треба, бо цей плагін іде разом з ядром, просто у конфігурації клієнта одразу після install(ContentNegotiation) пишемо:

install(HttpCookies)

І все! Цього достатньо, щоб клієнт сам віддавав cookies назад. Якщо цікаво, як це працює «під капотом», то запускайте дебагер, бо якщо його запущу я, то ця стаття вже вийде про нутрощі Ktor, хе-хе.

Як дати гуглу знати, хто зливає дані, або User-Agent

У Cat Facts API User-Agent необов’язковий, але у багатьох інших API (і не тільки API, а ще й для скрапінгу) його треба встановлювати.

install(UserAgent) {
    agent = "My cool secret user agent"
}

Можна і Curl-подібний, і браузероподібний
val client = HttpClient(CIO) {
    BrowserUserAgent()
    // ... або
    CurlUserAgent()
}

А знаєте, що найприємніше? Не треба копирсатися у Gradle, цей плагін іде з ядром, як і HttpCookies.

Злиття даних за дефолтом, або Параметри запиту за замовчуванням

А у нас вже виходить повноцінний (та гнучкий!) клієнт:

val client = HttpClient(CIO) {
    install(ContentNegotiation) {
        json(
            Json {
                ignoreUnknownKeys = true
            }
        )
    }
    install(HttpCookies)
    install(UserAgent) {
        agent = "My cool secret user agent"
    }
}

Але коли цей клієнт часто використовують, усюди з’являється стрічка з одним і тим самим початком — URL. А що, як API переїде? Не бігати ж всюди та міняти базовий урл? Можна про це подбати зараз та встановити параметри запиту за замовчуванням:

install(DefaultRequest) {
    url {
        host = "catfact.ninja"
        protocol = URLProtocol.HTTPS
    }
}

...і прибрати base url із запиту

val response = client.get("fact") { ... }

Ну що, клієнт написали, все як положено, все працює, є ще про що розповісти, але пора закінчувати. Приємної кави, бажаю менше багів та Слава Україні!

А тепер, найсолодше

Як я і обіцяв, ось посилання на цікаві ресурси:

Буде друга частина ;-)

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному4
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дякую за цікаву статтю!

Вмієте ви Богдане зацікавити читача. Не думав цю статтю читати, коли переходив по заголовку, але here we are :)

Через дві статті я здивую тебе ще більше, як і весь доу. І трішки спойлерів — у другій частині ми з читачем (а як же без нього?) напишемо мультиплатформенний SDK для отримання цікавих фактів про котів та використаємо його через Kotlin/JS. Гадаю, базова робота з Kotlin/JS зацікавить велику частину DOU та буде шанс потрапити у канал фронтендерів. У мене також є плани і на бекендерів, і на реактистів (щоправда мені реакт треба підучити, в останній раз я джаваскрипт взагалі 6 років назад мацав)

Я колись бавився з Флаттером, який всередині використовував лібу на Kotlin Multiplatform. Виглядало все дуже цікаво. Правда, в мультиплатформеному колтіні не підтримувався тоді API format(), прийшлося самому з джава сорсів спрошувати і робити лібу — github.com/Simplx-dev/kotlin-format

Жесть. На щастя зараз є більш просунуті та мультиплатформенні аналоги String.format()

Підписатись на коментарі