Eu gosto do Retrofit2 como desenvolvedor Android, mas que tal tentar obter a qualidade do cliente HTTP Ktor? Na minha opinião, para o desenvolvimento do Android, não é pior e nem melhor, apenas uma das opções, embora, se você embrulhar tudo um pouco, pode resultar muito bem. Vou considerar os recursos básicos com os quais será possível começar a usar o Ktor como um cliente HTTP - criando vários tipos de solicitações, recebendo respostas brutas e respostas na forma de texto, desserializando o json em classes por meio de conversores e registrando.

Em geral, o Ktor é uma estrutura que pode atuar como um cliente HTTP. Vou considerá-lo do lado do desenvolvimento para o Android. É improvável que você veja abaixo casos de uso muito complexos, mas os recursos básicos são precisos. O código dos exemplos abaixo pode ser visualizado no
GitHub .
O Ktor usa as corotinas do Kotlin 1.3, uma lista de artefatos disponíveis pode ser encontrada
aqui , a versão atual é
1.0.1
.
Para consultas, usarei o
HttpBin .
Uso simples
Para começar, você precisará de dependências básicas para o cliente Android:
implementation "io.ktor:ktor-client-core:1.0.1" implementation "io.ktor:ktor-client-android:1.0.1"
Não se esqueça de adicionar ao Manifest informações que você usa a Internet.
<uses-permission android:name="android.permission.INTERNET"/>
Vamos tentar obter a resposta do servidor como uma string, o que poderia ser mais fácil?
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) } }
Você pode criar um cliente sem parâmetros, basta criar uma instância de
HttpClient()
. Nesse caso, o Ktor selecionará o mecanismo desejado e o utilizará com as configurações padrão (temos um mecanismo conectado - Android, mas existem outros, por exemplo, OkHttp).
Por que corotinas? Porque
get()
é uma função de
suspend
.
O que pode ser feito a seguir? Você já possui dados do servidor na forma de uma sequência, basta analisá-los e obter classes com as quais já pode trabalhar. Parece ser simples e rápido neste caso de uso.
Temos uma resposta bruta
Às vezes, pode ser necessário obter um conjunto de bytes em vez de uma string. Ao mesmo tempo, experimente a assincronia.
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() }) }
Nos locais onde os métodos
HttpClient
são
HttpClient
, como
call()
e
get()
,
await()
serão chamados sob o capô. Portanto, nesse caso, as chamadas para
simpleCase()
e
bytesCase()
sempre serão seqüenciais. Você precisa disso em paralelo - basta agrupar cada chamada em uma rotina separada. Neste exemplo, novos métodos apareceram. A
call(GET_UUID)
retornará um objeto a partir do qual podemos obter informações sobre a solicitação, sua configuração, resposta e cliente. O objeto contém muitas informações úteis - do código de resposta e da versão do protocolo ao canal com os mesmos bytes.
Você precisa fechá-lo de alguma forma?
Os desenvolvedores indicam que, para que o mecanismo HTTP seja desligado corretamente, você precisa chamar o método
close()
no cliente. Se você precisar fazer uma chamada e fechar imediatamente o cliente, poderá usar o método
use{}
, pois o
HttpClient
implementa a interface
Closable
.
suspend fun closableSimpleCase() { HttpClient().use { val data: String = it.get(GET_UUID) Log.i("$BASE_TAG Closable case", data) } }
Exemplos além de GET
No meu trabalho, o segundo método mais popular é o
POST
. Considere o exemplo de configuração de parâmetros, cabeçalhos e corpo da solicitação.
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")
De fato, no último parâmetro da função
post()
, você tem acesso ao
HttpRequestBuilder
, com o qual pode formar qualquer solicitação.
O método
post()
simplesmente analisa a string, converte-a em uma URL, define explicitamente o tipo do método e faz uma solicitação.
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) }
Se você executar o código dos dois últimos métodos, o resultado será semelhante. A diferença não é grande, mas o uso de wrappers é mais conveniente. A situação é semelhante para
put()
,
delete()
,
patch()
,
head()
e
options()
, portanto, não as consideraremos.
No entanto, se você olhar com atenção, poderá ver que há uma diferença na digitação. Ao ligar para
call()
você recebe uma resposta de baixo nível e deve ler os dados por conta própria, mas e a digitação automática? Afinal, estamos acostumados a conectar um conversor (como o
Gson
) no Retrofit2 e a indicar o tipo de retorno como uma classe específica. Falaremos sobre a conversão para classes mais tarde, mas o método de
request
ajudará a tipificar o resultado sem vincular a um método HTTP específico.
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) }
Enviar dados do formulário
Geralmente você precisa passar parâmetros na cadeia de consulta ou no corpo. No exemplo acima, já examinamos como fazer isso usando o
HttpRequestBuilder
. Mas pode ser mais fácil.
A função
submitForm
aceita o URL como uma sequência, parâmetros para a solicitação e um sinalizador booleano que informa como passar parâmetros - na sequência de consultas ou como pares no formulário.
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)
Mas e os dados multipart / form?
Além dos pares de cadeias, você pode passar como parâmetros números de solicitação POST, matrizes de bytes e vários fluxos de entrada. Diferenças na função e formação de parâmetros. Nós parecemos:
suspend fun submitFormBinaryCase(client: HttpClient) { val inputStream = ByteArrayInputStream(byteArrayOf(77, 78, 79)) val formData = formData { append("String value", "My name is")
Como você deve ter notado - você também pode anexar um conjunto de cabeçalhos a cada parâmetro.
Desserialize a resposta para a classe
Você precisa obter alguns dados da solicitação, não como uma string ou bytes, mas imediatamente convertidos em uma classe. Para começar, na documentação, recomendamos conectar um recurso para trabalhar com o json, mas quero fazer uma reserva de que o jvm precisa de uma dependência específica e sem a serialização do kotlinx tudo isso não decolará. Sugiro usar o Gson como um conversor (há links para outras bibliotecas suportadas na documentação, os links para a documentação estarão no final do artigo).
nível do projeto build.gradle:
buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } allprojects { repositories { maven { url "https://kotlin.bintray.com/kotlinx" } } }
nível do aplicativo 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" }
Agora execute a solicitação. A partir do novo, haverá apenas uma conexão do recurso de trabalhar com Json ao criar o cliente. Vou usar a API do clima aberto. Para completar, mostrarei o modelo de dados.
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()) }
E o que mais pode
Por exemplo, o servidor retorna um erro e você tem o código como no exemplo anterior. Nesse caso, você receberá um erro de serialização, mas poderá configurar o cliente para que um erro
BadResponseStatus
seja lançado quando o código de resposta for <300. É suficiente definir o
expectSuccess
como
true
ao criar o cliente.
val client = HttpClient(Android) { install(JsonFeature) { serializer = GsonSerializer() } expectSuccess = true }
Ao depurar, o log pode ser útil. Basta adicionar uma dependência e configurar o cliente.
implementation "io.ktor:ktor-client-logging-jvm:1.0.1"
val client = HttpClient(Android) { install(Logging) { logger = Logger.DEFAULT level = LogLevel.ALL } }
Especificamos o logger DEFAULT e tudo será direcionado ao LogCat, mas você pode redefinir a interface e criar seu próprio logger, se desejar (embora eu não tenha visto grandes oportunidades lá, há apenas uma mensagem na entrada, mas não há nível de log). Também indicamos o nível de logs que precisam ser refletidos.
Referências:
O que não é considerado:
- Trabalhar com o mecanismo OkHttp
- Configurações do mecanismo
- Mecanismo e testes simulados
- Módulo de autorização
- Recursos separados, como armazenamento de cookies entre solicitações, etc.
- Tudo o que não se aplica ao cliente HTTP para Android (outras plataformas, funciona através de soquetes, implementação de servidor etc.)