Ktor كعميل HTTP لالروبوت

أحب Retrofit2 كمطور لنظام Android ، ولكن ماذا عن محاولة الحصول على جودة عميل Ktor HTTP؟ في رأيي ، بالنسبة لتطور Android ، فإنه ليس أسوأ ولا أفضل ، إنه أحد الخيارات ، على الرغم من أنه إذا قمت بلف كل شيء قليلاً ، فيمكن أن يتحول بشكل جيد للغاية. سأدرس الميزات الأساسية التي سيكون من الممكن البدء باستخدام Ktor كعميل HTTP - إنشاء أنواع مختلفة من الطلبات ، وتلقي الردود الأولية والإجابات في شكل نص ، وإلغاء تسلسل json إلى فئات عبر المحولات ، والتسجيل.



بشكل عام ، Ktor هو إطار يمكن أن يعمل كعميل HTTP. سوف أعتبر ذلك من جانب التطوير لنظام Android. من غير المحتمل أن ترى أدناه حالات استخدام معقدة للغاية ، ولكن الميزات الأساسية مؤكدة. يمكن الاطلاع على الكود من الأمثلة أدناه على جيثب .

يستخدم Ktor coroutines من Kotlin 1.3 ، ويمكن الاطلاع على قائمة من القطع الأثرية المتاحة هنا ، الإصدار الحالي هو 1.0.1 .
للاستعلامات ، سأستخدم HttpBin .

استخدام بسيط


للبدء ، ستحتاج إلى تبعيات أساسية لعميل Android:

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

لا تنس إضافة معلومات إلى مانيفست تستخدمها للإنترنت.

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

دعونا نحاول الحصول على استجابة الخادم كسلسلة ، ماذا يمكن أن يكون أسهل؟

 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) } } 

يمكنك إنشاء عميل بدون معلمات ، فقط قم بإنشاء مثيل HttpClient() . في هذه الحالة ، ستقوم Ktor بتحديد المحرك المطلوب واستخدامه بالإعدادات الافتراضية (لدينا محرك واحد متصل - Android ، ولكن هناك آخرون ، على سبيل المثال ، OkHttp).
لماذا coroutines؟ لأن get() هي وظيفة suspend .

ما الذي يمكن عمله بعد ذلك؟ لديك بالفعل بيانات من الخادم في شكل سلسلة ، وهو ما يكفي لتحليلها والحصول على فئات يمكنك العمل معها بالفعل. يبدو أنه بسيط وسريع في حالة الاستخدام هذه.

نحصل على إجابة الخام


في بعض الأحيان قد يكون من الضروري الحصول على مجموعة من وحدات البايت بدلاً من سلسلة. في الوقت نفسه ، قم بتجربة عدم التزامن.

 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() }) } 

في الأماكن التي يتم HttpClient أساليب HttpClient ، مثل call() و get() ، سيتم استدعاء await() تحت الغطاء. لذلك في هذه الحالة ، ستكون المكالمات إلى simpleCase() و bytesCase() متسلسلة دائمًا. أنت في حاجة إليها بالتوازي - فقط قم بلف كل مكالمة في coroutine منفصل. في هذا المثال ، ظهرت طرق جديدة. call(GET_UUID) كائنًا يمكننا من خلاله الحصول على معلومات حول الطلب وتكوينه واستجابته call(GET_UUID) . يحتوي الكائن على الكثير من المعلومات المفيدة - من رمز الاستجابة وإصدار البروتوكول إلى القناة بنفس البايتات.

هل تحتاج إلى إغلاقه بطريقة أو بأخرى؟


يشير المطورون إلى أنه حتى يتم إيقاف تشغيل محرك HTTP بشكل صحيح ، تحتاج إلى استدعاء الأسلوب close() على العميل. إذا كنت بحاجة إلى إجراء مكالمة واحدة وإغلاق العميل على الفور ، فيمكنك استخدام طريقة use{} ، حيث تقوم HttpClient بتنفيذ واجهة Closable .

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

أمثلة إلى جانب الحصول على


في عملي ، الطريقة الثانية الأكثر شيوعًا هي POST . اطلع على مثال إعداد المعلمات والرؤوس ونص الطلب.

 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") } ) } 

في الواقع ، في المعلمة الأخيرة للدالة post() ، يمكنك الوصول إلى HttpRequestBuilder ، والتي يمكنك من خلالها تكوين أي طلب.
طريقة post() ببساطة بتوزيع السلسلة وتحويلها إلى عنوان URL وتعيين نوع الطريقة بشكل صريح وتقديم طلب.

 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) } 

