Redundant DTO-Domain Mapping in Kotlin Flow
Separating layers is a best practice to write high-quality code, to prevent errors and to avoid unexpected behavior inside projects.
When fetching data from an API, we often parse the result into DTO class. Using a Mapper, we transform it into Domain class. Then, in other layers, we only consume this Domain object. This ensures the correct isolation of data between architecture layers.
Within a flow, we use map
to convert a DTO to a Domain object and consume it in downstream flows. We thought it might be elegant to avoid repeating these map
operators doing the same simple thing. We thought we could create a custom operator and map DTO to Domain with it in one line.
Mapping DTO-Domain 🎭
In Kotlin Flow, this mapping can be done using map
operator and a simple class extension.
In our DTO object, we should have all fields nullable. This can prevent an exception in case of a missing value from the API. For example, we could receive some user information and parse them into a DTO object as follows:
On the other side, with our Domain object, we choose to produce a data class without any optional fields. This will avoid to deal with nullable properties all over our layers, also to prevent multiple checks with ?
operators. Instead of checking for null
, we can deal with default value. So we should use this data class:
In a new Kotlin file, we make a DTO extension to build a Domain object from its data as a Mapper function:
Then, we map it in our flow thanks to this above extension:
In the downstream flows, we can now use Domain object instead of DTO object.
Fetch, Map, Repeat 🏋️
In Geev’s android app, we like to isolate operations specifically and separate each action into a single operator.
We often end up repeating the same simple line on each flows we collect.
This is a redundant task... Wouldn’t it be nicer to call .toDomain()
directly and to deal with the Domain object in downstream flows, hiding these blocks redundancies?
Attempt #1: interfaces ✨
Transform
allows us to create custom operator in Kotlin Flow.
This operator generalizes filter and map operators and can be used as a building block for other operators.
This is an extension of Flow
and takes two generic types: T
as initial value from a Flow<T>
and R
as final value in a Flow<R>
from a FlowCollector<R>
. It is created with a flow builder and applies the transform
suspended action on the flow’s value.
By using this powerful extension, we can create our own operators. So we could use it and transform our object like this:
Note that we renamed the DTO extension toDomain
to asDomain
to avoid confusion with our custom flow’s operator.
With it, our flow process becomes less verbose and avoids unnecessary noise.
While this is working, it is not generic, and can be used only for User
’s flow. In order to create a generic transform
function, we could create two interfaces and implement the asDomain
function for DTO classes.
So all our models should used these two interfaces, as these classes:
By modifying our custom operator to handle the interfaces instead of the models itself, we can now use this for all our flows.
fetchUserData
, fetchStatsData
and fetchArticleData
can use this flow’s extension and remove the redundant map
operator. Great!
Pros:
- Force us to implement asDomain
, then it should be never forget.
- It is generic and works with every class which has Dto
and Domain
.
Cons:
- The mapping function is inside the DTO class, increasing the noise.
- ALL data classes will have to implement this interface.
Attempt #2: extensions 💫
Although the previous attempt succeed, we cannot separate the mapping function from its class owner in a respective file, it is not possible with an interface.
To keep our asDomain
function separated from its class declaration, we could simply apply the same logic to the flow: have a custom extension of the flow.
Then our Mapper file, which already includes the DTO function, will also contain a flow extension.
We need a specific extension function for each DTO’s flows and we do not need to do it with transform
, a simple map
as before works well.
Then each Mapper files hold two functions: the DTO mapping to Domain function, and the flow custom extension to call it. Okay!
Pros:
- Keep the mapping functions inside a specific file.
- Easy to use and to understand.
Cons:
- Almost all flows will have to create an extension.
Attempt #3: generics ⭐️
The second attempt also works. However, we do not felt confortable to create an extension function for each of our flows. Why can’t we use a single flow extension?
We could create only one flow’s extension for all objects (Any
), pass the class type we need to retrieve, check with a when
statement what is the entered class type and do the asDomain
function if it suits.
We can also add an else
branch if the type class found is not referenced here. This will throw an error and force us to implement the mapping function for the type class.
Then, we could rewrite our flows to use this single extension and passing the expected Domain class in it.
This does the job: only one extension function for all, can be used with every flow, clears the process a little and easy to implement. Yes!
Pros:
- Force to implement it otherwise, this throws an exception.
- Clear a little the process for all flows.
Cons:
- Hard to maintain when it becomes to more than three classes.
- Must to write the class name into the flow process: less readable.
Results ⛳️
We tried to implement each of our attempts, they all have pros and cons. We do not found the clear way to do it, so we decide to stick with the simple map
operator. However, this is clearly a nice try and we enjoyed found other ways to do our mapping data. What about you, how do you map your DTO to Domain object in Kotlin Flow?
If you found this post helpful, feel free to clap! 👏 Thanks for reading.