Модульний монстр, або Альтернатива 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") { ... }
Ну що, клієнт написали, все як положено, все працює, є ще про що розповісти, але пора закінчувати. Приємної кави, бажаю менше багів та Слава Україні!
А тепер, найсолодше
Як я і обіцяв, ось посилання на цікаві ресурси:
- Увесь той код, що ми писали.
- Документація.
- Replacing Retrofit with Ktor Client and Kotlin Serialization for Android — Medium.
- Мій улюблений розділ документації.
Буде друга частина ;-)
5 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів