As an Android developer, I constantly write API clients:
90% of all mobile apps fetch some JSON, parse it and show it in a list ¯\_(ツ)_/¯
To keep a clear separation between what’s coming from the network and what I want in my domain, I usually have a DTO and a Domain Model.
DTO and Domain Model
The DTO (Data Transfer Object) represents the JSON coming from the API. Something like this:
data class Address( @SerializedName("street") val street: String )
This later gets mapped to a Domain equivalent that looks like this:
data class Address(val street: String)
This is an fairly common scenario 👍
More types
Something that is getting more common in my daily work is receiving from BE an object that can actually be only one of N possible variations. Something like this:
data class Address( @SerializedName("street") val street: String, @SerializedName("road_type") val roadType: String )
where type
is indeed a String
, but it can actually be only one of these valid possibilities:
- city
- countryside
- gravel
- uncharted
In our Domain we don’t want those strings going around, instead we prefer to use types. Types allow us to do exhaustive pattern matching and keep things a bit more deterministic.
As a side-note, we still need to keep those string values for business reasons, i.e. send it as a tracking parameter. We move from the raw string values to a sealed class
:
sealed class RoadType (val type: String) { object City : RoadType("city") object Countryside : RoadType("countryside") object Gravel : RoadType("gravel") object Uncharted : RoadType("Uncharted") object Unknown : RoadType("UNKNOWN") }
We cover every possibility and we also add an Unknown
case to keep the whole class back-compatible.
How do we go from “city” to City?
That was my main question: how do I go from city
to City
? My first solution was the classic:
fun mapToDomain(dtoType: String): RoadType { return when (dtoType) { "city" -> RoadType.City "countryside" -> RoadType.Countryside "gravel" -> RoadType.Gravel "uncharted" -> RoadType.Uncharted else -> RoadType.Unknown } }
It works and it’s familiar to any developer skill-level. Problem is that it scales linearly.
You add one more type on the backend and you add one more type to your domain. That’s fair. You want to keep things in sync.
However, what I don’t want to do is unnecessary maintenance: I don’t want to keep adding things to mapToDomain
.
There must be a way to loop over a sealed class!!
Old dev yelling at clouds
It turns out, there is a way! You need to add this to your build.gradle
:
implementation org.jetbrains.kotlin:kotlin-reflect:${kotlin_version}
This allows you to access to some fancy reflection based trick and you can write something like this:
sealed class RoadType (val type: String) { [...] companion object { fun fromRoadString(possibleType: String): RoadType { return RoadType::class.sealedSubclasses .firstOrNull { it.objectInstance?.type == possibleType } ?.objectInstance ?: Unknown } } }
- We create a
companion object
in the sealed class - We create a function that takes a string, i.e. “city” and gives you back a
RoadType
, i.e.City
- We get every sub-class for the type
RoadType
🤯 - We take the first instance where the
type
property matches thepossibleType
we are passing in, i.e. “city” - If we have a match, we return the type itself
- If we have no match, i.e.
null
, we returnUnknown
That’s all! This is gonna work even when you add more RoadTypes
and it’s a total function, so you can write some Unit Test, if you want.
This little trick is helping a lot lately and it’s saving me so much time and stress. I hope it can help you as well.
Happy coding 😉
Awesome
Thanks ☺
Well, this will only work if you only have objects as your sealed subclasses (in which case you could have used enum as well). It’s not that straight-forward when you use classes having some constructor parameters etc.
There are almost no benefits in using a sealed class with only object subclasses compared to a classic enum. And your code relies on returning only objects.
So you should recommend using enums instead of sealed classes for looping, especially because Kotlin.reflect is a big dependency (much bigger than the Kotlin standard library itself) that can impact the size of an Android app, if your code is targeting Android.
Using reflection should be avoided when possible on Android and can cause issues if you’re using R8 or Proguard without providing extra rules to prevent shrinking/removing the classes accessed via reflection.