If you are starting a project with Kotlin Multiplatform and you want to share the network layer, the best way to go is definitely with Ktor. But if you don’t want to share the entire network layer but maybe only the DTOs? There could be many reasons for wanting this. Maybe you are starting to integrate Kotlin Multiplatform (I’ll call it KMP in the rest of the article) into an existing project and the work for sharing the entire network layer is simply too much.
And this was the case for the project that I’m working on. We decided to start integrating KMP and we thought that the perfect target to start with is the DTOs. Because in this way we can define a single source of truth and share it on the backend and the mobile clients. But how to start using KMP in an existing project, is a topic for another article, so stay tuned!
In this article, I will show you how to implement a Kotlin Multiplatform Mobile application that performs a network call on the native side with Retrofit (on Android) and Alamofire (on iOs) but the DTOs are defined on KMP side as well as the information about deserialization. And for the deserialization, I will use (of course) the Kotlin Serialization library.
API
For this example I will use the Bored Api that returns this kind of response:
|
|
And this response can be mapped to a simple data class:
|
|
And this data class is placed inside the shared KMP module.
Android
Now, let’s move to the Android side and I start with Android because things are simpler. In fact, you can use Retrofit and the Kotlin Serialization Converter. All you need to do is add the Converter Factory
for the Kotlin Serialization.
|
|
iOs
On iOs the equivalent to Retrofit is Alamofire. Alamofire let you easily handle the deserialization of the responses (and of course also the serialization of the requests) with the Decodable
protocol (and Encodable
- or Codable
to support both Encodable
and Decodable
at the same time). For more information about Codable
, I suggest you to look at the official documentation.
But unfortunately there is no Codable support on Kotlin/Native (maybe it will come with direct interoperability with Swift - Kotlin Roadmap).
Custom Response Deserialization with Alamofire
Fortunately, Alamofire gives the possibility to write a custom response serializer. The starting point is a struct
that extends ResponseSerializer
; this struct
overrides the serialize
method, which “does some magics” and returns the desired deserialized object, represented by the generic T
.
|
|
Before performing the object deserialization, a string representation of the response must be computed. To do that, I will use the StringResponseSerializer
provided by Alamofire.
|
|
And then, this string will be sent to a Kotlin helper function that performs the actual deserialization.
|
|
And at the end, the custom Alamofire deserializer will look something like this (with also a bit of error handling):
|
|
And then, the ViewModel can make the network request using the custom serializer.
|
|
Deserialization on Kotlin/Native
Now let’s move back to KMP, and let’s implement the decodeFromString
function mentioned above.
The first thing that popped into my mind is to use an inline reified
function that works with generics (for more info about inline functions and reified parameters, give a look to the Kotlin documentation).
|
|
But unfortunately, this approach does not work because Swift doesn’t have inline
functions support.
|
|
So, after a bit of exploring of the Kotlin Serialization documentation and sources, I’ve discovered that there is the possibility to get the serializer of a KClass
(KClass<T>.serializer()
) and then pass it to the decodeFromString
function.
|
|
This approach works! But unfortunately, the KClass<T>.serializer()
is an internal API. And (as stated in the documentation) it doesn’t work with generic classes, lists, custom serializers, etc (I’ve opened an issue on GitHub just to be sure).
So, given the limitations of using an internal API, I’ve decided to change (again!) approach. Since it is hard to create generic deserialization, it is better to specify the deserialization information for every DTO. To do that, I have defined an abstract class with an abstract deserialize method that every DTOs has to implement.
|
|
So, the Activity
class defined above need to override the deserialize
method.
|
|
Now, some modifications must be made to the custom Alamofire deserializer. First of all, the accepted generic type is not T
only, but T
that inherits from BaseResponseDTO
|
|
In this way, we can retrieve the serializer from the abstract class, deserialize the object and return it.
|
|
And finally, this works!
Here’s the full code of the updated serializer.
|
|
If you want to see all in action, I’ve published a little sample on my GitHub.
In the end, the result is a bit more boilerplate than what I’ve expected but not so much. I think that the benefits of having the DTOs defined in one place for both the clients and the backend are way higher than the “burden” of writing a bunch of lines of code for every DTOs.
If you have any suggestion to improve that solution or you have any kind of doubt, feel free to reach me out on Twitter @marcoGomier.