Clean MVVM Architecture (part 2)
Hi again. This is the second part of articles about using clean architecture with MVVM and custom tech-stack. The first part you can find here
In this article, we going to talk about THE DATA FLOW that takes place in our clean architecture. In general, the choice of the mechanism of interaction with data in clean architecture should not depend on the choice of approach to the development of the application and user interactions. It can be either MVP or MVVM or MVI patterns. In fact, it does not matter. Only the presentation level changes. I choose MVVM cause it’s a little bit hyped today as like as MVI ;)
Thin downward arrows in the figure marked input dependencies. A large thick arrow indicates an upstream data flow, which is carried out by transferring a Data Transfer Objects (next — DTO) across architectural boundaries, such as local and remote data storages, repositories, use-cases, and user interface. Now let’s look at one of the main components of our architecture, the data transfer object.
This is a Kotlin sealed class, that can store our results and transfer it throw architecture boundaries mapping the necessary object one to another. We will see how it can be later in this section. DTO represents the states of our data such as Loading
, Empty
, Error
and Success
results. Of course, there are can be more than four extended classes, and this is just an example. I simplify getting these types by wrapping it in high order and inline functions.
Domain level
The domain level is such type of abstraction where we can manipulate our data flow. Let’s get see the simple example of how our use-case work.
GetGenresUseCase.kt
is a combined use-case that can do two actions: get already saved data and if there is no data, take it from the remote server. But on the fact, it did only single work, - get our data for presentation level. It doesn’t break the Single Responsibility Principle. In this class, I use suspend function called execute
cause we cannot extend suspend function, but in the next version of Kotlin it could be possible (i hope) and we will use class GetGenresUseCase : suspend ()->T
or it’s Typealias. We already injected all of our dependencies through Kodein in the first part, so I will not pay attention to this. So let’s look at the included use-cases.
From repository.getLocalGenreList(): SResult<List<GenreEntity>>
takes the DTO with a list of our local entity models and maps it to the necessary DTO with the list of data for another layer - SResult<List<GenreUI>>
. Here is a very important part, in that each layer of the architecture should work only with its inherent data and provide the necessary data format for the next layer.
Another GetRemoteGenresUseCase.kt
get data from remote service by repository implementation who produce only local entities, check it for success result (SResult.Success
) and if it is not empty, save our data to local data storage. This is his single function!
Look at the picture below:
As you can see, Data Layer works with two types of data, — Remote Model and Local Entity, but produce only Local Entity; Domain Layer work with Local Entities, it can get, save, sort, filter, etc and manipulate this type of data as it logic includes, then it should produce UI Model for Presentation Layer. We follow two principles of “SOLID” to map one type of data to another: OCP (The Open Closed Principle) and LSP (The Liskov Substitution Principle).
We use this interface to hide converting realization:
interface IConvertableTo<T> {
fun convertTo(): T?
}
Now, fun mapListTo()
is not interested in the types of objects passed into List<I>
. It simply calls their method convertTo(): O?
. All it knows about types is that the objects it processes should belong to the interface IConvertableTo<O>
or its implementations. And this is a current example of how we can do that:
Now each data class itself decides how to convert one data to another. We cannot map our data if there is a null value in val id:Int?
or val name: String?
at the class GenreModel.kt
, cause this is the main parameters that we use in UI Layer (GenreUI.kt
) to show some information. We also use this.data.mapNotNull
to exclude this situation. Of course, you can choose another mapping method or you can throw an error or filter empty entities in special use-cases.
The picture illustrates how data flow and mutate from one boundary to another. This is the safest way to transfer data with DTO because each layer is only responsible for the consumption and production of its needed data types.
At this point, I want to finish the second part of the Clean Architecture articles.
Conclusion
We looked at how to make our domain layer more flexible, dividing the levels of responsibility and combining them, which gives us the opportunity to write flexible systems and repeatedly reuse the use-cases. We also saw how the Data Transfer Object (DTO) might look and how it is possible to mutate the data passing it across the architectural boundaries without breaking the principles of clean architecture. In the final topic, I will discuss the last architecture layer called Data Layer. It will be interesting. Also, I will show to you some UI tricks with AnkoLayout from JetBrain.
There is a link to the repository:
Thanks for reading this story. I hope you learned something new for yourself. It will be good if you give me some feedback and your recommendations for improving this article.
Please, click a CLAP and SHARE this post if you like it:)
Have a good day, coders!
The third part is here: