Moving to Kotlin Multiplatform (Part 3/4)

Using a custom HTTP engine with Ktor

Ronald Van Duren
4 min readMar 22, 2021

Out of the box, Ktor works great for almost all network requests. It also provides all kinds of nice features like response validation, a great way of writing requests, serialization, default HTTP engines etc. The default engines that Ktor supplies work great for making requests to third-party APIs. However, these client engines don’t allow us to communicate with our company APIs. We have to use an SDK that handles the security configuration for these APIs. At first, we thought that this might be a deal-breaker but lucky for us it is actually really easy to create a new HTTP engine. So let us get to the interesting stuff!

NetworkWrapper and Engine

Both the Android app and the iOS app will have to create an implementation to propagate the call to the networking SDK. We expose an interface from our KMM library to create this implementation. This interface allows us the be future-proof. If for any reason we have to change the implementation, we don’t have to change the code of our KMM library.

We also expose two data classes, we use these classes in favor of the standard HttpRequestData and HttpResponseData from Ktor because they are easier to use on both platforms. We make use of two mappers to map to and from these classes in our HTTP engine. The engine uses the default config, this suits our needs.

ByteArray to NSData

Ktor expects a ByteArray as the request body and so does our Request class. But on the iOS side, they use NSData. That means we needed to expose two functions on the iOS side to make sure they can convert a ByteArray to NSData and back.

Setting the content-type header

One of the things we ran into is that the content-type header is removed by the installed JsonFeature (JsonFeature.kt, line 144). Our API expects this header so this was a problem and manually setting this header does not seem to work (issue on Github), Ktor is quite opinionated that the content-type header should be set in the engine. Since we are exposing the Request class and using a custom engine, we can add the header by using an extension function on Ktor’s OutgoingContent in the RequestMapper.

Dependency injection

In part 2, I mentioned that the HTTP engine is initialized via an expected/actual class that initializes the KMM library. The implementation of the NetworkWrapper is passed to the HTTP engine and the HTTP engine is used to construct a Ktor HttpClient. This HttpClient is added to a Koin module so we can inject the client into our services. The following gist is an example of how to implement this with Koin.

As you can see below, it is very easy to create a service that makes use of the HttpClient. To free up resources, Ktor closes the used HttpClient after each request. Luckily, Koin constructs a new one for us for the next request!

Let’s start connecting the dots! Both platforms have to create an implementation for the NetworkWrapper. For Android, this looked similar to the following gist. We have to make use of a callback-based SDK so we are wrapping these callbacks in coroutines.

This implementation is passed the KMM library to construct the engine. So now, when we make a call with the HttpClient that uses this engine in our KMM library, that call is actually sent back to our SDK and the response is, in turn, send back to the engine. Inside the engine, the response object that we expose is mapped back to Ktor’s HttpResponseData so Ktor is able to handle the response and serialize the response body.

Wrapping it up

  • Using this construction gives us the flexibility we need by injecting an HttpClient with a default engine or the custom one.
  • We are able to benefit from all the nice features Ktor has!
  • We are exposing simpler classes that both platforms can handle well.
  • By exposing the NetworkWrapper interface we make sure that the HTTP engine is protected from implementation changes on Android and iOS.

Next: Final thoughts

--

--