Ktor sebagai klien HTTP untuk Android

Saya suka Retrofit2 sebagai pengembang Android, tetapi bagaimana dengan mencoba mendapatkan kualitas klien HTTP Ktor? Menurut pendapat saya, untuk pengembangan Android itu tidak lebih buruk dan tidak lebih baik, hanya salah satu opsi, meskipun jika Anda membungkus semuanya sedikit, itu bisa berubah dengan sangat baik. Saya akan mempertimbangkan fitur-fitur dasar yang memungkinkan untuk mulai menggunakan Ktor sebagai klien HTTP - membuat berbagai jenis permintaan, menerima tanggapan dan jawaban mentah dalam bentuk teks, menghapus json ke dalam kelas melalui konverter, log.



Secara umum, Ktor adalah kerangka kerja yang dapat bertindak sebagai klien HTTP. Saya akan mempertimbangkannya dari sisi pengembangan untuk Android. Sepertinya Anda tidak akan melihat kasus penggunaan yang sangat rumit di bawah ini, tetapi fitur dasarnya pasti. Kode dari contoh di bawah ini dapat dilihat di GitHub .

Ktor menggunakan coroutine dari Kotlin 1.3, daftar artefak yang tersedia dapat ditemukan di sini , versi saat ini adalah 1.0.1 .
Untuk pertanyaan, saya akan menggunakan HttpBin .

Penggunaan sederhana


Untuk memulai, Anda akan memerlukan dependensi dasar untuk klien Android:

 implementation "io.ktor:ktor-client-core:1.0.1" implementation "io.ktor:ktor-client-android:1.0.1" 

Jangan lupa menambahkan informasi ke Manifest bahwa Anda menggunakan Internet.

 <uses-permission android:name="android.permission.INTERNET"/> 

Mari kita coba untuk mendapatkan respons server sebagai string, apa yang bisa lebih mudah?

 private const val BASE_URL = "https://httpbin.org" private const val GET_UUID = "$BASE_URL/uuid" fun simpleCase() { val client = HttpClient() GlobalScope.launch(Dispatchers.IO) { val data = client.get<String>(GET_UUID) Log.i("$BASE_TAG Simple case ", data) } } 

Anda dapat membuat klien tanpa parameter, cukup buat instance HttpClient() . Dalam hal ini, Ktor akan memilih mesin yang diinginkan dan menggunakannya dengan pengaturan default (kami memiliki satu mesin yang terhubung - Android, tetapi ada yang lain, misalnya, OkHttp).
Mengapa coroutine? Karena get() adalah fungsi suspend .

Apa yang bisa dilakukan selanjutnya? Anda sudah memiliki data dari server dalam bentuk string, itu sudah cukup untuk menguraikannya dan mendapatkan kelas yang sudah bisa Anda gunakan. Tampaknya sederhana dan cepat dalam hal penggunaan ini.

Kami mendapat jawaban mentah


Kadang-kadang mungkin perlu untuk mendapatkan satu set byte daripada string. Pada saat yang sama, bereksperimenlah dengan asinkron.

 fun performAllCases() { GlobalScope.launch(Dispatchers.IO) { simpleCase() bytesCase() } } suspend fun simpleCase() { val client = HttpClient() val data = client.get<String>(GET_UUID) Log.i("$BASE_TAG Simple case", data) } suspend fun bytesCase() { val client = HttpClient() val data = client.call(GET_UUID).response.readBytes() Log.i("$BASE_TAG Bytes case", data.joinToString(" ", "[", "]") { it.toString(16).toUpperCase() }) } 

Di tempat-tempat di mana metode HttpClient , seperti call() dan get() , await() akan dipanggil di bawah tenda. Jadi dalam hal ini panggilan ke simpleCase() dan bytesCase() akan selalu berurutan. Anda membutuhkannya secara paralel - cukup bungkus setiap panggilan dalam coroutine terpisah. Dalam contoh ini, metode baru telah muncul. Panggilan call(GET_UUID) akan mengembalikan objek tempat kami dapat memperoleh informasi tentang permintaan, konfigurasi, respons, dan klien. Objek berisi banyak informasi berguna - dari kode respons dan versi protokol ke saluran dengan byte yang sama.

Apakah Anda perlu menutupnya?


Pengembang menunjukkan bahwa agar mesin HTTP untuk dimatikan dengan benar, Anda perlu memanggil metode close() pada klien. Jika Anda perlu melakukan satu panggilan dan segera menutup klien, Anda dapat menggunakan metode use{} , karena HttpClient mengimplementasikan antarmuka Closable .

 suspend fun closableSimpleCase() { HttpClient().use { val data: String = it.get(GET_UUID) Log.i("$BASE_TAG Closable case", data) } } 

