Clean MVVM Architecture (part 3)

Alexandr Minkin
6 min readJul 14, 2019

Hello everyone. This is a final part of stories about clean architecture and MVVM on Android. In the previous part, we talked about Domain Layer. In this article, I want to talk about Data Layer which is the main layer who can retrieve data streaming in the application. Data and Domain Layer has closely coupled with each other by abstractions. As you can remember, the level of domain logic contains the rules by which we can manipulate data (filter, sort and also provide them for the presentation layer), it can be reused many times by other parts of our application, and it can also consist of nested uses-cases. The data level provides us with the methods by which we can receive data, as well as save them to the local storage (if provided), and also provide the level of abstraction for our providers for which we receive data. It’s divided into two parts — Repository and Data Storages (such as Remote, Local or Memory Data Storages).

These objects are separated from the repository by the necessary level of abstraction since we cannot guarantee that they will not change in the future, but on the other hand, their implementation should not depend on what data we receive. For example, today we can use the Room from Google, and tomorrow a new database will be released and we will want to use it. That is why this layer should be separated from the repository. Lets’s look at our implementation of GenresRepository.kt:

as you can see we use only interfaces for Local and Remote Data Sources.

interface IGenresRemoteDataSource {
suspend fun getRemoteGenresList(): SResult<List<GenreModel>>
}
interface IGenresLocalDataSource {
suspend fun getGenresList(): SResult<List<GenreEntity>>
suspend fun insertGenres(data: List<GenreEntity>)
}

Here we applied the DIP: The Dependency Inversion Principle

The modules of the upper levels should not depend on the modules of the lower levels. Both types of modules should depend on abstractions.

Abstractions should not depend on the details. Details must depend on abstractions.

I recommend creating a separate repository for each data type, as well as local and remote data storage that we inject into our repository.

remote data source implementation

I use Retrofit to get our remote data from the server and I prefer to use Moshi converter for our data models. Also, I use gildor’s sealed classes for better response handling. But in the newest version of retrofit (since 2.6.0), you can use suspend function without any third-party libraries. Thus, our remote data storage is responsible only for the logic of receiving a response from the server and its processing. In the future, if we want to use a different algorithm for processing server requests, all we need to do is implement the interface IGenresRemoteDataSource.kt and override the function suspend fun getRemoteGenresList(): SResult<List<GenreModel>>. Simple and flexible, right?! And now let’s get see the local data sourceGenresLocalDataSource.kt:

So what if we need to store our data in local memory list, like array list, for better performance. We can extend IGenresLocalDataSource.kt and make something like this:

interface IGenresLocalDataSource {
suspend fun getGenresList(): SResult<List<GenreEntity>>
suspend fun insertGenres(data: List<GenreEntity>)

suspend fun putGenresInCache(genresList: List<GenreEntity>)
suspend fun getGenresFromCache(): SResult.Success<List<GenreEntity>>
}

Here, the separation of functions with different responsibilities may entail the worst consequences. Suppose again that tomorrow we changed the database and rewrote the class of local resources, in this case, we will also need to add the implementation of storing the data from the new database to the cache, and these are unnecessary maintenance costs or potential errors. The best solution here is to apply the ISP (Interface segregation principle), which says:

Clients should not depend on methods that they do not use.

Now we can have two independent interfaces and accompany them separately, which gives us more flexibility and suitable for refactoring. I would also add that the correct use of ISP allows you to create optimal software models: to maintain a balance between minimum granularity and maximum code reuse.

interface IGenresCacheDataSource {
suspend fun putGenresInCache(genresList: List<GenreEntity>)
suspend fun getGenresFromCache(): SResult.Success<List<GenreEntity>>
}
interface IGenresLocalDataSource {
suspend fun getGenresList(): SResult<List<GenreEntity>>
suspend fun insertGenres(data: List<GenreEntity>)
}

We also share responsibility for their implementation if we using them separately in our repository.

Now that how we have dealt with the DataLayer. I want to go back to the presentation layer again and show how easy and convenient we can create our ViewHolders.

Presentation Layer (ViewHolders)

To work with data mapping, I use AnkoLayout and Mike Penz library, FastAdapter. It is truly convenient and easy to use, especially since the latest versions are already fully translated into Kotlin. Since I have already created a base class for the adapter which will contain our maps

From previous articles, you noticed that I use the GenreUI.kt model for the presentation layer, let’s take a closer look at its contents.

As you can see, I inherit from the base class BaseRecyclerUI.kt and override some of its methods. Look at how I create a view instance from AnkoLayoutGenreViewHolder.createView(AnkoContext.Companion.create(ctx, this)) . Here I use fun createView(ui: AnkoContext<GenreUI>) from ViewHolder and pass needed AnkoContext that was created from AnkoContext companion object function create(ctx: Context, owner: T, setContentView: Boolean = false) . This is the easiest way to create our view.

We don’t need to set layoutRes cause we using AnkoLayout for our ViewHolders, and you can set type for specific items as you want. This is a simple FastAdapter item realization. The main interest is GenreViewHolder.kt

I use the companion object to describe how our view layout will look like. And I don’t need to create another object) And after we described our interface and data assignment in the fun bindView(item: GenreUI, payloads: MutableList<Any>) , all we need to do is create instances of this class and pass them to the fast adapter object by calling itemAdapter.setNewList (see BaseRecyclerController.kt)

AnkoLayout View

This is another example of more complex UI using MovieUI.kt model. You can see full code into my repository.

AnkoLayout UI

I love ANKO for its declarative style of interface description, but the project itself is developing very slowly and I am afraid that soon the company JetBrains will not support it anymore since Google released the new library Compose.

Conclusion

In all three articles, we have examined in sufficient detail the principles of building a clean architecture, as well as the usage of SOLID principles in the MVVM pattern. We also examined which objects can cross architectural boundaries (DTO), how to build a layout according to the declarative style (DSL) with the help of AnkoLayouts.
In the end, I want to say a few words about architecture as a whole:

Architecture is the most important part of software development. This is the skeleton on which most of your program is built. A clean architecture and adherence to the principles of SOLID development, allows us to create flexible and easy-to-maintain software products.

And of course:

“If you think that good architecture is expensive, try bad architecture.”
— Brian Foote and Joseph Yoder

“Hurry unhurriedly.” — Robert Martin

Thanks for reading these articles. I hope everyone discovered something new for yourself. Please make a CLAP and don’t forget to be amazing!)

Link to the repository:

--

--

Alexandr Minkin

Lead Software Developer. Kotlin enthusiast, Android developer, Swift lover, Clean Architecture Supporter