إذا قمت بتنفيذ الرمز من طريقتين أخريين ، فستكون النتيجة مماثلة. الفرق ليس كبيرا ، ولكن استخدام الأغلفة أكثر ملاءمة. الموقف مشابه لـ put() ، delete() ، patch() ، head() و options() ، لذلك لن نفكر فيها.

ومع ذلك ، إذا نظرت عن كثب ، يمكنك أن ترى أن هناك اختلافًا في الكتابة. عند call() تحصل على إجابة منخفضة المستوى ويجب عليك قراءة البيانات بنفسك ، ولكن ماذا عن الكتابة التلقائية؟ بعد كل شيء ، نحن معتادون على توصيل محول (مثل Gson ) في Retrofit2 والإشارة إلى نوع الإرجاع كفئة معينة. سنتحدث عن التحويل إلى فئات في وقت لاحق ، لكن طريقة request ستساعد في تحديد النتيجة دون ربطها بأسلوب HTTP محدد.

 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) } 

تقديم بيانات النموذج


عادة ما تحتاج إلى تمرير المعلمات إما في سلسلة الاستعلام أو في النص. في المثال أعلاه ، درسنا بالفعل كيفية القيام بذلك باستخدام HttpRequestBuilder . ولكن يمكن أن يكون أسهل.

تقبل الدالة submitForm عنوان url كسلسلة ومعلمات للطلب submitForm منطقية توضح كيفية تمرير المعلمات - في سطر الطلب أو كأزواج في النموذج.

 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) } 

ولكن ماذا عن البيانات متعددة الأجزاء / النماذج؟


بالإضافة إلى أزواج السلسلة ، يمكنك تمرير أرقام طلب POST ومصفوفات البايت وتدفقات الإدخال المختلفة كمعلمات. الاختلافات في وظيفة وتشكيل المعلمة. نحن تبدو مثل:

 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) } 

كما لاحظت - يمكنك أيضًا إرفاق مجموعة من الرؤوس بكل معلمة.

إلغاء تسلسل الإجابة إلى الفصل


تحتاج إلى الحصول على بعض البيانات من الطلب ، ليس كسلسلة أو بايت ، ولكن يتم تحويلها على الفور إلى فصل دراسي. بادئ ذي بدء ، في الوثائق نوصي بتوصيل ميزة للعمل مع json ، لكنني أريد أن أبدي تحفظًا أن jvm يحتاج إلى تبعية محددة وبدون تسلسل kotlinx ، لن ينطلق كل هذا. أقترح استخدام Gson كمحول (هناك روابط إلى المكتبات المدعومة الأخرى في الوثائق ، وستكون روابط الوثائق في نهاية المقال).

مستوى بناء المشروع:

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

مستوى التطبيق 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" } 

الآن تنفيذ الطلب. من الجديد ، لن يكون هناك سوى اتصال بميزة العمل مع Json عند إنشاء العميل. سأستخدم API الطقس المفتوحة. للتأكد من اكتمالها ، سأعرض نموذج البيانات.

 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()) } 

وماذا يمكن


على سبيل المثال ، يعرض الخادم خطأ ، ولديك الرمز كما في المثال السابق. في هذه الحالة ، ستتلقى خطأ في التسلسل ، ولكن يمكنك تكوين العميل بحيث يتم طرح خطأ BadResponseStatus عندما يكون رمز الاستجابة <300. يكفي لتعيين expectSuccess إلى true عند بناء العميل.

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

عند تصحيح الأخطاء ، قد يكون التسجيل مفيدًا. فقط إضافة تبعية واحدة وتكوين العميل.

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

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

نحدد مسجل DEFAULT وكل شيء سينتقل إلى LogCat ، ولكن يمكنك إعادة تعريف الواجهة وإنشاء المسجل الخاص بك إذا كنت ترغب في ذلك (على الرغم من أنني لم أر أي فرص كبيرة هناك ، إلا أن هناك رسالة فقط عند الإدخال ، لكن لا يوجد مستوى سجل). نشير أيضًا إلى مستوى السجلات التي يجب أن تنعكس.

المراجع


ما لا يعتبر:

  • العمل مع محرك OkHttp
  • إعدادات المحرك
  • محرك وهمية والاختبار
  • وحدة التخويل
  • افصل بين الميزات مثل تخزين ملفات تعريف الارتباط بين الطلبات ، إلخ.
  • كل ما لا ينطبق على عميل HTTP لنظام Android (الأنظمة الأساسية الأخرى ، العمل من خلال مآخذ ، تنفيذ الخادم ، إلخ.

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


All Articles