Contoh selain GET


Dalam pekerjaan saya, metode kedua yang paling populer adalah POST . Pertimbangkan contoh pengaturan parameter, tajuk dan badan permintaan.

 suspend fun postHeadersCase(client: HttpClient) { val data: String = client.post(POST_TEST) { fillHeadersCaseParameters() } Log.i("$BASE_TAG Post case", data) } private fun HttpRequestBuilder.fillHeadersCaseParameters() { parameter("name", "Andrei") // +     url.parameters.appendAll( parametersOf( "ducks" to listOf("White duck", "Grey duck"), // +      "fish" to listOf("Goldfish") // +     ) ) header("Ktor", "https://ktor.io") // +  headers /*       */ { append("Kotlin", "https://kotl.in") } headers.append("Planet", "Mars") // +  headers.appendMissing("Planet", listOf("Mars", "Earth")) // +   , "Mars"   headers.appendAll("Pilot", listOf("Starman")) //     body = FormDataContent( //  ,     form Parameters.build { append("Low-level", "C") append("High-level", "Java") } ) } 

Bahkan, dalam parameter terakhir dari fungsi post() , Anda memiliki akses ke HttpRequestBuilder , yang dengannya Anda dapat membentuk permintaan apa pun.
Metode post() mem-parsing string, mengkonversinya menjadi URL, secara eksplisit menetapkan jenis metode, dan membuat permintaan.

 suspend fun rawPostHeadersCase(client: HttpClient) { val data: String = client.call { url.takeFrom(POST_TEST) method = HttpMethod.Post fillHeadersCaseParameters() } .response .readText() Log.i("$BASE_TAG Raw post case", data) } 

Jika Anda menjalankan kode dari dua metode terakhir, hasilnya akan serupa. Perbedaannya tidak besar, tetapi menggunakan pembungkus lebih nyaman. Situasinya mirip dengan put() , delete() , patch() , head() dan options() , jadi kami tidak akan mempertimbangkannya.

Namun, jika Anda perhatikan dengan seksama, Anda dapat melihat bahwa ada perbedaan dalam pengetikan. Saat Anda menelepon call() Anda mendapatkan jawaban tingkat rendah dan Anda harus membaca sendiri datanya, tetapi bagaimana dengan mengetik otomatis? Bagaimanapun, kita semua terbiasa menghubungkan konverter (seperti Gson ) di Retrofit2 dan menunjukkan tipe pengembalian sebagai kelas tertentu. Kita akan berbicara tentang mengonversi ke kelas nanti, tetapi metode request akan membantu menentukan hasil tanpa mengikat ke metode HTTP tertentu.

 suspend fun typedRawPostHeadersCase(client: HttpClient) { val data = client.request<String>() { url.takeFrom(POST_TEST) method = HttpMethod.Post fillHeadersCaseParameters() } Log.i("$BASE_TAG Typed raw post", data) } 

Kirim data formulir


Biasanya Anda perlu meneruskan parameter baik di string kueri atau di badan. Dalam contoh di atas, kami telah memeriksa cara melakukan ini menggunakan HttpRequestBuilder . Tapi itu bisa lebih mudah.

Fungsi submitForm menerima url sebagai string, parameter untuk permintaan dan bendera boolean yang memberi tahu cara meneruskan parameter - di baris permintaan atau sebagai pasangan dalam formulir.

 suspend fun submitFormCase(client: HttpClient) { val params = Parameters.build { append("Star", "Sun") append("Planet", "Mercury") } val getData: String = client.submitForm(GET_TEST, params, encodeInQuery = true) //     val postData: String = client.submitForm(POST_TEST, params, encodeInQuery = false) //   form Log.i("$BASE_TAG Submit form get", getData) Log.i("$BASE_TAG Submit form post", postData) } 

Tapi bagaimana dengan multipart / form-data?


Selain pasangan string, Anda dapat lulus sebagai parameter nomor permintaan POST, array byte dan berbagai aliran Input. Perbedaan dalam fungsi dan pembentukan parameter. Kami terlihat seperti:

 suspend fun submitFormBinaryCase(client: HttpClient) { val inputStream = ByteArrayInputStream(byteArrayOf(77, 78, 79)) val formData = formData { append("String value", "My name is") //   append("Number value", 179) //  append("Bytes value", byteArrayOf(12, 74, 98)) //   append("Input value", inputStream.asInput(), headersOf("Stream header", "Stream header value")) //    } val data: String = client.submitFormWithBinaryData(POST_TEST, formData) Log.i("$BASE_TAG Submit binary case", data) } 

Seperti yang mungkin Anda perhatikan - Anda juga dapat melampirkan satu set header ke setiap parameter.

Deserialize jawaban ke kelas


Anda perlu mendapatkan beberapa data dari permintaan, bukan sebagai string atau byte, tetapi segera dikonversi ke kelas. Untuk memulainya, dalam dokumentasi kami sarankan untuk menghubungkan fitur untuk bekerja dengan json, tetapi saya ingin membuat reservasi bahwa jvm membutuhkan ketergantungan tertentu dan tanpa kotlinx-serialisasi semua ini tidak akan berjalan. Saya sarankan menggunakan Gson sebagai konverter (ada tautan ke perpustakaan lain yang didukung dalam dokumentasi, tautan ke dokumentasi akan ada di akhir artikel).

tingkat proyek build.gradle:

 buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } allprojects { repositories { maven { url "https://kotlin.bintray.com/kotlinx" } } } 

tingkat aplikasi build.gradle:

 apply plugin: 'kotlinx-serialization' dependencies { implementation "io.ktor:ktor-client-json-jvm:1.0.1" implementation "io.ktor:ktor-client-gson:1.0.1" } 

Sekarang jalankan permintaan. Dari yang baru, hanya akan ada koneksi fitur bekerja dengan Json saat membuat klien. Saya akan menggunakan API cuaca terbuka. Untuk kelengkapan, saya akan menunjukkan model data.

 data class Weather( val consolidated_weather: List<ConsolidatedWeather>, val time: String, val sun_rise: String, val sun_set: String, val timezone_name: String, val parent: Parent, val sources: List<Source>, val title: String, val location_type: String, val woeid: Int, val latt_long: String, val timezone: String ) data class Source( val title: String, val slug: String, val url: String, val crawl_rate: Int ) data class ConsolidatedWeather( val id: Long, val weather_state_name: String, val weather_state_abbr: String, val wind_direction_compass: String, val created: String, val applicable_date: String, val min_temp: Double, val max_temp: Double, val the_temp: Double, val wind_speed: Double, val wind_direction: Double, val air_pressure: Double, val humidity: Int, val visibility: Double, val predictability: Int ) data class Parent( val title: String, val location_type: String, val woeid: Int, val latt_long: String ) private const val SF_WEATHER_URL = "https://www.metaweather.com/api/location/2487956/" suspend fun getAndPrintWeather() { val client = HttpClient(Android) { install(JsonFeature) { serializer = GsonSerializer() } } val weather: Weather = client.get(SF_WEATHER_URL) Log.i("$BASE_TAG Serialization", weather.toString()) } 

Dan apa lagi yang bisa


Misalnya, server mengembalikan kesalahan, dan Anda memiliki kode seperti pada contoh sebelumnya. Dalam hal ini, Anda akan menerima kesalahan serialisasi, tetapi Anda dapat mengonfigurasi klien sehingga kesalahan BadResponseStatus dilemparkan ketika kode respons <300. Sudah cukup untuk mengatur expectSuccess menjadi true ketika membangun klien.

  val client = HttpClient(Android) { install(JsonFeature) { serializer = GsonSerializer() } expectSuccess = true } 

Saat debugging, logging mungkin berguna. Cukup tambahkan satu ketergantungan dan konfigurasikan klien.

 implementation "io.ktor:ktor-client-logging-jvm:1.0.1" 

  val client = HttpClient(Android) { install(Logging) { logger = Logger.DEFAULT level = LogLevel.ALL } } 

Kami menentukan DEFAULT logger dan semuanya akan masuk ke LogCat, tetapi Anda dapat mendefinisikan ulang antarmuka dan membuat logger Anda sendiri jika Anda mau (walaupun saya tidak melihat peluang besar di sana, hanya ada pesan di input, tetapi tidak ada level log). Kami juga menunjukkan tingkat log yang perlu direfleksikan.

Referensi:


Apa yang tidak dipertimbangkan:

  • Bekerja dengan mesin OkHttp
  • Pengaturan Mesin
  • Mesin tiruan dan pengujian
  • Modul otorisasi
  • Fitur terpisah seperti menyimpan cookie di antara permintaan, dll.
  • Segala sesuatu yang tidak berlaku untuk klien HTTP untuk Android (platform lain, bekerja melalui soket, implementasi server, dll.

Source: https://habr.com/ru/post/id432310/


All Articles