📜 ⬆️ ⬇️

Ktor as HTTP client for Android

Retrofit2, as an Android developer, I like it, but what about trying to use Ktor as an HTTP client? In my opinion, for Android development, it is not worse and not better, just one of the options, although if you wrap everything up a bit, it can turn out very well. I will consider the basic features with which you can start using Ktor as an HTTP client - creating requests of various kinds, receiving raw answers and responses in the form of text, deserializing json into classes through converters, logging.



If in general, Ktor is a framework that can act as an HTTP client. I will consider it from the development side for Android. It is unlikely that you will see very complex use cases below, but the basic features are accurate. The code from the examples below can be viewed on GitHub .

Ktor uses korutin from Kotlin 1.3, a list of available artifacts can be found here , the current version is 1.0.1 .
For requests I will use HttpBin .
')

Simple use


To get started, you will need basic dependencies for the Android client:

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

Do not forget to add information in Manifest that you are using the Internet.

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

Let's try to get the server response as a string, what could be simpler?

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

You can create a client without parameters, just create an instance of HttpClient() . In this case, Ktor will select the required engine himself and use it with the default settings (we have one connected engine - Android, but there are others, for example, OkHttp).
Why korutiny? Because get() is a suspend function.

What can be done next? You already have data from the server as a string, you just have to parse it and get classes that you can work with. It seems to be easy and fast in this case of use.

We get a raw answer


Sometimes it is necessary to get a set of bytes instead of a string. Experiment with asynchrony at the same time.

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

In places where HttpClient methods are HttpClient , such as call() and get() , await() will be called under the hood. So in this case, the calls simpleCase() and bytesCase() will always be consistent. It is necessary in parallel - just wrap each call into a separate coruntine. In this example, new methods have appeared. A call(GET_UUID) will return to us an object from which we can get information about the request, its configuration, response, and customer. The object contains a lot of useful information - from the response code and the protocol version to the channel with the same bytes.

Do you need to close it somehow?


Developers indicate that to correctly shut down the HTTP engine, you must call the close() method on the client. If you need to make one call and immediately close the client, you can use the use{} method, since HttpClient implements the Closable interface.

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

Examples besides GET


In my work, the second most popular method is POST . Consider the example of setting parameters, headers and the request body.

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

In fact, in the last parameter of the post() function, you have access to the HttpRequestBuilder , with which you can create any request.
The post() method simply parses the string, converts it to a URL, explicitly specifies the type of the method, and makes the request.

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

If you run the code from the last two methods, the result will be the same. The difference is not great, but it is more convenient to use wrappers. The situation is similar for put() , delete() , patch() , head() and options() , so we will not consider them.

However, if you look closely, you can see that there is a difference in typing. When you call call() you get a low-level response and you have to read the data yourself, but what about automatic typing? After all, we are all used to connect a converter in Retrofit2 (such as Gson ) and specify the return type as a specific class. We'll talk about conversion into classes later, but the request method will help typify the result without being tied to a specific HTTP method.

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

Submitting form data


Usually, you need to pass parameters either in the query string or in the body. In the example above, we have already considered how to do this with the HttpRequestBuilder . But it can be easier.

The submitForm function accepts a url as a string, parameters for a query, and a boolean flag that tells you how to pass parameters — in the query string or as pairs in a form.

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

And what about multipart / form-data?


In addition to string pairs, you can pass as POST parameters for requesting numbers, arrays of bytes, and various Input streams. Differences in the function and the formation of parameters. See how:

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

As you can see, you can attach a set of headers to each parameter.

We deserialize the answer to the class.


It is necessary to get some data from the query not in the form of a string or byte, but immediately converted into a class. For a start, we are recommended in the documentation to connect the feature of working with json, but I want to make a reservation that jvm needs a specific dependency and without kotlinx-serialization, all this will not take off. I suggest using Gson as a converter (links to other supported libraries are in the documentation, links to the documentation will be at the end of the article).

build.gradle project level:

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

build.gradle application level:

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

Now let's execute the query. From the new there will be only a connection feature of working with Json when creating a client. I will use open weather API. For completeness, show the data model.

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

And what else can


For example, the server returns an error, and you have the code as in the previous example. In this case, you will get a serialization error, but you can configure the client so that when the response code <300, the BadResponseStatus error is BadResponseStatus . It is enough to set the expectSuccess client to true when building the client.

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

When debugging, logging may be useful. It is enough to add one dependency and tune the client.

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

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

We specify the DEFAULT logger and everything will fall into the LogCat, but you can redefine the interface and make your logger if desired (although I didn’t see any great opportunities there, there is only a message at the entrance, but there is no log level). We also indicate the level of logs to be reflected.

References:


What is not considered:

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


All Articles