Ktor作为Android的HTTP客户端

我喜欢Retrofit2作为Android开发人员,但是如何尝试获得Ktor HTTP客户端的质量呢? 以我的观点,对于Android开发来说,这并不坏也没有更好,只是选择之一,尽管如果将所有内容都包装一遍,效果可能会很好。 我将考虑可以开始将Ktor用作HTTP客户端的基本功能-创建各种类型的请求,接收文本形式的原始响应和答案,通过转换器将json反序列化为类以及进行日志记录。



通常,Ktor是一个可以充当HTTP客户端的框架。 我将从Android的开发方面考虑。 下面不太可能看到非常复杂的用例,但是基本功能是准确的。 以下示例中的代码可以在GitHub上查看。

Ktor使用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" 

不要忘了向使用Internet的清单添加信息。

 <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)。
为什么要协程? 因为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()将在await()被调用。 因此,在这种情况下,对simpleCase()bytesCase()的调用将始终是顺序的。 您需要并行处理-只需将每个调用包装在单独的协程中即可。 在此示例中,出现了新方法。 调用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) } } 

GET以外的例子


在我的工作中,第二受欢迎的方法是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()您会得到一个低级答案,您必须自己读取数据,但是自动键入又如何呢? 毕竟,我们都习惯于在Retrofit2中连接转换器(例如Gson )并将返回类型指示为特定类。 稍后我们将讨论转换为类,但是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作为字符串,请求的参数以及一个布尔标志,该标志指示如何传递参数-在请求行中还是在表单中成对传递。

 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请求编号,字节数组和各种Input流作为参数。 功能和参数形成方面的差异。 我们看起来像:

 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作为转换器(文档中有指向其他受支持库的链接,该文档的链接将在本文结尾)。

build.gradle项目级别:

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

还有什么可以


例如,服务器返回一个错误,并且您具有前面示例中的代码。 在这种情况下,您将收到序列化错误,但是您可以配置客户端,以便当响应代码小于300时引发BadResponseStatus错误。 在构建客户端时,将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引擎
  • 引擎设定
  • 模拟引擎和测试
  • 授权模块
  • 独立的功能,例如在请求之间存储cookie等。
  • 不适用于Android的HTTP客户端的所有内容(其他平台,通过套接字工作,服务器实现等)。

Source: https://habr.com/ru/post/zh-CN432310/


All Articles