Use Ktor for networking
Conventionally, modern applications with client-server architecture use the HTTP protocol for transferring data between the server and the client. If your mobile app has a server to exchange data with, an HTTP client is an essential part of this app that enables its interaction with the server.
For Kotlin projects, we recommend Ktor- a framework for building asynchronous clients and servers. It’s written in Kotlin and leverages its core features such as coroutines or targeting multiple platforms. For more detailed information, see the Ktor website.
In Kotlin Multiplatform Mobile (KMM) projects, you can use the Ktor Http Client for interactions with servers. On this page, we’ll take a brief tour on how to connect the Ktor client to a KMM project, create and configure an HTTP client, and perform network requests with it.
Connect Ktor
To use the Ktor HTTP client in your project, connect the client as a Gradle dependency: add the corresponding entries in the dependencies
block of a module’s build file (build.gradle
or build.gradle.kts
).
Ktor provides separate artifacts for using the HTTP client: a common module and different engines that process the network request.
To use Ktor KMM module in the common code, add the dependency to io.ktor:ktor-client-core to the commonMain
source set in the build.gradle
or build.gradle.kts
file of the KMM module:
val commonMain by getting { dependencies { implementation("io.ktor:ktor-client-core:$ktor_version") } }
commonMain { dependencies { implementation "io.ktor:ktor-client-core:$ktor_version" } }
Then connect the platform engines by adding the dependencies on them. For Android, add the ktor-client-android
dependency to the corresponding source set:
val androidMain by getting { dependencies { implementation("io.ktor:ktor-client-android:$ktor_version") } }
androidMain { dependencies { implementation "io.ktor:ktor-client-android:$ktor_version" } }
For iOS, add the ktor-client-ios
dependency to the corresponding source set:
val iosMain by getting { dependencies { implementation("io.ktor:ktor-client-ios:$ktor_version") } }
iosMain { dependencies { implementation "io.ktor:ktor-client-ios:$ktor_version" } }
Instead of $ktor_version
, use the required version of the library.
For more information about connecting the Ktor client to the multiplatform project, see the Ktor documentation.
Set up an HTTP client
In Ktor, HTTP clients are represented by the HttpClient
class. To create an HTTP client with default settings, call the HttpClient()
constructor:
val httpClient: HttpClient = HttpClient(CIO)
CIO
here is the class that represents an HTTP engine that the client will use. Let’s take a closer look at the available HTTP engines.
Select an engine
Ktor offers you multiple HTTP engines to use in your project: Apache, CIO, Android, iOS, and others. Engines differ by sets of supported features or platforms they work on. For the full list of supported HTTP engines, refer to the Ktor documentation.
To use a specific HTTP engine, connect the corresponding Ktor artifact as a dependency, for example:
dependencies { implementation("io.ktor:ktor-client-cio:$ktor_version") }
dependencies { implementation "io.ktor:ktor-client-cio:$ktor_version" }
Now you can create an HTTP client with this engine: just pass the engine class as an argument of the HttpClient()
constructor.
val client = HttpClient(CIO)
If you call the HttpClient()
constructor without an argument, then one of the engines available to Ktor will be automatically selected at compile time.
val httpClient: HttpClient = HttpClient()
Mock engine
Ktor offers a special HTTP engine for testing purposes - MockEngine
, which simulates HTTP calls without an actual connection to an API endpoint.
There are several platform-specific implementations of MockEngine
. To use them in your KMM project, connect the corresponding dependencies: io.ktor:ktor-client-mock-jvm
for Android io.ktor:ktor-client-mock-native
for iOS
dependencies { testImplementation("io.ktor:ktor-client-mock:$ktor_version") }
dependencies { api "io.ktor:ktor-client-mock:$ktor_version" }
Then create an HttpClient
instance with MockEngine
:
val httpClient: HttpClient = HttpClient(MockEngine)
For detailed information about testing with Ktor, refer to the Ktor documentation.
Configure the client
Client configuration can be done through a lambda expression with the receiver. In other words, the receiver object of the HttpClientConfig
class for a specific HTTP engine through which the entire configuration is performed will be transferred to the lambda, which is transferred as an argument to the HttpClient()
function.
To configure the client, pass a lambda expression to the HttpClient()
call.
val httpClient = HttpClient { expectSuccess = false ResponseObserver { response -> println("HTTP status: ${response.status.value}") } }
In this example, the following configuration is used: Receiving HTTP errors in response don’t cause exceptions A ResponseObserver
is created that prints response statuses to the standard output.
Engine configuration
When you create an HTTP client with a specific engine, pass the engine configuration in the same lambda in the engine
block.
val client = HttpClient(Android) { engine { connectTimeout = 100_000 socketTimeout = 100_000 proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("localhost", serverPort)) } }
For more information on engines configuration, see the Ktor documentation.
Features
Ktor lets you use additional HTTP client functionality (features) that is not available by default, for example, logging, authorization, or serialization. Most of them are distributed in separate artifacts. To use them, you should connect them as dependencies to the common source set. For example:
val commonMain by getting { dependencies { implementation("io.ktor:ktor-client-auth:$ktor_version") } }
commonMain { dependencies { implementation "io.ktor:ktor-client-auth:$ktor_version" } }
Then, add the required features in the client configuration using the install()
function.
val client = HttpClient() { install(Auth) { // providers config ... } }
For example, you can use the ResponseObserver
class to set up an observer for responses. At the beginning of the article, an observer was added using the ResponseObserver{}
builder function, which internally calls up the install
function. An observer as additional functionality can be explicitly added as follows:
val httpClient = HttpClient { install(ResponseObserver) { onResponse { response -> println("HTTP status: ${response.status.value}") } } }
For the full list of available HTTP client features and instructions on their configuration, see the Ktor documentation.
Create HTTP requests
The main function for creating HTTP requests is request
- an extension function for the HttpClient
class. All the request settings are generated using the HttpRequestBuilder
class. The request
function has the suspend
modifier, so requests can be executed in coroutines. For detailed information about creating and sending requests with the Ktor client, see the Ktor documentation.
Method
To define an HTTP method (for example, GET
or POST
) for a request, provide a value for the method
property: a GET
request whose result comes as a string:
val htmlContent = httpClient.request<String> { url("https://en.wikipedia.org/wiki/Main_Page") method = HttpMethod.Get }
a POST
request:
val response = httpClient.post<HttpResponse>("http://127.0.0.1:8080/") { headers { append("Authorization", "token") } body = "Command" }
Ktor provides extension functions for the HttpClient
class for using basic HTTP methods: get
, post
, put
, patch
, delete
, options
, head
. This is how you use them to send a GET
request:
val response = httpClient.get<HttpResponse>("http://127.0.0.1:8080/") { headers { append("Accept", "application/json") } }
Headers
To add headers to the request, use the headers
extension function.
val htmlContent = httpClient.request<String> { url("https://en.wikipedia.org/wiki/Main_Page") method = HttpMethod.Get headers { append("Accept", "application/json") append("Authorization", "oauth token") } }
Body
To set the body of a request, assign a value to the body
property in the HttpRequestBuilder
class. You can assign a string or an OutgoingContent
object to this property. For example, sending data with a text/plain
text MIME type can be implemented as follows:
val htmlContent = httpClient.request<String> { url("http://127.0.0.1:8080/") method = HttpMethod.Post body = TextContent( text = "Body content", contentType = ContentType.Text.Plain ) }
Response type
To obtain more information in the response, such as HTTP status, you can use the HttpResponse
type as the request result:
val response = httpClient.request<HttpResponse> { url("https://en.wikipedia.org/wiki/Main_Page") method = HttpMethod.Get } if (response.status == HttpStatusCode.OK) { // HTTP-200 }
For more information about the HttpResponse
, refer to the Documentation.
You can also obtain the request results in the form of a byte array:
val response = httpClient.request<ByteArray> { url("https://en.wikipedia.org/wiki/Main_Page") method = HttpMethod.Get }
Multipart requests
To send a multipart request, pass a MultiPartFormDataContent
object to the body
property. Create this object by calling the MultiPartFormDataContent()
constructor with the argument parts: List<PartData>
. To create this list, use the FormBuilder
builder class. It provides multiple variations of the append
function for adding the data. There is also formData
builder function, which accepts a lambda with the FormBuilder
receiver.
An example of creating a POST request with Multipart data may look as follows:
val request: String = httpClient.post("http://127.0.0.1:8080/") { body = MultiPartFormDataContent( formData { append("key", "value") } ) }
Concurrency
The Ktor API is based on suspend
functions, so Kotlin coroutines are used when working with asynchronous requests. Therefore, all requests must be executed in coroutines, which will suspend their execution while awaiting a response.
For concurrent execution of two or more requests, you can use coroutine builders: launch
or async
. For example, sending two concurrent requests using async
might look as follows:
suspend fun parallelRequests() = coroutineScope<Unit> { val httpClient = HttpClient() val firstRequest = async { httpClient.get<ByteArray>("https://127.0.0.1:8080/a") } val secondRequest = async { httpClient.get<ByteArray>("https://127.0.0.1:8080/b") } val bytes1 = firstRequest.await() // Suspension point. val bytes2 = secondRequest.await() // Suspension point. httpClient.close() }
Close the HTTP client
After you finish working with the HTTP client, don’t forget to free up the resources that it uses: threads, connections, and CoroutineScope
for coroutines. To do this, call up the close()
function in HttpClient
:
httpClient.close()
If you need to use HttpClient
for a single request, call the extension function use()
that will automatically calls close()
after executing the code block:
val status = HttpClient().use { httpClient -> // ... }
Note that the close
function prohibits the creation of new requests, but doesn’t terminate currently active ones. Resources will only be released after all client requests are completed
We'd like to thank the IceRock team for helping us write this article.
© 2010–2021 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/kmm-use-ktor-for-networking.html